From 658559699f5cd482bb19ade298db43a65d750664 Mon Sep 17 00:00:00 2001 From: daniel oeh Date: Sat, 11 Oct 2014 17:43:07 +0200 Subject: Moved core classes into subproject --- app/build.gradle | 6 +- app/core/build.gradle | 83 -- app/src/main/AndroidManifest.xml | 4 +- .../aocate/presto/service/IDeathCallback_0_8.aidl | 18 - .../IOnBufferingUpdateListenerCallback_0_8.aidl | 19 - .../service/IOnCompletionListenerCallback_0_8.aidl | 19 - .../service/IOnErrorListenerCallback_0_8.aidl | 19 - .../service/IOnInfoListenerCallback_0_8.aidl | 19 - ...stmentAvailableChangedListenerCallback_0_8.aidl | 19 - .../service/IOnPreparedListenerCallback_0_8.aidl | 19 - .../IOnSeekCompleteListenerCallback_0_8.aidl | 19 - ...stmentAvailableChangedListenerCallback_0_8.aidl | 19 - .../com/aocate/presto/service/IPlayMedia_0_8.aidl | 75 -- .../java/com/aocate/media/AndroidMediaPlayer.java | 470 ------- .../main/java/com/aocate/media/MediaPlayer.java | 1296 ------------------ .../java/com/aocate/media/MediaPlayerImpl.java | 118 -- .../com/aocate/media/ServiceBackedMediaPlayer.java | 1170 ---------------- .../com/aocate/media/SpeedAdjustmentAlgorithm.java | 31 - .../main/java/de/danoeh/antennapod/PodcastApp.java | 1 - .../antennapod/activity/AudioplayerActivity.java | 4 +- .../antennapod/activity/FeedInfoActivity.java | 2 +- .../danoeh/antennapod/activity/MainActivity.java | 2 +- .../antennapod/activity/MediaplayerActivity.java | 2 +- .../activity/OpmlImportBaseActivity.java | 4 +- .../antennapod/activity/PreferenceActivity.java | 2 +- .../antennapod/asynctask/OpmlExportWorker.java | 118 ++ .../antennapod/asynctask/OpmlFeedQueuer.java | 69 + .../antennapod/asynctask/OpmlImportWorker.java | 116 ++ .../config/ApplicationCallbacksImpl.java | 23 + .../antennapod/config/ClientConfigurator.java | 19 + .../config/DownloadServiceCallbacksImpl.java | 49 + .../antennapod/config/FlattrCallbacksImpl.java | 53 + .../antennapod/config/GpodnetCallbacksImpl.java | 22 + .../config/PlaybackServiceCallbacksImpl.java | 21 + .../antennapod/config/StorageCallbacksImpl.java | 107 ++ .../de/danoeh/antennapod/core/ClientConfig.java | 21 - .../antennapod/core/DownloadServiceCallbacks.java | 43 - .../de/danoeh/antennapod/core/FlattrCallbacks.java | 24 - .../danoeh/antennapod/core/GpodnetCallbacks.java | 26 - .../antennapod/core/PlaybackServiceCallbacks.java | 20 - .../danoeh/antennapod/core/StorageCallbacks.java | 27 - .../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 - .../danoeh/antennapod/dialog/FeedItemDialog.java | 2 +- .../de/danoeh/antennapod/dialog/TimeDialog.java | 138 ++ .../antennapod/fragment/ItemlistFragment.java | 6 +- .../antennapod/fragment/NewEpisodesFragment.java | 4 +- .../fragment/PlaybackHistoryFragment.java | 4 +- .../danoeh/antennapod/fragment/QueueFragment.java | 4 +- .../danoeh/antennapod/fragment/SearchFragment.java | 4 +- .../fragment/gpodnet/PodcastListFragment.java | 4 +- .../fragment/gpodnet/SearchListFragment.java | 4 +- .../fragment/gpodnet/TagListFragment.java | 4 +- .../menuhandler/FeedItemMenuHandler.java | 191 +++ .../antennapod/menuhandler/FeedMenuHandler.java | 86 ++ .../antennapod/menuhandler/MenuItemUtils.java | 31 + .../antennapod/menuhandler/NavDrawerActivity.java | 9 + .../danoeh/antennapod/receiver/PlayerWidget.java | 49 + .../antennapod/service/PlayerWidgetService.java | 192 +++ .../main/res/drawable-hdpi-v11/ic_stat_antenna.png | Bin 678 -> 0 bytes .../drawable-hdpi-v11/ic_stat_authentication.png | Bin 467 -> 0 bytes .../res/drawable-hdpi-v11/stat_notify_sync.png | Bin 1012 -> 0 bytes .../drawable-hdpi-v11/stat_notify_sync_error.png | Bin 1103 -> 0 bytes app/src/main/res/drawable-hdpi/action_about.png | Bin 1764 -> 0 bytes .../main/res/drawable-hdpi/action_about_dark.png | Bin 1629 -> 0 bytes app/src/main/res/drawable-hdpi/action_search.png | Bin 1759 -> 0 bytes .../main/res/drawable-hdpi/action_search_dark.png | Bin 1764 -> 0 bytes app/src/main/res/drawable-hdpi/action_settings.png | Bin 1505 -> 0 bytes .../res/drawable-hdpi/action_settings_dark.png | Bin 1540 -> 0 bytes app/src/main/res/drawable-hdpi/action_stream.png | Bin 803 -> 0 bytes .../main/res/drawable-hdpi/action_stream_dark.png | Bin 693 -> 0 bytes app/src/main/res/drawable-hdpi/av_download.png | Bin 1328 -> 0 bytes .../main/res/drawable-hdpi/av_download_dark.png | Bin 1331 -> 0 bytes app/src/main/res/drawable-hdpi/av_fast_forward.png | Bin 1416 -> 0 bytes .../res/drawable-hdpi/av_fast_forward_dark.png | Bin 1366 -> 0 bytes app/src/main/res/drawable-hdpi/av_pause.png | Bin 1116 -> 0 bytes app/src/main/res/drawable-hdpi/av_pause_dark.png | Bin 1114 -> 0 bytes app/src/main/res/drawable-hdpi/av_play.png | Bin 1405 -> 0 bytes app/src/main/res/drawable-hdpi/av_play_dark.png | Bin 1410 -> 0 bytes app/src/main/res/drawable-hdpi/av_rewind.png | Bin 1426 -> 0 bytes app/src/main/res/drawable-hdpi/av_rewind_dark.png | Bin 1449 -> 0 bytes app/src/main/res/drawable-hdpi/content_discard.png | Bin 1624 -> 0 bytes .../res/drawable-hdpi/content_discard_dark.png | Bin 1611 -> 0 bytes app/src/main/res/drawable-hdpi/content_new.png | Bin 1157 -> 0 bytes .../main/res/drawable-hdpi/content_new_dark.png | Bin 1142 -> 0 bytes app/src/main/res/drawable-hdpi/default_cover.png | Bin 1404 -> 0 bytes .../main/res/drawable-hdpi/default_cover_dark.png | Bin 1426 -> 0 bytes .../main/res/drawable-hdpi/device_access_time.png | Bin 1875 -> 0 bytes .../res/drawable-hdpi/device_access_time_dark.png | Bin 1794 -> 0 bytes .../main/res/drawable-hdpi/ic_action_overflow.png | Bin 225 -> 0 bytes .../res/drawable-hdpi/ic_action_overflow_dark.png | Bin 217 -> 0 bytes .../drawable-hdpi/ic_action_pause_over_video.png | Bin 6552 -> 0 bytes .../drawable-hdpi/ic_action_play_over_video.png | Bin 7123 -> 0 bytes app/src/main/res/drawable-hdpi/ic_drag_handle.png | Bin 220 -> 0 bytes .../main/res/drawable-hdpi/ic_drag_handle_dark.png | Bin 204 -> 0 bytes app/src/main/res/drawable-hdpi/ic_drawer.png | Bin 2829 -> 0 bytes app/src/main/res/drawable-hdpi/ic_drawer_dark.png | Bin 2826 -> 0 bytes app/src/main/res/drawable-hdpi/ic_launcher.png | Bin 3955 -> 0 bytes app/src/main/res/drawable-hdpi/ic_new.png | Bin 891 -> 0 bytes app/src/main/res/drawable-hdpi/ic_new_dark.png | Bin 716 -> 0 bytes app/src/main/res/drawable-hdpi/ic_stat_antenna.png | Bin 649 -> 0 bytes .../res/drawable-hdpi/ic_stat_authentication.png | Bin 648 -> 0 bytes .../main/res/drawable-hdpi/location_web_site.png | Bin 2529 -> 0 bytes .../res/drawable-hdpi/location_web_site_dark.png | Bin 2516 -> 0 bytes .../main/res/drawable-hdpi/navigation_accept.png | Bin 1320 -> 0 bytes .../res/drawable-hdpi/navigation_accept_dark.png | Bin 1335 -> 0 bytes .../main/res/drawable-hdpi/navigation_cancel.png | Bin 1358 -> 0 bytes .../res/drawable-hdpi/navigation_cancel_dark.png | Bin 1285 -> 0 bytes .../main/res/drawable-hdpi/navigation_chapters.png | Bin 1979 -> 0 bytes .../res/drawable-hdpi/navigation_chapters_dark.png | Bin 1821 -> 0 bytes .../main/res/drawable-hdpi/navigation_collapse.png | Bin 1425 -> 0 bytes .../res/drawable-hdpi/navigation_collapse_dark.png | Bin 1384 -> 0 bytes .../main/res/drawable-hdpi/navigation_expand.png | Bin 1444 -> 0 bytes .../res/drawable-hdpi/navigation_expand_dark.png | Bin 1405 -> 0 bytes .../main/res/drawable-hdpi/navigation_refresh.png | Bin 3171 -> 0 bytes .../res/drawable-hdpi/navigation_refresh_dark.png | Bin 3138 -> 0 bytes .../res/drawable-hdpi/navigation_shownotes.png | Bin 1363 -> 0 bytes .../drawable-hdpi/navigation_shownotes_dark.png | Bin 1386 -> 0 bytes app/src/main/res/drawable-hdpi/navigation_up.png | Bin 2270 -> 0 bytes .../main/res/drawable-hdpi/navigation_up_dark.png | Bin 2221 -> 0 bytes app/src/main/res/drawable-hdpi/social_share.png | Bin 1695 -> 0 bytes .../main/res/drawable-hdpi/social_share_dark.png | Bin 1606 -> 0 bytes .../main/res/drawable-hdpi/spinner_button.9.png | Bin 318 -> 0 bytes .../res/drawable-hdpi/spinner_button_dark.9.png | Bin 316 -> 0 bytes .../main/res/drawable-hdpi/stat_notify_sync.png | Bin 674 -> 0 bytes .../res/drawable-hdpi/stat_notify_sync_error.png | Bin 708 -> 0 bytes app/src/main/res/drawable-hdpi/stat_playlist.png | Bin 412 -> 0 bytes .../main/res/drawable-hdpi/stat_playlist_dark.png | Bin 338 -> 0 bytes app/src/main/res/drawable-hdpi/type_audio.png | Bin 1983 -> 0 bytes app/src/main/res/drawable-hdpi/type_audio_dark.png | Bin 2008 -> 0 bytes app/src/main/res/drawable-hdpi/type_video.png | Bin 1215 -> 0 bytes app/src/main/res/drawable-hdpi/type_video_dark.png | Bin 1211 -> 0 bytes .../main/res/drawable-ldpi-v11/ic_stat_antenna.png | Bin 307 -> 0 bytes app/src/main/res/drawable-ldpi/action_stream.png | Bin 367 -> 0 bytes .../main/res/drawable-ldpi/action_stream_dark.png | Bin 307 -> 0 bytes app/src/main/res/drawable-ldpi/ic_launcher.png | Bin 1658 -> 0 bytes app/src/main/res/drawable-ldpi/ic_stat_antenna.png | Bin 271 -> 0 bytes app/src/main/res/drawable-ldpi/stat_playlist.png | Bin 239 -> 0 bytes .../main/res/drawable-ldpi/stat_playlist_dark.png | Bin 219 -> 0 bytes .../main/res/drawable-mdpi-v11/ic_stat_antenna.png | Bin 414 -> 0 bytes .../drawable-mdpi-v11/ic_stat_authentication.png | Bin 293 -> 0 bytes .../res/drawable-mdpi-v11/stat_notify_sync.png | Bin 732 -> 0 bytes .../drawable-mdpi-v11/stat_notify_sync_error.png | Bin 746 -> 0 bytes app/src/main/res/drawable-mdpi/action_about.png | Bin 1441 -> 0 bytes .../main/res/drawable-mdpi/action_about_dark.png | Bin 1333 -> 0 bytes app/src/main/res/drawable-mdpi/action_search.png | Bin 1429 -> 0 bytes .../main/res/drawable-mdpi/action_search_dark.png | Bin 1394 -> 0 bytes app/src/main/res/drawable-mdpi/action_settings.png | Bin 1358 -> 0 bytes .../res/drawable-mdpi/action_settings_dark.png | Bin 1339 -> 0 bytes app/src/main/res/drawable-mdpi/action_stream.png | Bin 506 -> 0 bytes .../main/res/drawable-mdpi/action_stream_dark.png | Bin 426 -> 0 bytes app/src/main/res/drawable-mdpi/av_download.png | Bin 1230 -> 0 bytes .../main/res/drawable-mdpi/av_download_dark.png | Bin 1238 -> 0 bytes app/src/main/res/drawable-mdpi/av_fast_forward.png | Bin 1277 -> 0 bytes .../res/drawable-mdpi/av_fast_forward_dark.png | Bin 1285 -> 0 bytes app/src/main/res/drawable-mdpi/av_pause.png | Bin 1109 -> 0 bytes app/src/main/res/drawable-mdpi/av_pause_dark.png | Bin 1107 -> 0 bytes app/src/main/res/drawable-mdpi/av_play.png | Bin 1261 -> 0 bytes app/src/main/res/drawable-mdpi/av_play_dark.png | Bin 1248 -> 0 bytes app/src/main/res/drawable-mdpi/av_rewind.png | Bin 1277 -> 0 bytes app/src/main/res/drawable-mdpi/av_rewind_dark.png | Bin 1277 -> 0 bytes app/src/main/res/drawable-mdpi/content_discard.png | Bin 1359 -> 0 bytes .../res/drawable-mdpi/content_discard_dark.png | Bin 1358 -> 0 bytes app/src/main/res/drawable-mdpi/content_new.png | Bin 1099 -> 0 bytes .../main/res/drawable-mdpi/content_new_dark.png | Bin 1090 -> 0 bytes app/src/main/res/drawable-mdpi/default_cover.png | Bin 1246 -> 0 bytes .../main/res/drawable-mdpi/default_cover_dark.png | Bin 1240 -> 0 bytes .../main/res/drawable-mdpi/device_access_time.png | Bin 1493 -> 0 bytes .../res/drawable-mdpi/device_access_time_dark.png | Bin 1408 -> 0 bytes .../main/res/drawable-mdpi/ic_action_overflow.png | Bin 197 -> 0 bytes .../res/drawable-mdpi/ic_action_overflow_dark.png | Bin 201 -> 0 bytes .../drawable-mdpi/ic_action_pause_over_video.png | Bin 3233 -> 0 bytes .../drawable-mdpi/ic_action_play_over_video.png | Bin 3510 -> 0 bytes app/src/main/res/drawable-mdpi/ic_drag_handle.png | Bin 175 -> 0 bytes .../main/res/drawable-mdpi/ic_drag_handle_dark.png | Bin 159 -> 0 bytes app/src/main/res/drawable-mdpi/ic_drawer.png | Bin 2820 -> 0 bytes app/src/main/res/drawable-mdpi/ic_drawer_dark.png | Bin 2816 -> 0 bytes app/src/main/res/drawable-mdpi/ic_launcher.png | Bin 2382 -> 0 bytes app/src/main/res/drawable-mdpi/ic_new.png | Bin 593 -> 0 bytes app/src/main/res/drawable-mdpi/ic_new_dark.png | Bin 484 -> 0 bytes app/src/main/res/drawable-mdpi/ic_stat_antenna.png | Bin 412 -> 0 bytes .../res/drawable-mdpi/ic_stat_authentication.png | Bin 460 -> 0 bytes .../main/res/drawable-mdpi/location_web_site.png | Bin 1827 -> 0 bytes .../res/drawable-mdpi/location_web_site_dark.png | Bin 1842 -> 0 bytes .../main/res/drawable-mdpi/navigation_accept.png | Bin 1197 -> 0 bytes .../res/drawable-mdpi/navigation_accept_dark.png | Bin 1191 -> 0 bytes .../main/res/drawable-mdpi/navigation_cancel.png | Bin 1202 -> 0 bytes .../res/drawable-mdpi/navigation_cancel_dark.png | Bin 1138 -> 0 bytes .../main/res/drawable-mdpi/navigation_chapters.png | Bin 1584 -> 0 bytes .../res/drawable-mdpi/navigation_chapters_dark.png | Bin 1453 -> 0 bytes .../main/res/drawable-mdpi/navigation_collapse.png | Bin 1238 -> 0 bytes .../res/drawable-mdpi/navigation_collapse_dark.png | Bin 1208 -> 0 bytes .../main/res/drawable-mdpi/navigation_expand.png | Bin 1242 -> 0 bytes .../res/drawable-mdpi/navigation_expand_dark.png | Bin 1214 -> 0 bytes .../main/res/drawable-mdpi/navigation_refresh.png | Bin 3058 -> 0 bytes .../res/drawable-mdpi/navigation_refresh_dark.png | Bin 3033 -> 0 bytes .../res/drawable-mdpi/navigation_shownotes.png | Bin 1254 -> 0 bytes .../drawable-mdpi/navigation_shownotes_dark.png | Bin 1253 -> 0 bytes app/src/main/res/drawable-mdpi/navigation_up.png | Bin 2123 -> 0 bytes .../main/res/drawable-mdpi/navigation_up_dark.png | Bin 2060 -> 0 bytes app/src/main/res/drawable-mdpi/social_share.png | Bin 1394 -> 0 bytes .../main/res/drawable-mdpi/social_share_dark.png | Bin 1341 -> 0 bytes .../main/res/drawable-mdpi/spinner_button.9.png | Bin 266 -> 0 bytes .../res/drawable-mdpi/spinner_button_dark.9.png | Bin 266 -> 0 bytes .../main/res/drawable-mdpi/stat_notify_sync.png | Bin 628 -> 0 bytes .../res/drawable-mdpi/stat_notify_sync_error.png | Bin 627 -> 0 bytes app/src/main/res/drawable-mdpi/stat_playlist.png | Bin 327 -> 0 bytes .../main/res/drawable-mdpi/stat_playlist_dark.png | Bin 271 -> 0 bytes app/src/main/res/drawable-mdpi/type_audio.png | Bin 1580 -> 0 bytes app/src/main/res/drawable-mdpi/type_audio_dark.png | Bin 1582 -> 0 bytes app/src/main/res/drawable-mdpi/type_video.png | Bin 1129 -> 0 bytes app/src/main/res/drawable-mdpi/type_video_dark.png | Bin 1129 -> 0 bytes .../res/drawable-xhdpi-v11/ic_stat_antenna.png | Bin 1005 -> 0 bytes .../drawable-xhdpi-v11/ic_stat_authentication.png | Bin 529 -> 0 bytes .../res/drawable-xhdpi-v11/stat_notify_sync.png | Bin 1306 -> 0 bytes .../drawable-xhdpi-v11/stat_notify_sync_error.png | Bin 1434 -> 0 bytes app/src/main/res/drawable-xhdpi/action_about.png | Bin 2257 -> 0 bytes .../main/res/drawable-xhdpi/action_about_dark.png | Bin 2040 -> 0 bytes app/src/main/res/drawable-xhdpi/action_search.png | Bin 2117 -> 0 bytes .../main/res/drawable-xhdpi/action_search_dark.png | Bin 2127 -> 0 bytes .../main/res/drawable-xhdpi/action_settings.png | Bin 1671 -> 0 bytes .../res/drawable-xhdpi/action_settings_dark.png | Bin 1641 -> 0 bytes app/src/main/res/drawable-xhdpi/action_stream.png | Bin 1099 -> 0 bytes .../main/res/drawable-xhdpi/action_stream_dark.png | Bin 974 -> 0 bytes app/src/main/res/drawable-xhdpi/av_download.png | Bin 1473 -> 0 bytes .../main/res/drawable-xhdpi/av_download_dark.png | Bin 1482 -> 0 bytes .../main/res/drawable-xhdpi/av_fast_forward.png | Bin 1668 -> 0 bytes .../res/drawable-xhdpi/av_fast_forward_dark.png | Bin 1664 -> 0 bytes app/src/main/res/drawable-xhdpi/av_pause.png | Bin 1159 -> 0 bytes app/src/main/res/drawable-xhdpi/av_pause_dark.png | Bin 1181 -> 0 bytes app/src/main/res/drawable-xhdpi/av_play.png | Bin 1578 -> 0 bytes app/src/main/res/drawable-xhdpi/av_play_dark.png | Bin 1620 -> 0 bytes app/src/main/res/drawable-xhdpi/av_rewind.png | Bin 1659 -> 0 bytes app/src/main/res/drawable-xhdpi/av_rewind_dark.png | Bin 1694 -> 0 bytes .../main/res/drawable-xhdpi/content_discard.png | Bin 1848 -> 0 bytes .../res/drawable-xhdpi/content_discard_dark.png | Bin 1824 -> 0 bytes app/src/main/res/drawable-xhdpi/content_new.png | Bin 1225 -> 0 bytes .../main/res/drawable-xhdpi/content_new_dark.png | Bin 1221 -> 0 bytes app/src/main/res/drawable-xhdpi/content_remove.png | Bin 1488 -> 0 bytes .../res/drawable-xhdpi/content_remove_dark.png | Bin 1348 -> 0 bytes app/src/main/res/drawable-xhdpi/default_cover.png | Bin 1522 -> 0 bytes .../main/res/drawable-xhdpi/default_cover_dark.png | Bin 1544 -> 0 bytes .../main/res/drawable-xhdpi/device_access_time.png | Bin 2423 -> 0 bytes .../res/drawable-xhdpi/device_access_time_dark.png | Bin 2284 -> 0 bytes .../main/res/drawable-xhdpi/ic_action_overflow.png | Bin 267 -> 0 bytes .../res/drawable-xhdpi/ic_action_overflow_dark.png | Bin 262 -> 0 bytes .../drawable-xhdpi/ic_action_pause_over_video.png | Bin 10241 -> 0 bytes .../drawable-xhdpi/ic_action_play_over_video.png | Bin 11175 -> 0 bytes app/src/main/res/drawable-xhdpi/ic_drag_handle.png | Bin 234 -> 0 bytes .../res/drawable-xhdpi/ic_drag_handle_dark.png | Bin 216 -> 0 bytes app/src/main/res/drawable-xhdpi/ic_drawer.png | Bin 2836 -> 0 bytes app/src/main/res/drawable-xhdpi/ic_drawer_dark.png | Bin 1038 -> 0 bytes app/src/main/res/drawable-xhdpi/ic_launcher.png | Bin 5589 -> 0 bytes app/src/main/res/drawable-xhdpi/ic_new.png | Bin 1189 -> 0 bytes app/src/main/res/drawable-xhdpi/ic_new_dark.png | Bin 989 -> 0 bytes .../main/res/drawable-xhdpi/ic_stat_antenna.png | Bin 942 -> 0 bytes .../res/drawable-xhdpi/ic_stat_authentication.png | Bin 882 -> 0 bytes .../main/res/drawable-xhdpi/ic_undobar_undo.png | Bin 1558 -> 0 bytes .../main/res/drawable-xhdpi/location_web_site.png | Bin 3291 -> 0 bytes .../res/drawable-xhdpi/location_web_site_dark.png | Bin 3307 -> 0 bytes .../main/res/drawable-xhdpi/navigation_accept.png | Bin 1546 -> 0 bytes .../res/drawable-xhdpi/navigation_accept_dark.png | Bin 1599 -> 0 bytes .../main/res/drawable-xhdpi/navigation_cancel.png | Bin 1488 -> 0 bytes .../res/drawable-xhdpi/navigation_cancel_dark.png | Bin 1348 -> 0 bytes .../res/drawable-xhdpi/navigation_chapters.png | Bin 2524 -> 0 bytes .../drawable-xhdpi/navigation_chapters_dark.png | Bin 2366 -> 0 bytes .../res/drawable-xhdpi/navigation_collapse.png | Bin 1658 -> 0 bytes .../drawable-xhdpi/navigation_collapse_dark.png | Bin 1635 -> 0 bytes .../main/res/drawable-xhdpi/navigation_expand.png | Bin 1702 -> 0 bytes .../res/drawable-xhdpi/navigation_expand_dark.png | Bin 1677 -> 0 bytes .../main/res/drawable-xhdpi/navigation_refresh.png | Bin 3272 -> 0 bytes .../res/drawable-xhdpi/navigation_refresh_dark.png | Bin 3219 -> 0 bytes .../res/drawable-xhdpi/navigation_shownotes.png | Bin 1414 -> 0 bytes .../drawable-xhdpi/navigation_shownotes_dark.png | Bin 1446 -> 0 bytes app/src/main/res/drawable-xhdpi/navigation_up.png | Bin 2471 -> 0 bytes .../main/res/drawable-xhdpi/navigation_up_dark.png | Bin 2445 -> 0 bytes app/src/main/res/drawable-xhdpi/social_share.png | Bin 1989 -> 0 bytes .../main/res/drawable-xhdpi/social_share_dark.png | Bin 1780 -> 0 bytes .../main/res/drawable-xhdpi/spinner_button.9.png | Bin 405 -> 0 bytes .../res/drawable-xhdpi/spinner_button_dark.9.png | Bin 406 -> 0 bytes app/src/main/res/drawable-xhdpi/stat_playlist.png | Bin 494 -> 0 bytes .../main/res/drawable-xhdpi/stat_playlist_dark.png | Bin 440 -> 0 bytes app/src/main/res/drawable-xhdpi/type_audio.png | Bin 2437 -> 0 bytes .../main/res/drawable-xhdpi/type_audio_dark.png | Bin 2489 -> 0 bytes app/src/main/res/drawable-xhdpi/type_video.png | Bin 1327 -> 0 bytes .../main/res/drawable-xhdpi/type_video_dark.png | Bin 1337 -> 0 bytes app/src/main/res/drawable-xhdpi/undobar.9.png | Bin 1665 -> 0 bytes .../drawable-xhdpi/undobar_button_focused.9.png | Bin 1141 -> 0 bytes .../drawable-xhdpi/undobar_button_pressed.9.png | Bin 1123 -> 0 bytes .../main/res/drawable-xhdpi/undobar_divider.9.png | Bin 963 -> 0 bytes .../res/drawable-xxhdpi/ic_action_overflow.png | Bin 264 -> 0 bytes .../drawable-xxhdpi/ic_action_overflow_dark.png | Bin 264 -> 0 bytes .../drawable-xxhdpi/ic_action_pause_over_video.png | Bin 21550 -> 0 bytes .../drawable-xxhdpi/ic_action_play_over_video.png | Bin 23322 -> 0 bytes .../main/res/drawable-xxhdpi/ic_drag_handle.png | Bin 290 -> 0 bytes .../res/drawable-xxhdpi/ic_drag_handle_dark.png | Bin 265 -> 0 bytes app/src/main/res/drawable-xxhdpi/ic_drawer.png | Bin 202 -> 0 bytes .../main/res/drawable-xxhdpi/ic_drawer_dark.png | Bin 202 -> 0 bytes app/src/main/res/drawable-xxhdpi/ic_launcher.png | Bin 14262 -> 0 bytes app/src/main/res/drawable-xxhdpi/ic_new.png | Bin 1759 -> 0 bytes app/src/main/res/drawable-xxhdpi/ic_new_dark.png | Bin 1501 -> 0 bytes .../res/drawable-xxhdpi/ic_stat_authentication.png | Bin 1266 -> 0 bytes app/src/main/res/drawable/badge.xml | 13 - app/src/main/res/drawable/borderless_button.xml | 13 - .../main/res/drawable/borderless_button_dark.xml | 13 - app/src/main/res/drawable/horizontal_divider.9.png | Bin 159 -> 0 bytes .../drawable/overlay_button_circle_background.xml | 10 - app/src/main/res/drawable/overlay_drawable.xml | 20 - .../main/res/drawable/overlay_drawable_dark.xml | 15 - app/src/main/res/drawable/type_audio.png | Bin 1580 -> 0 bytes app/src/main/res/drawable/type_video.png | Bin 1129 -> 0 bytes app/src/main/res/drawable/undobar_button.xml | 22 - app/src/main/res/drawable/vertical_divider.9.png | Bin 191 -> 0 bytes app/src/main/res/drawable/white_circle.xml | 11 - app/src/main/res/values-az/strings.xml | 217 --- app/src/main/res/values-ca/strings.xml | 341 ----- app/src/main/res/values-cs-rCZ/strings.xml | 272 ---- app/src/main/res/values-da/strings.xml | 329 ----- app/src/main/res/values-de/strings.xml | 341 ----- app/src/main/res/values-es-rES/strings.xml | 200 --- app/src/main/res/values-es/strings.xml | 313 ----- app/src/main/res/values-fr/strings.xml | 340 ----- app/src/main/res/values-hi-rIN/strings.xml | 281 ---- app/src/main/res/values-it-rIT/strings.xml | 289 ---- app/src/main/res/values-iw-rIL/strings.xml | 305 ----- app/src/main/res/values-ko/strings.xml | 305 ----- app/src/main/res/values-land/styles.xml | 6 - app/src/main/res/values-large/dimens.xml | 8 - app/src/main/res/values-nl/strings.xml | 305 ----- app/src/main/res/values-pl-rPL/strings.xml | 330 ----- app/src/main/res/values-pt-rBR/strings.xml | 280 ---- app/src/main/res/values-pt/strings.xml | 341 ----- app/src/main/res/values-ro-rRO/strings.xml | 245 ---- app/src/main/res/values-ru/strings.xml | 311 ----- app/src/main/res/values-sv-rSE/strings.xml | 341 ----- app/src/main/res/values-uk-rUA/strings.xml | 329 ----- app/src/main/res/values-v11/colors.xml | 5 - app/src/main/res/values-v14/dimens.xml | 5 - app/src/main/res/values-v14/styles.xml | 9 - app/src/main/res/values-v16/styles.xml | 17 - app/src/main/res/values-v19/colors.xml | 5 - app/src/main/res/values-zh-rCN/strings.xml | 317 ----- app/src/main/res/values/arrays.xml | 114 -- app/src/main/res/values/attrs.xml | 43 - app/src/main/res/values/colors.xml | 24 - app/src/main/res/values/dimens.xml | 23 - app/src/main/res/values/ids.xml | 27 - app/src/main/res/values/integers.xml | 4 - app/src/main/res/values/strings.xml | 374 ------ app/src/main/res/values/styles.xml | 174 --- core/.gitignore | 1 + core/build.gradle | 43 + core/proguard-rules.pro | 17 + .../de/danoeh/antennapod/core/ApplicationTest.java | 13 + core/src/main/AndroidManifest.xml | 11 + .../aocate/presto/service/IDeathCallback_0_8.aidl | 18 + .../IOnBufferingUpdateListenerCallback_0_8.aidl | 19 + .../service/IOnCompletionListenerCallback_0_8.aidl | 19 + .../service/IOnErrorListenerCallback_0_8.aidl | 19 + .../service/IOnInfoListenerCallback_0_8.aidl | 19 + ...stmentAvailableChangedListenerCallback_0_8.aidl | 19 + .../service/IOnPreparedListenerCallback_0_8.aidl | 19 + .../IOnSeekCompleteListenerCallback_0_8.aidl | 19 + ...stmentAvailableChangedListenerCallback_0_8.aidl | 19 + .../com/aocate/presto/service/IPlayMedia_0_8.aidl | 75 ++ .../java/com/aocate/media/AndroidMediaPlayer.java | 470 +++++++ .../main/java/com/aocate/media/MediaPlayer.java | 1278 ++++++++++++++++++ .../java/com/aocate/media/MediaPlayerImpl.java | 118 ++ .../com/aocate/media/ServiceBackedMediaPlayer.java | 1170 ++++++++++++++++ .../com/aocate/media/SpeedAdjustmentAlgorithm.java | 31 + .../antennapod/core/ApplicationCallbacks.java | 22 + .../de/danoeh/antennapod/core/ClientConfig.java | 25 + .../antennapod/core/DownloadServiceCallbacks.java | 44 + .../de/danoeh/antennapod/core/FlattrCallbacks.java | 36 + .../danoeh/antennapod/core/GpodnetCallbacks.java | 27 + .../antennapod/core/PlaybackServiceCallbacks.java | 21 + .../danoeh/antennapod/core/StorageCallbacks.java | 27 + .../core/asynctask/DownloadObserver.java | 177 +++ .../antennapod/core/asynctask/FeedRemover.java | 74 ++ .../core/asynctask/FlattrClickWorker.java | 237 ++++ .../core/asynctask/FlattrStatusFetcher.java | 47 + .../core/asynctask/FlattrTokenFetcher.java | 92 ++ .../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 + .../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 | 410 ++++++ .../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 | 247 ++++ .../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 + .../core/service/GpodnetSyncService.java | 251 ++++ .../core/service/download/APRedirectHandler.java | 54 + .../service/download/AntennapodHttpClient.java | 98 ++ .../core/service/download/DownloadRequest.java | 209 +++ .../core/service/download/DownloadService.java | 1200 +++++++++++++++++ .../core/service/download/DownloadStatus.java | 181 +++ .../core/service/download/Downloader.java | 73 + .../core/service/download/DownloaderCallback.java | 10 + .../core/service/download/HttpDownloader.java | 252 ++++ .../core/service/playback/PlaybackService.java | 1072 +++++++++++++++ .../playback/PlaybackServiceMediaPlayer.java | 979 ++++++++++++++ .../playback/PlaybackServiceTaskManager.java | 384 ++++++ .../core/service/playback/PlayerStatus.java | 14 + .../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 | 1310 ++++++++++++++++++ .../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 | 67 + .../de/danoeh/antennapod/core/util/ThemeUtils.java | 23 + .../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 | 304 +++++ .../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 + .../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 + .../main/res/drawable-hdpi-v11/ic_stat_antenna.png | Bin 0 -> 678 bytes .../drawable-hdpi-v11/ic_stat_authentication.png | Bin 0 -> 467 bytes .../res/drawable-hdpi-v11/stat_notify_sync.png | Bin 0 -> 1012 bytes .../drawable-hdpi-v11/stat_notify_sync_error.png | Bin 0 -> 1103 bytes core/src/main/res/drawable-hdpi/action_about.png | Bin 0 -> 1764 bytes .../main/res/drawable-hdpi/action_about_dark.png | Bin 0 -> 1629 bytes core/src/main/res/drawable-hdpi/action_search.png | Bin 0 -> 1759 bytes .../main/res/drawable-hdpi/action_search_dark.png | Bin 0 -> 1764 bytes .../src/main/res/drawable-hdpi/action_settings.png | Bin 0 -> 1505 bytes .../res/drawable-hdpi/action_settings_dark.png | Bin 0 -> 1540 bytes core/src/main/res/drawable-hdpi/action_stream.png | Bin 0 -> 803 bytes .../main/res/drawable-hdpi/action_stream_dark.png | Bin 0 -> 693 bytes core/src/main/res/drawable-hdpi/av_download.png | Bin 0 -> 1328 bytes .../main/res/drawable-hdpi/av_download_dark.png | Bin 0 -> 1331 bytes .../src/main/res/drawable-hdpi/av_fast_forward.png | Bin 0 -> 1416 bytes .../res/drawable-hdpi/av_fast_forward_dark.png | Bin 0 -> 1366 bytes core/src/main/res/drawable-hdpi/av_pause.png | Bin 0 -> 1116 bytes core/src/main/res/drawable-hdpi/av_pause_dark.png | Bin 0 -> 1114 bytes core/src/main/res/drawable-hdpi/av_play.png | Bin 0 -> 1405 bytes core/src/main/res/drawable-hdpi/av_play_dark.png | Bin 0 -> 1410 bytes core/src/main/res/drawable-hdpi/av_rewind.png | Bin 0 -> 1426 bytes core/src/main/res/drawable-hdpi/av_rewind_dark.png | Bin 0 -> 1449 bytes .../src/main/res/drawable-hdpi/content_discard.png | Bin 0 -> 1624 bytes .../res/drawable-hdpi/content_discard_dark.png | Bin 0 -> 1611 bytes core/src/main/res/drawable-hdpi/content_new.png | Bin 0 -> 1157 bytes .../main/res/drawable-hdpi/content_new_dark.png | Bin 0 -> 1142 bytes core/src/main/res/drawable-hdpi/default_cover.png | Bin 0 -> 1404 bytes .../main/res/drawable-hdpi/default_cover_dark.png | Bin 0 -> 1426 bytes .../main/res/drawable-hdpi/device_access_time.png | Bin 0 -> 1875 bytes .../res/drawable-hdpi/device_access_time_dark.png | Bin 0 -> 1794 bytes .../main/res/drawable-hdpi/ic_action_overflow.png | Bin 0 -> 225 bytes .../res/drawable-hdpi/ic_action_overflow_dark.png | Bin 0 -> 217 bytes .../drawable-hdpi/ic_action_pause_over_video.png | Bin 0 -> 6552 bytes .../drawable-hdpi/ic_action_play_over_video.png | Bin 0 -> 7123 bytes core/src/main/res/drawable-hdpi/ic_drag_handle.png | Bin 0 -> 220 bytes .../main/res/drawable-hdpi/ic_drag_handle_dark.png | Bin 0 -> 204 bytes core/src/main/res/drawable-hdpi/ic_drawer.png | Bin 0 -> 2829 bytes core/src/main/res/drawable-hdpi/ic_drawer_dark.png | Bin 0 -> 2826 bytes core/src/main/res/drawable-hdpi/ic_launcher.png | Bin 0 -> 3955 bytes core/src/main/res/drawable-hdpi/ic_new.png | Bin 0 -> 891 bytes core/src/main/res/drawable-hdpi/ic_new_dark.png | Bin 0 -> 716 bytes .../src/main/res/drawable-hdpi/ic_stat_antenna.png | Bin 0 -> 649 bytes .../res/drawable-hdpi/ic_stat_authentication.png | Bin 0 -> 648 bytes .../main/res/drawable-hdpi/location_web_site.png | Bin 0 -> 2529 bytes .../res/drawable-hdpi/location_web_site_dark.png | Bin 0 -> 2516 bytes .../main/res/drawable-hdpi/navigation_accept.png | Bin 0 -> 1320 bytes .../res/drawable-hdpi/navigation_accept_dark.png | Bin 0 -> 1335 bytes .../main/res/drawable-hdpi/navigation_cancel.png | Bin 0 -> 1358 bytes .../res/drawable-hdpi/navigation_cancel_dark.png | Bin 0 -> 1285 bytes .../main/res/drawable-hdpi/navigation_chapters.png | Bin 0 -> 1979 bytes .../res/drawable-hdpi/navigation_chapters_dark.png | Bin 0 -> 1821 bytes .../main/res/drawable-hdpi/navigation_collapse.png | Bin 0 -> 1425 bytes .../res/drawable-hdpi/navigation_collapse_dark.png | Bin 0 -> 1384 bytes .../main/res/drawable-hdpi/navigation_expand.png | Bin 0 -> 1444 bytes .../res/drawable-hdpi/navigation_expand_dark.png | Bin 0 -> 1405 bytes .../main/res/drawable-hdpi/navigation_refresh.png | Bin 0 -> 3171 bytes .../res/drawable-hdpi/navigation_refresh_dark.png | Bin 0 -> 3138 bytes .../res/drawable-hdpi/navigation_shownotes.png | Bin 0 -> 1363 bytes .../drawable-hdpi/navigation_shownotes_dark.png | Bin 0 -> 1386 bytes core/src/main/res/drawable-hdpi/navigation_up.png | Bin 0 -> 2270 bytes .../main/res/drawable-hdpi/navigation_up_dark.png | Bin 0 -> 2221 bytes core/src/main/res/drawable-hdpi/social_share.png | Bin 0 -> 1695 bytes .../main/res/drawable-hdpi/social_share_dark.png | Bin 0 -> 1606 bytes .../main/res/drawable-hdpi/spinner_button.9.png | Bin 0 -> 318 bytes .../res/drawable-hdpi/spinner_button_dark.9.png | Bin 0 -> 316 bytes .../main/res/drawable-hdpi/stat_notify_sync.png | Bin 0 -> 674 bytes .../res/drawable-hdpi/stat_notify_sync_error.png | Bin 0 -> 708 bytes core/src/main/res/drawable-hdpi/stat_playlist.png | Bin 0 -> 412 bytes .../main/res/drawable-hdpi/stat_playlist_dark.png | Bin 0 -> 338 bytes core/src/main/res/drawable-hdpi/type_audio.png | Bin 0 -> 1983 bytes .../src/main/res/drawable-hdpi/type_audio_dark.png | Bin 0 -> 2008 bytes core/src/main/res/drawable-hdpi/type_video.png | Bin 0 -> 1215 bytes .../src/main/res/drawable-hdpi/type_video_dark.png | Bin 0 -> 1211 bytes .../main/res/drawable-ldpi-v11/ic_stat_antenna.png | Bin 0 -> 307 bytes core/src/main/res/drawable-ldpi/action_stream.png | Bin 0 -> 367 bytes .../main/res/drawable-ldpi/action_stream_dark.png | Bin 0 -> 307 bytes core/src/main/res/drawable-ldpi/ic_launcher.png | Bin 0 -> 1658 bytes .../src/main/res/drawable-ldpi/ic_stat_antenna.png | Bin 0 -> 271 bytes core/src/main/res/drawable-ldpi/stat_playlist.png | Bin 0 -> 239 bytes .../main/res/drawable-ldpi/stat_playlist_dark.png | Bin 0 -> 219 bytes .../main/res/drawable-mdpi-v11/ic_stat_antenna.png | Bin 0 -> 414 bytes .../drawable-mdpi-v11/ic_stat_authentication.png | Bin 0 -> 293 bytes .../res/drawable-mdpi-v11/stat_notify_sync.png | Bin 0 -> 732 bytes .../drawable-mdpi-v11/stat_notify_sync_error.png | Bin 0 -> 746 bytes core/src/main/res/drawable-mdpi/action_about.png | Bin 0 -> 1441 bytes .../main/res/drawable-mdpi/action_about_dark.png | Bin 0 -> 1333 bytes core/src/main/res/drawable-mdpi/action_search.png | Bin 0 -> 1429 bytes .../main/res/drawable-mdpi/action_search_dark.png | Bin 0 -> 1394 bytes .../src/main/res/drawable-mdpi/action_settings.png | Bin 0 -> 1358 bytes .../res/drawable-mdpi/action_settings_dark.png | Bin 0 -> 1339 bytes core/src/main/res/drawable-mdpi/action_stream.png | Bin 0 -> 506 bytes .../main/res/drawable-mdpi/action_stream_dark.png | Bin 0 -> 426 bytes core/src/main/res/drawable-mdpi/av_download.png | Bin 0 -> 1230 bytes .../main/res/drawable-mdpi/av_download_dark.png | Bin 0 -> 1238 bytes .../src/main/res/drawable-mdpi/av_fast_forward.png | Bin 0 -> 1277 bytes .../res/drawable-mdpi/av_fast_forward_dark.png | Bin 0 -> 1285 bytes core/src/main/res/drawable-mdpi/av_pause.png | Bin 0 -> 1109 bytes core/src/main/res/drawable-mdpi/av_pause_dark.png | Bin 0 -> 1107 bytes core/src/main/res/drawable-mdpi/av_play.png | Bin 0 -> 1261 bytes core/src/main/res/drawable-mdpi/av_play_dark.png | Bin 0 -> 1248 bytes core/src/main/res/drawable-mdpi/av_rewind.png | Bin 0 -> 1277 bytes core/src/main/res/drawable-mdpi/av_rewind_dark.png | Bin 0 -> 1277 bytes .../src/main/res/drawable-mdpi/content_discard.png | Bin 0 -> 1359 bytes .../res/drawable-mdpi/content_discard_dark.png | Bin 0 -> 1358 bytes core/src/main/res/drawable-mdpi/content_new.png | Bin 0 -> 1099 bytes .../main/res/drawable-mdpi/content_new_dark.png | Bin 0 -> 1090 bytes core/src/main/res/drawable-mdpi/default_cover.png | Bin 0 -> 1246 bytes .../main/res/drawable-mdpi/default_cover_dark.png | Bin 0 -> 1240 bytes .../main/res/drawable-mdpi/device_access_time.png | Bin 0 -> 1493 bytes .../res/drawable-mdpi/device_access_time_dark.png | Bin 0 -> 1408 bytes .../main/res/drawable-mdpi/ic_action_overflow.png | Bin 0 -> 197 bytes .../res/drawable-mdpi/ic_action_overflow_dark.png | Bin 0 -> 201 bytes .../drawable-mdpi/ic_action_pause_over_video.png | Bin 0 -> 3233 bytes .../drawable-mdpi/ic_action_play_over_video.png | Bin 0 -> 3510 bytes core/src/main/res/drawable-mdpi/ic_drag_handle.png | Bin 0 -> 175 bytes .../main/res/drawable-mdpi/ic_drag_handle_dark.png | Bin 0 -> 159 bytes core/src/main/res/drawable-mdpi/ic_drawer.png | Bin 0 -> 2820 bytes core/src/main/res/drawable-mdpi/ic_drawer_dark.png | Bin 0 -> 2816 bytes core/src/main/res/drawable-mdpi/ic_launcher.png | Bin 0 -> 2382 bytes core/src/main/res/drawable-mdpi/ic_new.png | Bin 0 -> 593 bytes core/src/main/res/drawable-mdpi/ic_new_dark.png | Bin 0 -> 484 bytes .../src/main/res/drawable-mdpi/ic_stat_antenna.png | Bin 0 -> 412 bytes .../res/drawable-mdpi/ic_stat_authentication.png | Bin 0 -> 460 bytes .../main/res/drawable-mdpi/location_web_site.png | Bin 0 -> 1827 bytes .../res/drawable-mdpi/location_web_site_dark.png | Bin 0 -> 1842 bytes .../main/res/drawable-mdpi/navigation_accept.png | Bin 0 -> 1197 bytes .../res/drawable-mdpi/navigation_accept_dark.png | Bin 0 -> 1191 bytes .../main/res/drawable-mdpi/navigation_cancel.png | Bin 0 -> 1202 bytes .../res/drawable-mdpi/navigation_cancel_dark.png | Bin 0 -> 1138 bytes .../main/res/drawable-mdpi/navigation_chapters.png | Bin 0 -> 1584 bytes .../res/drawable-mdpi/navigation_chapters_dark.png | Bin 0 -> 1453 bytes .../main/res/drawable-mdpi/navigation_collapse.png | Bin 0 -> 1238 bytes .../res/drawable-mdpi/navigation_collapse_dark.png | Bin 0 -> 1208 bytes .../main/res/drawable-mdpi/navigation_expand.png | Bin 0 -> 1242 bytes .../res/drawable-mdpi/navigation_expand_dark.png | Bin 0 -> 1214 bytes .../main/res/drawable-mdpi/navigation_refresh.png | Bin 0 -> 3058 bytes .../res/drawable-mdpi/navigation_refresh_dark.png | Bin 0 -> 3033 bytes .../res/drawable-mdpi/navigation_shownotes.png | Bin 0 -> 1254 bytes .../drawable-mdpi/navigation_shownotes_dark.png | Bin 0 -> 1253 bytes core/src/main/res/drawable-mdpi/navigation_up.png | Bin 0 -> 2123 bytes .../main/res/drawable-mdpi/navigation_up_dark.png | Bin 0 -> 2060 bytes core/src/main/res/drawable-mdpi/social_share.png | Bin 0 -> 1394 bytes .../main/res/drawable-mdpi/social_share_dark.png | Bin 0 -> 1341 bytes .../main/res/drawable-mdpi/spinner_button.9.png | Bin 0 -> 266 bytes .../res/drawable-mdpi/spinner_button_dark.9.png | Bin 0 -> 266 bytes .../main/res/drawable-mdpi/stat_notify_sync.png | Bin 0 -> 628 bytes .../res/drawable-mdpi/stat_notify_sync_error.png | Bin 0 -> 627 bytes core/src/main/res/drawable-mdpi/stat_playlist.png | Bin 0 -> 327 bytes .../main/res/drawable-mdpi/stat_playlist_dark.png | Bin 0 -> 271 bytes core/src/main/res/drawable-mdpi/type_audio.png | Bin 0 -> 1580 bytes .../src/main/res/drawable-mdpi/type_audio_dark.png | Bin 0 -> 1582 bytes core/src/main/res/drawable-mdpi/type_video.png | Bin 0 -> 1129 bytes .../src/main/res/drawable-mdpi/type_video_dark.png | Bin 0 -> 1129 bytes .../res/drawable-xhdpi-v11/ic_stat_antenna.png | Bin 0 -> 1005 bytes .../drawable-xhdpi-v11/ic_stat_authentication.png | Bin 0 -> 529 bytes .../res/drawable-xhdpi-v11/stat_notify_sync.png | Bin 0 -> 1306 bytes .../drawable-xhdpi-v11/stat_notify_sync_error.png | Bin 0 -> 1434 bytes core/src/main/res/drawable-xhdpi/action_about.png | Bin 0 -> 2257 bytes .../main/res/drawable-xhdpi/action_about_dark.png | Bin 0 -> 2040 bytes core/src/main/res/drawable-xhdpi/action_search.png | Bin 0 -> 2117 bytes .../main/res/drawable-xhdpi/action_search_dark.png | Bin 0 -> 2127 bytes .../main/res/drawable-xhdpi/action_settings.png | Bin 0 -> 1671 bytes .../res/drawable-xhdpi/action_settings_dark.png | Bin 0 -> 1641 bytes core/src/main/res/drawable-xhdpi/action_stream.png | Bin 0 -> 1099 bytes .../main/res/drawable-xhdpi/action_stream_dark.png | Bin 0 -> 974 bytes core/src/main/res/drawable-xhdpi/av_download.png | Bin 0 -> 1473 bytes .../main/res/drawable-xhdpi/av_download_dark.png | Bin 0 -> 1482 bytes .../main/res/drawable-xhdpi/av_fast_forward.png | Bin 0 -> 1668 bytes .../res/drawable-xhdpi/av_fast_forward_dark.png | Bin 0 -> 1664 bytes core/src/main/res/drawable-xhdpi/av_pause.png | Bin 0 -> 1159 bytes core/src/main/res/drawable-xhdpi/av_pause_dark.png | Bin 0 -> 1181 bytes core/src/main/res/drawable-xhdpi/av_play.png | Bin 0 -> 1578 bytes core/src/main/res/drawable-xhdpi/av_play_dark.png | Bin 0 -> 1620 bytes core/src/main/res/drawable-xhdpi/av_rewind.png | Bin 0 -> 1659 bytes .../src/main/res/drawable-xhdpi/av_rewind_dark.png | Bin 0 -> 1694 bytes .../main/res/drawable-xhdpi/content_discard.png | Bin 0 -> 1848 bytes .../res/drawable-xhdpi/content_discard_dark.png | Bin 0 -> 1824 bytes core/src/main/res/drawable-xhdpi/content_new.png | Bin 0 -> 1225 bytes .../main/res/drawable-xhdpi/content_new_dark.png | Bin 0 -> 1221 bytes .../src/main/res/drawable-xhdpi/content_remove.png | Bin 0 -> 1488 bytes .../res/drawable-xhdpi/content_remove_dark.png | Bin 0 -> 1348 bytes core/src/main/res/drawable-xhdpi/default_cover.png | Bin 0 -> 1522 bytes .../main/res/drawable-xhdpi/default_cover_dark.png | Bin 0 -> 1544 bytes .../main/res/drawable-xhdpi/device_access_time.png | Bin 0 -> 2423 bytes .../res/drawable-xhdpi/device_access_time_dark.png | Bin 0 -> 2284 bytes .../main/res/drawable-xhdpi/ic_action_overflow.png | Bin 0 -> 267 bytes .../res/drawable-xhdpi/ic_action_overflow_dark.png | Bin 0 -> 262 bytes .../drawable-xhdpi/ic_action_pause_over_video.png | Bin 0 -> 10241 bytes .../drawable-xhdpi/ic_action_play_over_video.png | Bin 0 -> 11175 bytes .../src/main/res/drawable-xhdpi/ic_drag_handle.png | Bin 0 -> 234 bytes .../res/drawable-xhdpi/ic_drag_handle_dark.png | Bin 0 -> 216 bytes core/src/main/res/drawable-xhdpi/ic_drawer.png | Bin 0 -> 2836 bytes .../src/main/res/drawable-xhdpi/ic_drawer_dark.png | Bin 0 -> 1038 bytes core/src/main/res/drawable-xhdpi/ic_launcher.png | Bin 0 -> 5589 bytes core/src/main/res/drawable-xhdpi/ic_new.png | Bin 0 -> 1189 bytes core/src/main/res/drawable-xhdpi/ic_new_dark.png | Bin 0 -> 989 bytes .../main/res/drawable-xhdpi/ic_stat_antenna.png | Bin 0 -> 942 bytes .../res/drawable-xhdpi/ic_stat_authentication.png | Bin 0 -> 882 bytes .../main/res/drawable-xhdpi/ic_undobar_undo.png | Bin 0 -> 1558 bytes .../main/res/drawable-xhdpi/location_web_site.png | Bin 0 -> 3291 bytes .../res/drawable-xhdpi/location_web_site_dark.png | Bin 0 -> 3307 bytes .../main/res/drawable-xhdpi/navigation_accept.png | Bin 0 -> 1546 bytes .../res/drawable-xhdpi/navigation_accept_dark.png | Bin 0 -> 1599 bytes .../main/res/drawable-xhdpi/navigation_cancel.png | Bin 0 -> 1488 bytes .../res/drawable-xhdpi/navigation_cancel_dark.png | Bin 0 -> 1348 bytes .../res/drawable-xhdpi/navigation_chapters.png | Bin 0 -> 2524 bytes .../drawable-xhdpi/navigation_chapters_dark.png | Bin 0 -> 2366 bytes .../res/drawable-xhdpi/navigation_collapse.png | Bin 0 -> 1658 bytes .../drawable-xhdpi/navigation_collapse_dark.png | Bin 0 -> 1635 bytes .../main/res/drawable-xhdpi/navigation_expand.png | Bin 0 -> 1702 bytes .../res/drawable-xhdpi/navigation_expand_dark.png | Bin 0 -> 1677 bytes .../main/res/drawable-xhdpi/navigation_refresh.png | Bin 0 -> 3272 bytes .../res/drawable-xhdpi/navigation_refresh_dark.png | Bin 0 -> 3219 bytes .../res/drawable-xhdpi/navigation_shownotes.png | Bin 0 -> 1414 bytes .../drawable-xhdpi/navigation_shownotes_dark.png | Bin 0 -> 1446 bytes core/src/main/res/drawable-xhdpi/navigation_up.png | Bin 0 -> 2471 bytes .../main/res/drawable-xhdpi/navigation_up_dark.png | Bin 0 -> 2445 bytes core/src/main/res/drawable-xhdpi/social_share.png | Bin 0 -> 1989 bytes .../main/res/drawable-xhdpi/social_share_dark.png | Bin 0 -> 1780 bytes .../main/res/drawable-xhdpi/spinner_button.9.png | Bin 0 -> 405 bytes .../res/drawable-xhdpi/spinner_button_dark.9.png | Bin 0 -> 406 bytes core/src/main/res/drawable-xhdpi/stat_playlist.png | Bin 0 -> 494 bytes .../main/res/drawable-xhdpi/stat_playlist_dark.png | Bin 0 -> 440 bytes core/src/main/res/drawable-xhdpi/type_audio.png | Bin 0 -> 2437 bytes .../main/res/drawable-xhdpi/type_audio_dark.png | Bin 0 -> 2489 bytes core/src/main/res/drawable-xhdpi/type_video.png | Bin 0 -> 1327 bytes .../main/res/drawable-xhdpi/type_video_dark.png | Bin 0 -> 1337 bytes core/src/main/res/drawable-xhdpi/undobar.9.png | Bin 0 -> 1665 bytes .../drawable-xhdpi/undobar_button_focused.9.png | Bin 0 -> 1141 bytes .../drawable-xhdpi/undobar_button_pressed.9.png | Bin 0 -> 1123 bytes .../main/res/drawable-xhdpi/undobar_divider.9.png | Bin 0 -> 963 bytes .../res/drawable-xxhdpi/ic_action_overflow.png | Bin 0 -> 264 bytes .../drawable-xxhdpi/ic_action_overflow_dark.png | Bin 0 -> 264 bytes .../drawable-xxhdpi/ic_action_pause_over_video.png | Bin 0 -> 21550 bytes .../drawable-xxhdpi/ic_action_play_over_video.png | Bin 0 -> 23322 bytes .../main/res/drawable-xxhdpi/ic_drag_handle.png | Bin 0 -> 290 bytes .../res/drawable-xxhdpi/ic_drag_handle_dark.png | Bin 0 -> 265 bytes core/src/main/res/drawable-xxhdpi/ic_drawer.png | Bin 0 -> 202 bytes .../main/res/drawable-xxhdpi/ic_drawer_dark.png | Bin 0 -> 202 bytes core/src/main/res/drawable-xxhdpi/ic_launcher.png | Bin 0 -> 14262 bytes core/src/main/res/drawable-xxhdpi/ic_new.png | Bin 0 -> 1759 bytes core/src/main/res/drawable-xxhdpi/ic_new_dark.png | Bin 0 -> 1501 bytes .../res/drawable-xxhdpi/ic_stat_authentication.png | Bin 0 -> 1266 bytes core/src/main/res/drawable/badge.xml | 13 + core/src/main/res/drawable/borderless_button.xml | 13 + .../main/res/drawable/borderless_button_dark.xml | 13 + .../src/main/res/drawable/horizontal_divider.9.png | Bin 0 -> 159 bytes .../drawable/overlay_button_circle_background.xml | 10 + core/src/main/res/drawable/overlay_drawable.xml | 20 + .../main/res/drawable/overlay_drawable_dark.xml | 15 + core/src/main/res/drawable/type_audio.png | Bin 0 -> 1580 bytes core/src/main/res/drawable/type_video.png | Bin 0 -> 1129 bytes core/src/main/res/drawable/undobar_button.xml | 22 + core/src/main/res/drawable/vertical_divider.9.png | Bin 0 -> 191 bytes core/src/main/res/drawable/white_circle.xml | 11 + core/src/main/res/values-az/strings.xml | 217 +++ core/src/main/res/values-ca/strings.xml | 341 +++++ core/src/main/res/values-cs-rCZ/strings.xml | 272 ++++ core/src/main/res/values-da/strings.xml | 329 +++++ core/src/main/res/values-de/strings.xml | 341 +++++ core/src/main/res/values-es-rES/strings.xml | 200 +++ core/src/main/res/values-es/strings.xml | 313 +++++ core/src/main/res/values-fr/strings.xml | 340 +++++ core/src/main/res/values-hi-rIN/strings.xml | 281 ++++ core/src/main/res/values-it-rIT/strings.xml | 289 ++++ core/src/main/res/values-iw-rIL/strings.xml | 305 +++++ core/src/main/res/values-ko/strings.xml | 305 +++++ core/src/main/res/values-land/styles.xml | 6 + core/src/main/res/values-large/dimens.xml | 8 + core/src/main/res/values-nl/strings.xml | 305 +++++ core/src/main/res/values-pl-rPL/strings.xml | 330 +++++ core/src/main/res/values-pt-rBR/strings.xml | 280 ++++ core/src/main/res/values-pt/strings.xml | 341 +++++ core/src/main/res/values-ro-rRO/strings.xml | 245 ++++ core/src/main/res/values-ru/strings.xml | 311 +++++ core/src/main/res/values-sv-rSE/strings.xml | 341 +++++ core/src/main/res/values-uk-rUA/strings.xml | 329 +++++ core/src/main/res/values-v11/colors.xml | 5 + core/src/main/res/values-v14/dimens.xml | 5 + core/src/main/res/values-v14/styles.xml | 9 + core/src/main/res/values-v16/styles.xml | 17 + core/src/main/res/values-v19/colors.xml | 5 + core/src/main/res/values-zh-rCN/strings.xml | 317 +++++ core/src/main/res/values/arrays.xml | 114 ++ core/src/main/res/values/attrs.xml | 43 + core/src/main/res/values/colors.xml | 24 + core/src/main/res/values/dimens.xml | 23 + core/src/main/res/values/ids.xml | 27 + core/src/main/res/values/integers.xml | 4 + core/src/main/res/values/strings.xml | 374 ++++++ core/src/main/res/values/styles.xml | 174 +++ settings.gradle | 2 +- 941 files changed, 32705 insertions(+), 32483 deletions(-) delete mode 100644 app/core/build.gradle delete mode 100644 app/src/main/aidl/com/aocate/presto/service/IDeathCallback_0_8.aidl delete mode 100644 app/src/main/aidl/com/aocate/presto/service/IOnBufferingUpdateListenerCallback_0_8.aidl delete mode 100644 app/src/main/aidl/com/aocate/presto/service/IOnCompletionListenerCallback_0_8.aidl delete mode 100644 app/src/main/aidl/com/aocate/presto/service/IOnErrorListenerCallback_0_8.aidl delete mode 100644 app/src/main/aidl/com/aocate/presto/service/IOnInfoListenerCallback_0_8.aidl delete mode 100644 app/src/main/aidl/com/aocate/presto/service/IOnPitchAdjustmentAvailableChangedListenerCallback_0_8.aidl delete mode 100644 app/src/main/aidl/com/aocate/presto/service/IOnPreparedListenerCallback_0_8.aidl delete mode 100644 app/src/main/aidl/com/aocate/presto/service/IOnSeekCompleteListenerCallback_0_8.aidl delete mode 100644 app/src/main/aidl/com/aocate/presto/service/IOnSpeedAdjustmentAvailableChangedListenerCallback_0_8.aidl delete mode 100644 app/src/main/aidl/com/aocate/presto/service/IPlayMedia_0_8.aidl delete mode 100644 app/src/main/java/com/aocate/media/AndroidMediaPlayer.java delete mode 100644 app/src/main/java/com/aocate/media/MediaPlayer.java delete mode 100644 app/src/main/java/com/aocate/media/MediaPlayerImpl.java delete mode 100644 app/src/main/java/com/aocate/media/ServiceBackedMediaPlayer.java delete mode 100644 app/src/main/java/com/aocate/media/SpeedAdjustmentAlgorithm.java create mode 100644 app/src/main/java/de/danoeh/antennapod/asynctask/OpmlExportWorker.java create mode 100644 app/src/main/java/de/danoeh/antennapod/asynctask/OpmlFeedQueuer.java create mode 100644 app/src/main/java/de/danoeh/antennapod/asynctask/OpmlImportWorker.java create mode 100644 app/src/main/java/de/danoeh/antennapod/config/ApplicationCallbacksImpl.java create mode 100644 app/src/main/java/de/danoeh/antennapod/config/ClientConfigurator.java create mode 100644 app/src/main/java/de/danoeh/antennapod/config/DownloadServiceCallbacksImpl.java create mode 100644 app/src/main/java/de/danoeh/antennapod/config/FlattrCallbacksImpl.java create mode 100644 app/src/main/java/de/danoeh/antennapod/config/GpodnetCallbacksImpl.java create mode 100644 app/src/main/java/de/danoeh/antennapod/config/PlaybackServiceCallbacksImpl.java create mode 100644 app/src/main/java/de/danoeh/antennapod/config/StorageCallbacksImpl.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/ClientConfig.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/DownloadServiceCallbacks.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/FlattrCallbacks.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/GpodnetCallbacks.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/PlaybackServiceCallbacks.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/StorageCallbacks.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/asynctask/DownloadObserver.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/asynctask/FeedRemover.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/asynctask/FlattrClickWorker.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/asynctask/FlattrStatusFetcher.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/asynctask/FlattrTokenFetcher.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/asynctask/OpmlExportWorker.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/asynctask/OpmlFeedQueuer.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/asynctask/OpmlImportWorker.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/asynctask/PicassoImageResource.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/asynctask/PicassoProvider.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/backup/OpmlBackupAgent.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/dialog/ConfirmationDialog.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/dialog/DownloadRequestErrorDialogCreator.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/dialog/TimeDialog.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/feed/Chapter.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/feed/EventDistributor.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/feed/Feed.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/feed/FeedComponent.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/feed/FeedFile.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/feed/FeedImage.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/feed/FeedItem.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/feed/FeedMedia.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/feed/FeedPreferences.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/feed/ID3Chapter.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/feed/MediaType.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/feed/SearchResult.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/feed/SimpleChapter.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/feed/VorbisCommentChapter.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetService.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetServiceAuthenticationException.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetServiceBadStatusCodeException.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetServiceException.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetDevice.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetPodcast.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetSubscriptionChange.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetTag.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetUploadChangesResponse.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/opml/OpmlElement.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/opml/OpmlReader.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/opml/OpmlSymbols.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/opml/OpmlWriter.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/preferences/GpodnetPreferences.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/preferences/PlaybackPreferences.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/preferences/UserPreferences.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/receiver/AlarmUpdateReceiver.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/receiver/ConnectivityActionReceiver.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/receiver/FeedUpdateReceiver.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/receiver/MediaButtonReceiver.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/receiver/PlayerWidget.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/service/GpodnetSyncService.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/service/download/APRedirectHandler.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/service/download/AntennapodHttpClient.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/service/download/DownloadRequest.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/service/download/DownloadService.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/service/download/DownloadStatus.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/service/download/Downloader.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/service/download/DownloaderCallback.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/service/download/HttpDownloader.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceMediaPlayer.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceTaskManager.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/service/playback/PlayerStatus.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/service/playback/PlayerWidgetService.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/storage/DownloadRequestException.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/storage/DownloadRequester.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/storage/FeedItemStatistics.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/storage/FeedSearcher.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/storage/PodDBAdapter.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/syndication/handler/FeedHandler.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/syndication/handler/FeedHandlerResult.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/syndication/handler/HandlerState.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/syndication/handler/SyndHandler.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/syndication/handler/TypeGetter.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/syndication/handler/UnsupportedFeedtypeException.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSContent.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSITunes.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSMedia.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSRSS20.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSSimpleChapters.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/syndication/namespace/Namespace.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/syndication/namespace/SyndElement.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/syndication/namespace/atom/AtomText.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/syndication/namespace/atom/NSAtom.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/syndication/util/SyndDateUtils.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/syndication/util/SyndTypeUtils.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/ChapterUtils.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/Converter.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/DownloadError.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/DuckType.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/EpisodeFilter.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/FeedtitleComparator.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/FileNameGenerator.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/InvalidFeedException.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/LangUtils.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/NetworkUtils.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/QueueAccess.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/ShareUtils.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/ShownotesProvider.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/StorageUtils.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/ThemeUtils.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/URIUtil.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/URLChecker.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/UndoBarController.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/comparator/ChapterStartTimeComparator.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/comparator/DownloadStatusComparator.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/comparator/FeedItemPubdateComparator.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/comparator/PlaybackCompletionDateComparator.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/comparator/SearchResultValueComparator.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/exception/MediaFileNotFoundException.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/flattr/FlattrServiceCreator.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/flattr/FlattrStatus.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/flattr/FlattrThing.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/flattr/FlattrUtils.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/flattr/SimpleFlattrThing.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/gui/FeedItemUndoToken.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/id3reader/ChapterReader.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/id3reader/ID3Reader.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/id3reader/ID3ReaderException.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/id3reader/model/FrameHeader.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/id3reader/model/Header.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/id3reader/model/TagHeader.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/menuhandler/FeedItemMenuHandler.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/menuhandler/FeedMenuHandler.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/menuhandler/MenuItemUtils.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/menuhandler/NavDrawerActivity.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/playback/AudioPlayer.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/playback/ExternalMedia.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/playback/IPlayer.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/playback/MediaPlayerError.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/playback/Playable.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/playback/Timeline.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/playback/VideoPlayer.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/syndication/FeedDiscoverer.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/OggInputStream.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/VorbisCommentChapterReader.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/VorbisCommentHeader.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/VorbisCommentReader.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/VorbisCommentReaderException.java create mode 100644 app/src/main/java/de/danoeh/antennapod/dialog/TimeDialog.java create mode 100644 app/src/main/java/de/danoeh/antennapod/menuhandler/FeedItemMenuHandler.java create mode 100644 app/src/main/java/de/danoeh/antennapod/menuhandler/FeedMenuHandler.java create mode 100644 app/src/main/java/de/danoeh/antennapod/menuhandler/MenuItemUtils.java create mode 100644 app/src/main/java/de/danoeh/antennapod/menuhandler/NavDrawerActivity.java create mode 100644 app/src/main/java/de/danoeh/antennapod/receiver/PlayerWidget.java create mode 100644 app/src/main/java/de/danoeh/antennapod/service/PlayerWidgetService.java delete mode 100644 app/src/main/res/drawable-hdpi-v11/ic_stat_antenna.png delete mode 100755 app/src/main/res/drawable-hdpi-v11/ic_stat_authentication.png delete mode 100644 app/src/main/res/drawable-hdpi-v11/stat_notify_sync.png delete mode 100644 app/src/main/res/drawable-hdpi-v11/stat_notify_sync_error.png delete mode 100644 app/src/main/res/drawable-hdpi/action_about.png delete mode 100755 app/src/main/res/drawable-hdpi/action_about_dark.png delete mode 100644 app/src/main/res/drawable-hdpi/action_search.png delete mode 100755 app/src/main/res/drawable-hdpi/action_search_dark.png delete mode 100644 app/src/main/res/drawable-hdpi/action_settings.png delete mode 100755 app/src/main/res/drawable-hdpi/action_settings_dark.png delete mode 100644 app/src/main/res/drawable-hdpi/action_stream.png delete mode 100644 app/src/main/res/drawable-hdpi/action_stream_dark.png delete mode 100644 app/src/main/res/drawable-hdpi/av_download.png delete mode 100755 app/src/main/res/drawable-hdpi/av_download_dark.png delete mode 100644 app/src/main/res/drawable-hdpi/av_fast_forward.png delete mode 100755 app/src/main/res/drawable-hdpi/av_fast_forward_dark.png delete mode 100644 app/src/main/res/drawable-hdpi/av_pause.png delete mode 100755 app/src/main/res/drawable-hdpi/av_pause_dark.png delete mode 100644 app/src/main/res/drawable-hdpi/av_play.png delete mode 100755 app/src/main/res/drawable-hdpi/av_play_dark.png delete mode 100644 app/src/main/res/drawable-hdpi/av_rewind.png delete mode 100755 app/src/main/res/drawable-hdpi/av_rewind_dark.png delete mode 100644 app/src/main/res/drawable-hdpi/content_discard.png delete mode 100755 app/src/main/res/drawable-hdpi/content_discard_dark.png delete mode 100644 app/src/main/res/drawable-hdpi/content_new.png delete mode 100755 app/src/main/res/drawable-hdpi/content_new_dark.png delete mode 100644 app/src/main/res/drawable-hdpi/default_cover.png delete mode 100755 app/src/main/res/drawable-hdpi/default_cover_dark.png delete mode 100644 app/src/main/res/drawable-hdpi/device_access_time.png delete mode 100755 app/src/main/res/drawable-hdpi/device_access_time_dark.png delete mode 100644 app/src/main/res/drawable-hdpi/ic_action_overflow.png delete mode 100644 app/src/main/res/drawable-hdpi/ic_action_overflow_dark.png delete mode 100755 app/src/main/res/drawable-hdpi/ic_action_pause_over_video.png delete mode 100755 app/src/main/res/drawable-hdpi/ic_action_play_over_video.png delete mode 100755 app/src/main/res/drawable-hdpi/ic_drag_handle.png delete mode 100755 app/src/main/res/drawable-hdpi/ic_drag_handle_dark.png delete mode 100644 app/src/main/res/drawable-hdpi/ic_drawer.png delete mode 100644 app/src/main/res/drawable-hdpi/ic_drawer_dark.png delete mode 100644 app/src/main/res/drawable-hdpi/ic_launcher.png delete mode 100755 app/src/main/res/drawable-hdpi/ic_new.png delete mode 100755 app/src/main/res/drawable-hdpi/ic_new_dark.png delete mode 100644 app/src/main/res/drawable-hdpi/ic_stat_antenna.png delete mode 100755 app/src/main/res/drawable-hdpi/ic_stat_authentication.png delete mode 100644 app/src/main/res/drawable-hdpi/location_web_site.png delete mode 100755 app/src/main/res/drawable-hdpi/location_web_site_dark.png delete mode 100644 app/src/main/res/drawable-hdpi/navigation_accept.png delete mode 100755 app/src/main/res/drawable-hdpi/navigation_accept_dark.png delete mode 100644 app/src/main/res/drawable-hdpi/navigation_cancel.png delete mode 100755 app/src/main/res/drawable-hdpi/navigation_cancel_dark.png delete mode 100755 app/src/main/res/drawable-hdpi/navigation_chapters.png delete mode 100755 app/src/main/res/drawable-hdpi/navigation_chapters_dark.png delete mode 100755 app/src/main/res/drawable-hdpi/navigation_collapse.png delete mode 100755 app/src/main/res/drawable-hdpi/navigation_collapse_dark.png delete mode 100644 app/src/main/res/drawable-hdpi/navigation_expand.png delete mode 100755 app/src/main/res/drawable-hdpi/navigation_expand_dark.png delete mode 100644 app/src/main/res/drawable-hdpi/navigation_refresh.png delete mode 100755 app/src/main/res/drawable-hdpi/navigation_refresh_dark.png delete mode 100755 app/src/main/res/drawable-hdpi/navigation_shownotes.png delete mode 100755 app/src/main/res/drawable-hdpi/navigation_shownotes_dark.png delete mode 100755 app/src/main/res/drawable-hdpi/navigation_up.png delete mode 100755 app/src/main/res/drawable-hdpi/navigation_up_dark.png delete mode 100644 app/src/main/res/drawable-hdpi/social_share.png delete mode 100755 app/src/main/res/drawable-hdpi/social_share_dark.png delete mode 100644 app/src/main/res/drawable-hdpi/spinner_button.9.png delete mode 100644 app/src/main/res/drawable-hdpi/spinner_button_dark.9.png delete mode 100644 app/src/main/res/drawable-hdpi/stat_notify_sync.png delete mode 100644 app/src/main/res/drawable-hdpi/stat_notify_sync_error.png delete mode 100644 app/src/main/res/drawable-hdpi/stat_playlist.png delete mode 100644 app/src/main/res/drawable-hdpi/stat_playlist_dark.png delete mode 100644 app/src/main/res/drawable-hdpi/type_audio.png delete mode 100755 app/src/main/res/drawable-hdpi/type_audio_dark.png delete mode 100644 app/src/main/res/drawable-hdpi/type_video.png delete mode 100755 app/src/main/res/drawable-hdpi/type_video_dark.png delete mode 100644 app/src/main/res/drawable-ldpi-v11/ic_stat_antenna.png delete mode 100644 app/src/main/res/drawable-ldpi/action_stream.png delete mode 100644 app/src/main/res/drawable-ldpi/action_stream_dark.png delete mode 100644 app/src/main/res/drawable-ldpi/ic_launcher.png delete mode 100644 app/src/main/res/drawable-ldpi/ic_stat_antenna.png delete mode 100644 app/src/main/res/drawable-ldpi/stat_playlist.png delete mode 100644 app/src/main/res/drawable-ldpi/stat_playlist_dark.png delete mode 100644 app/src/main/res/drawable-mdpi-v11/ic_stat_antenna.png delete mode 100755 app/src/main/res/drawable-mdpi-v11/ic_stat_authentication.png delete mode 100644 app/src/main/res/drawable-mdpi-v11/stat_notify_sync.png delete mode 100644 app/src/main/res/drawable-mdpi-v11/stat_notify_sync_error.png delete mode 100644 app/src/main/res/drawable-mdpi/action_about.png delete mode 100755 app/src/main/res/drawable-mdpi/action_about_dark.png delete mode 100644 app/src/main/res/drawable-mdpi/action_search.png delete mode 100755 app/src/main/res/drawable-mdpi/action_search_dark.png delete mode 100644 app/src/main/res/drawable-mdpi/action_settings.png delete mode 100755 app/src/main/res/drawable-mdpi/action_settings_dark.png delete mode 100644 app/src/main/res/drawable-mdpi/action_stream.png delete mode 100644 app/src/main/res/drawable-mdpi/action_stream_dark.png delete mode 100644 app/src/main/res/drawable-mdpi/av_download.png delete mode 100755 app/src/main/res/drawable-mdpi/av_download_dark.png delete mode 100644 app/src/main/res/drawable-mdpi/av_fast_forward.png delete mode 100755 app/src/main/res/drawable-mdpi/av_fast_forward_dark.png delete mode 100644 app/src/main/res/drawable-mdpi/av_pause.png delete mode 100755 app/src/main/res/drawable-mdpi/av_pause_dark.png delete mode 100644 app/src/main/res/drawable-mdpi/av_play.png delete mode 100755 app/src/main/res/drawable-mdpi/av_play_dark.png delete mode 100644 app/src/main/res/drawable-mdpi/av_rewind.png delete mode 100755 app/src/main/res/drawable-mdpi/av_rewind_dark.png delete mode 100644 app/src/main/res/drawable-mdpi/content_discard.png delete mode 100755 app/src/main/res/drawable-mdpi/content_discard_dark.png delete mode 100644 app/src/main/res/drawable-mdpi/content_new.png delete mode 100755 app/src/main/res/drawable-mdpi/content_new_dark.png delete mode 100644 app/src/main/res/drawable-mdpi/default_cover.png delete mode 100755 app/src/main/res/drawable-mdpi/default_cover_dark.png delete mode 100644 app/src/main/res/drawable-mdpi/device_access_time.png delete mode 100755 app/src/main/res/drawable-mdpi/device_access_time_dark.png delete mode 100644 app/src/main/res/drawable-mdpi/ic_action_overflow.png delete mode 100644 app/src/main/res/drawable-mdpi/ic_action_overflow_dark.png delete mode 100755 app/src/main/res/drawable-mdpi/ic_action_pause_over_video.png delete mode 100755 app/src/main/res/drawable-mdpi/ic_action_play_over_video.png delete mode 100755 app/src/main/res/drawable-mdpi/ic_drag_handle.png delete mode 100755 app/src/main/res/drawable-mdpi/ic_drag_handle_dark.png delete mode 100644 app/src/main/res/drawable-mdpi/ic_drawer.png delete mode 100644 app/src/main/res/drawable-mdpi/ic_drawer_dark.png delete mode 100644 app/src/main/res/drawable-mdpi/ic_launcher.png delete mode 100755 app/src/main/res/drawable-mdpi/ic_new.png delete mode 100755 app/src/main/res/drawable-mdpi/ic_new_dark.png delete mode 100644 app/src/main/res/drawable-mdpi/ic_stat_antenna.png delete mode 100755 app/src/main/res/drawable-mdpi/ic_stat_authentication.png delete mode 100644 app/src/main/res/drawable-mdpi/location_web_site.png delete mode 100755 app/src/main/res/drawable-mdpi/location_web_site_dark.png delete mode 100644 app/src/main/res/drawable-mdpi/navigation_accept.png delete mode 100755 app/src/main/res/drawable-mdpi/navigation_accept_dark.png delete mode 100644 app/src/main/res/drawable-mdpi/navigation_cancel.png delete mode 100755 app/src/main/res/drawable-mdpi/navigation_cancel_dark.png delete mode 100755 app/src/main/res/drawable-mdpi/navigation_chapters.png delete mode 100755 app/src/main/res/drawable-mdpi/navigation_chapters_dark.png delete mode 100755 app/src/main/res/drawable-mdpi/navigation_collapse.png delete mode 100755 app/src/main/res/drawable-mdpi/navigation_collapse_dark.png delete mode 100644 app/src/main/res/drawable-mdpi/navigation_expand.png delete mode 100755 app/src/main/res/drawable-mdpi/navigation_expand_dark.png delete mode 100644 app/src/main/res/drawable-mdpi/navigation_refresh.png delete mode 100755 app/src/main/res/drawable-mdpi/navigation_refresh_dark.png delete mode 100755 app/src/main/res/drawable-mdpi/navigation_shownotes.png delete mode 100755 app/src/main/res/drawable-mdpi/navigation_shownotes_dark.png delete mode 100755 app/src/main/res/drawable-mdpi/navigation_up.png delete mode 100755 app/src/main/res/drawable-mdpi/navigation_up_dark.png delete mode 100644 app/src/main/res/drawable-mdpi/social_share.png delete mode 100755 app/src/main/res/drawable-mdpi/social_share_dark.png delete mode 100644 app/src/main/res/drawable-mdpi/spinner_button.9.png delete mode 100644 app/src/main/res/drawable-mdpi/spinner_button_dark.9.png delete mode 100644 app/src/main/res/drawable-mdpi/stat_notify_sync.png delete mode 100644 app/src/main/res/drawable-mdpi/stat_notify_sync_error.png delete mode 100644 app/src/main/res/drawable-mdpi/stat_playlist.png delete mode 100644 app/src/main/res/drawable-mdpi/stat_playlist_dark.png delete mode 100644 app/src/main/res/drawable-mdpi/type_audio.png delete mode 100755 app/src/main/res/drawable-mdpi/type_audio_dark.png delete mode 100644 app/src/main/res/drawable-mdpi/type_video.png delete mode 100755 app/src/main/res/drawable-mdpi/type_video_dark.png delete mode 100644 app/src/main/res/drawable-xhdpi-v11/ic_stat_antenna.png delete mode 100755 app/src/main/res/drawable-xhdpi-v11/ic_stat_authentication.png delete mode 100644 app/src/main/res/drawable-xhdpi-v11/stat_notify_sync.png delete mode 100644 app/src/main/res/drawable-xhdpi-v11/stat_notify_sync_error.png delete mode 100644 app/src/main/res/drawable-xhdpi/action_about.png delete mode 100755 app/src/main/res/drawable-xhdpi/action_about_dark.png delete mode 100644 app/src/main/res/drawable-xhdpi/action_search.png delete mode 100755 app/src/main/res/drawable-xhdpi/action_search_dark.png delete mode 100644 app/src/main/res/drawable-xhdpi/action_settings.png delete mode 100755 app/src/main/res/drawable-xhdpi/action_settings_dark.png delete mode 100644 app/src/main/res/drawable-xhdpi/action_stream.png delete mode 100644 app/src/main/res/drawable-xhdpi/action_stream_dark.png delete mode 100644 app/src/main/res/drawable-xhdpi/av_download.png delete mode 100755 app/src/main/res/drawable-xhdpi/av_download_dark.png delete mode 100644 app/src/main/res/drawable-xhdpi/av_fast_forward.png delete mode 100755 app/src/main/res/drawable-xhdpi/av_fast_forward_dark.png delete mode 100644 app/src/main/res/drawable-xhdpi/av_pause.png delete mode 100755 app/src/main/res/drawable-xhdpi/av_pause_dark.png delete mode 100644 app/src/main/res/drawable-xhdpi/av_play.png delete mode 100755 app/src/main/res/drawable-xhdpi/av_play_dark.png delete mode 100644 app/src/main/res/drawable-xhdpi/av_rewind.png delete mode 100755 app/src/main/res/drawable-xhdpi/av_rewind_dark.png delete mode 100644 app/src/main/res/drawable-xhdpi/content_discard.png delete mode 100755 app/src/main/res/drawable-xhdpi/content_discard_dark.png delete mode 100644 app/src/main/res/drawable-xhdpi/content_new.png delete mode 100755 app/src/main/res/drawable-xhdpi/content_new_dark.png delete mode 100644 app/src/main/res/drawable-xhdpi/content_remove.png delete mode 100755 app/src/main/res/drawable-xhdpi/content_remove_dark.png delete mode 100644 app/src/main/res/drawable-xhdpi/default_cover.png delete mode 100755 app/src/main/res/drawable-xhdpi/default_cover_dark.png delete mode 100644 app/src/main/res/drawable-xhdpi/device_access_time.png delete mode 100755 app/src/main/res/drawable-xhdpi/device_access_time_dark.png delete mode 100644 app/src/main/res/drawable-xhdpi/ic_action_overflow.png delete mode 100644 app/src/main/res/drawable-xhdpi/ic_action_overflow_dark.png delete mode 100755 app/src/main/res/drawable-xhdpi/ic_action_pause_over_video.png delete mode 100755 app/src/main/res/drawable-xhdpi/ic_action_play_over_video.png delete mode 100755 app/src/main/res/drawable-xhdpi/ic_drag_handle.png delete mode 100755 app/src/main/res/drawable-xhdpi/ic_drag_handle_dark.png delete mode 100644 app/src/main/res/drawable-xhdpi/ic_drawer.png delete mode 100644 app/src/main/res/drawable-xhdpi/ic_drawer_dark.png delete mode 100644 app/src/main/res/drawable-xhdpi/ic_launcher.png delete mode 100755 app/src/main/res/drawable-xhdpi/ic_new.png delete mode 100755 app/src/main/res/drawable-xhdpi/ic_new_dark.png delete mode 100644 app/src/main/res/drawable-xhdpi/ic_stat_antenna.png delete mode 100755 app/src/main/res/drawable-xhdpi/ic_stat_authentication.png delete mode 100644 app/src/main/res/drawable-xhdpi/ic_undobar_undo.png delete mode 100644 app/src/main/res/drawable-xhdpi/location_web_site.png delete mode 100755 app/src/main/res/drawable-xhdpi/location_web_site_dark.png delete mode 100644 app/src/main/res/drawable-xhdpi/navigation_accept.png delete mode 100755 app/src/main/res/drawable-xhdpi/navigation_accept_dark.png delete mode 100644 app/src/main/res/drawable-xhdpi/navigation_cancel.png delete mode 100755 app/src/main/res/drawable-xhdpi/navigation_cancel_dark.png delete mode 100755 app/src/main/res/drawable-xhdpi/navigation_chapters.png delete mode 100755 app/src/main/res/drawable-xhdpi/navigation_chapters_dark.png delete mode 100755 app/src/main/res/drawable-xhdpi/navigation_collapse.png delete mode 100755 app/src/main/res/drawable-xhdpi/navigation_collapse_dark.png delete mode 100644 app/src/main/res/drawable-xhdpi/navigation_expand.png delete mode 100755 app/src/main/res/drawable-xhdpi/navigation_expand_dark.png delete mode 100644 app/src/main/res/drawable-xhdpi/navigation_refresh.png delete mode 100755 app/src/main/res/drawable-xhdpi/navigation_refresh_dark.png delete mode 100755 app/src/main/res/drawable-xhdpi/navigation_shownotes.png delete mode 100755 app/src/main/res/drawable-xhdpi/navigation_shownotes_dark.png delete mode 100755 app/src/main/res/drawable-xhdpi/navigation_up.png delete mode 100755 app/src/main/res/drawable-xhdpi/navigation_up_dark.png delete mode 100644 app/src/main/res/drawable-xhdpi/social_share.png delete mode 100755 app/src/main/res/drawable-xhdpi/social_share_dark.png delete mode 100644 app/src/main/res/drawable-xhdpi/spinner_button.9.png delete mode 100644 app/src/main/res/drawable-xhdpi/spinner_button_dark.9.png delete mode 100644 app/src/main/res/drawable-xhdpi/stat_playlist.png delete mode 100644 app/src/main/res/drawable-xhdpi/stat_playlist_dark.png delete mode 100644 app/src/main/res/drawable-xhdpi/type_audio.png delete mode 100755 app/src/main/res/drawable-xhdpi/type_audio_dark.png delete mode 100644 app/src/main/res/drawable-xhdpi/type_video.png delete mode 100755 app/src/main/res/drawable-xhdpi/type_video_dark.png delete mode 100644 app/src/main/res/drawable-xhdpi/undobar.9.png delete mode 100644 app/src/main/res/drawable-xhdpi/undobar_button_focused.9.png delete mode 100644 app/src/main/res/drawable-xhdpi/undobar_button_pressed.9.png delete mode 100644 app/src/main/res/drawable-xhdpi/undobar_divider.9.png delete mode 100644 app/src/main/res/drawable-xxhdpi/ic_action_overflow.png delete mode 100644 app/src/main/res/drawable-xxhdpi/ic_action_overflow_dark.png delete mode 100755 app/src/main/res/drawable-xxhdpi/ic_action_pause_over_video.png delete mode 100755 app/src/main/res/drawable-xxhdpi/ic_action_play_over_video.png delete mode 100755 app/src/main/res/drawable-xxhdpi/ic_drag_handle.png delete mode 100755 app/src/main/res/drawable-xxhdpi/ic_drag_handle_dark.png delete mode 100644 app/src/main/res/drawable-xxhdpi/ic_drawer.png delete mode 100644 app/src/main/res/drawable-xxhdpi/ic_drawer_dark.png delete mode 100644 app/src/main/res/drawable-xxhdpi/ic_launcher.png delete mode 100755 app/src/main/res/drawable-xxhdpi/ic_new.png delete mode 100755 app/src/main/res/drawable-xxhdpi/ic_new_dark.png delete mode 100755 app/src/main/res/drawable-xxhdpi/ic_stat_authentication.png delete mode 100644 app/src/main/res/drawable/badge.xml delete mode 100644 app/src/main/res/drawable/borderless_button.xml delete mode 100644 app/src/main/res/drawable/borderless_button_dark.xml delete mode 100644 app/src/main/res/drawable/horizontal_divider.9.png delete mode 100644 app/src/main/res/drawable/overlay_button_circle_background.xml delete mode 100644 app/src/main/res/drawable/overlay_drawable.xml delete mode 100644 app/src/main/res/drawable/overlay_drawable_dark.xml delete mode 100644 app/src/main/res/drawable/type_audio.png delete mode 100644 app/src/main/res/drawable/type_video.png delete mode 100644 app/src/main/res/drawable/undobar_button.xml delete mode 100644 app/src/main/res/drawable/vertical_divider.9.png delete mode 100644 app/src/main/res/drawable/white_circle.xml delete mode 100644 app/src/main/res/values-az/strings.xml delete mode 100644 app/src/main/res/values-ca/strings.xml delete mode 100644 app/src/main/res/values-cs-rCZ/strings.xml delete mode 100644 app/src/main/res/values-da/strings.xml delete mode 100644 app/src/main/res/values-de/strings.xml delete mode 100644 app/src/main/res/values-es-rES/strings.xml delete mode 100644 app/src/main/res/values-es/strings.xml delete mode 100644 app/src/main/res/values-fr/strings.xml delete mode 100644 app/src/main/res/values-hi-rIN/strings.xml delete mode 100644 app/src/main/res/values-it-rIT/strings.xml delete mode 100644 app/src/main/res/values-iw-rIL/strings.xml delete mode 100644 app/src/main/res/values-ko/strings.xml delete mode 100644 app/src/main/res/values-land/styles.xml delete mode 100644 app/src/main/res/values-large/dimens.xml delete mode 100644 app/src/main/res/values-nl/strings.xml delete mode 100644 app/src/main/res/values-pl-rPL/strings.xml delete mode 100644 app/src/main/res/values-pt-rBR/strings.xml delete mode 100644 app/src/main/res/values-pt/strings.xml delete mode 100644 app/src/main/res/values-ro-rRO/strings.xml delete mode 100644 app/src/main/res/values-ru/strings.xml delete mode 100644 app/src/main/res/values-sv-rSE/strings.xml delete mode 100644 app/src/main/res/values-uk-rUA/strings.xml delete mode 100644 app/src/main/res/values-v11/colors.xml delete mode 100644 app/src/main/res/values-v14/dimens.xml delete mode 100644 app/src/main/res/values-v14/styles.xml delete mode 100644 app/src/main/res/values-v16/styles.xml delete mode 100644 app/src/main/res/values-v19/colors.xml delete mode 100644 app/src/main/res/values-zh-rCN/strings.xml delete mode 100644 app/src/main/res/values/arrays.xml delete mode 100644 app/src/main/res/values/attrs.xml delete mode 100644 app/src/main/res/values/colors.xml delete mode 100644 app/src/main/res/values/dimens.xml delete mode 100644 app/src/main/res/values/ids.xml delete mode 100644 app/src/main/res/values/integers.xml delete mode 100644 app/src/main/res/values/strings.xml delete mode 100644 app/src/main/res/values/styles.xml create mode 100644 core/.gitignore create mode 100644 core/build.gradle create mode 100644 core/proguard-rules.pro create mode 100644 core/src/androidTest/java/de/danoeh/antennapod/core/ApplicationTest.java create mode 100644 core/src/main/AndroidManifest.xml create mode 100644 core/src/main/aidl/com/aocate/presto/service/IDeathCallback_0_8.aidl create mode 100644 core/src/main/aidl/com/aocate/presto/service/IOnBufferingUpdateListenerCallback_0_8.aidl create mode 100644 core/src/main/aidl/com/aocate/presto/service/IOnCompletionListenerCallback_0_8.aidl create mode 100644 core/src/main/aidl/com/aocate/presto/service/IOnErrorListenerCallback_0_8.aidl create mode 100644 core/src/main/aidl/com/aocate/presto/service/IOnInfoListenerCallback_0_8.aidl create mode 100644 core/src/main/aidl/com/aocate/presto/service/IOnPitchAdjustmentAvailableChangedListenerCallback_0_8.aidl create mode 100644 core/src/main/aidl/com/aocate/presto/service/IOnPreparedListenerCallback_0_8.aidl create mode 100644 core/src/main/aidl/com/aocate/presto/service/IOnSeekCompleteListenerCallback_0_8.aidl create mode 100644 core/src/main/aidl/com/aocate/presto/service/IOnSpeedAdjustmentAvailableChangedListenerCallback_0_8.aidl create mode 100644 core/src/main/aidl/com/aocate/presto/service/IPlayMedia_0_8.aidl create mode 100644 core/src/main/java/com/aocate/media/AndroidMediaPlayer.java create mode 100644 core/src/main/java/com/aocate/media/MediaPlayer.java create mode 100644 core/src/main/java/com/aocate/media/MediaPlayerImpl.java create mode 100644 core/src/main/java/com/aocate/media/ServiceBackedMediaPlayer.java create mode 100644 core/src/main/java/com/aocate/media/SpeedAdjustmentAlgorithm.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/ApplicationCallbacks.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/ClientConfig.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/DownloadServiceCallbacks.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/FlattrCallbacks.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/GpodnetCallbacks.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/PlaybackServiceCallbacks.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/StorageCallbacks.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/asynctask/DownloadObserver.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/asynctask/FeedRemover.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/asynctask/FlattrClickWorker.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/asynctask/FlattrStatusFetcher.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/asynctask/FlattrTokenFetcher.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/asynctask/PicassoImageResource.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/asynctask/PicassoProvider.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/backup/OpmlBackupAgent.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/dialog/ConfirmationDialog.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/dialog/DownloadRequestErrorDialogCreator.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/feed/Chapter.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/feed/EventDistributor.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/feed/Feed.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/feed/FeedComponent.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/feed/FeedFile.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/feed/FeedImage.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/feed/FeedItem.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/feed/FeedMedia.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/feed/FeedPreferences.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/feed/ID3Chapter.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/feed/MediaType.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/feed/SearchResult.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/feed/SimpleChapter.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/feed/VorbisCommentChapter.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetService.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetServiceAuthenticationException.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetServiceBadStatusCodeException.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetServiceException.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetDevice.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetPodcast.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetSubscriptionChange.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetTag.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetUploadChangesResponse.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/opml/OpmlElement.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/opml/OpmlReader.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/opml/OpmlSymbols.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/opml/OpmlWriter.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/preferences/GpodnetPreferences.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/preferences/PlaybackPreferences.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/preferences/UserPreferences.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/receiver/AlarmUpdateReceiver.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/receiver/ConnectivityActionReceiver.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/receiver/FeedUpdateReceiver.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/receiver/MediaButtonReceiver.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/service/GpodnetSyncService.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/service/download/APRedirectHandler.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/service/download/AntennapodHttpClient.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadRequest.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadService.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadStatus.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/service/download/Downloader.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/service/download/DownloaderCallback.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/service/download/HttpDownloader.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceMediaPlayer.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceTaskManager.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/service/playback/PlayerStatus.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/storage/DownloadRequestException.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/storage/DownloadRequester.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/storage/FeedItemStatistics.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/storage/FeedSearcher.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/storage/PodDBAdapter.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/syndication/handler/FeedHandler.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/syndication/handler/FeedHandlerResult.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/syndication/handler/HandlerState.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/syndication/handler/SyndHandler.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/syndication/handler/TypeGetter.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/syndication/handler/UnsupportedFeedtypeException.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSContent.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSITunes.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSMedia.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSRSS20.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSSimpleChapters.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/Namespace.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/SyndElement.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/atom/AtomText.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/atom/NSAtom.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/syndication/util/SyndDateUtils.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/syndication/util/SyndTypeUtils.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/util/ChapterUtils.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/util/Converter.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/util/DownloadError.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/util/DuckType.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/util/EpisodeFilter.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/util/FeedtitleComparator.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/util/FileNameGenerator.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/util/InvalidFeedException.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/util/LangUtils.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/util/NetworkUtils.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/util/QueueAccess.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/util/ShareUtils.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/util/ShownotesProvider.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/util/StorageUtils.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/util/ThemeUtils.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/util/URIUtil.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/util/URLChecker.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/util/UndoBarController.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/util/comparator/ChapterStartTimeComparator.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/util/comparator/DownloadStatusComparator.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/util/comparator/FeedItemPubdateComparator.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/util/comparator/PlaybackCompletionDateComparator.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/util/comparator/SearchResultValueComparator.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/util/exception/MediaFileNotFoundException.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/util/flattr/FlattrServiceCreator.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/util/flattr/FlattrStatus.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/util/flattr/FlattrThing.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/util/flattr/FlattrUtils.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/util/flattr/SimpleFlattrThing.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/util/gui/FeedItemUndoToken.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/util/id3reader/ChapterReader.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/util/id3reader/ID3Reader.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/util/id3reader/ID3ReaderException.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/util/id3reader/model/FrameHeader.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/util/id3reader/model/Header.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/util/id3reader/model/TagHeader.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/util/playback/AudioPlayer.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/util/playback/ExternalMedia.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/util/playback/IPlayer.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/util/playback/MediaPlayerError.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/util/playback/Playable.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/util/playback/Timeline.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/util/playback/VideoPlayer.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/util/syndication/FeedDiscoverer.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/OggInputStream.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/VorbisCommentChapterReader.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/VorbisCommentHeader.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/VorbisCommentReader.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/VorbisCommentReaderException.java create mode 100644 core/src/main/res/drawable-hdpi-v11/ic_stat_antenna.png create mode 100755 core/src/main/res/drawable-hdpi-v11/ic_stat_authentication.png create mode 100644 core/src/main/res/drawable-hdpi-v11/stat_notify_sync.png create mode 100644 core/src/main/res/drawable-hdpi-v11/stat_notify_sync_error.png create mode 100644 core/src/main/res/drawable-hdpi/action_about.png create mode 100755 core/src/main/res/drawable-hdpi/action_about_dark.png create mode 100644 core/src/main/res/drawable-hdpi/action_search.png create mode 100755 core/src/main/res/drawable-hdpi/action_search_dark.png create mode 100644 core/src/main/res/drawable-hdpi/action_settings.png create mode 100755 core/src/main/res/drawable-hdpi/action_settings_dark.png create mode 100644 core/src/main/res/drawable-hdpi/action_stream.png create mode 100644 core/src/main/res/drawable-hdpi/action_stream_dark.png create mode 100644 core/src/main/res/drawable-hdpi/av_download.png create mode 100755 core/src/main/res/drawable-hdpi/av_download_dark.png create mode 100644 core/src/main/res/drawable-hdpi/av_fast_forward.png create mode 100755 core/src/main/res/drawable-hdpi/av_fast_forward_dark.png create mode 100644 core/src/main/res/drawable-hdpi/av_pause.png create mode 100755 core/src/main/res/drawable-hdpi/av_pause_dark.png create mode 100644 core/src/main/res/drawable-hdpi/av_play.png create mode 100755 core/src/main/res/drawable-hdpi/av_play_dark.png create mode 100644 core/src/main/res/drawable-hdpi/av_rewind.png create mode 100755 core/src/main/res/drawable-hdpi/av_rewind_dark.png create mode 100644 core/src/main/res/drawable-hdpi/content_discard.png create mode 100755 core/src/main/res/drawable-hdpi/content_discard_dark.png create mode 100644 core/src/main/res/drawable-hdpi/content_new.png create mode 100755 core/src/main/res/drawable-hdpi/content_new_dark.png create mode 100644 core/src/main/res/drawable-hdpi/default_cover.png create mode 100755 core/src/main/res/drawable-hdpi/default_cover_dark.png create mode 100644 core/src/main/res/drawable-hdpi/device_access_time.png create mode 100755 core/src/main/res/drawable-hdpi/device_access_time_dark.png create mode 100644 core/src/main/res/drawable-hdpi/ic_action_overflow.png create mode 100644 core/src/main/res/drawable-hdpi/ic_action_overflow_dark.png create mode 100755 core/src/main/res/drawable-hdpi/ic_action_pause_over_video.png create mode 100755 core/src/main/res/drawable-hdpi/ic_action_play_over_video.png create mode 100755 core/src/main/res/drawable-hdpi/ic_drag_handle.png create mode 100755 core/src/main/res/drawable-hdpi/ic_drag_handle_dark.png create mode 100644 core/src/main/res/drawable-hdpi/ic_drawer.png create mode 100644 core/src/main/res/drawable-hdpi/ic_drawer_dark.png create mode 100644 core/src/main/res/drawable-hdpi/ic_launcher.png create mode 100755 core/src/main/res/drawable-hdpi/ic_new.png create mode 100755 core/src/main/res/drawable-hdpi/ic_new_dark.png create mode 100644 core/src/main/res/drawable-hdpi/ic_stat_antenna.png create mode 100755 core/src/main/res/drawable-hdpi/ic_stat_authentication.png create mode 100644 core/src/main/res/drawable-hdpi/location_web_site.png create mode 100755 core/src/main/res/drawable-hdpi/location_web_site_dark.png create mode 100644 core/src/main/res/drawable-hdpi/navigation_accept.png create mode 100755 core/src/main/res/drawable-hdpi/navigation_accept_dark.png create mode 100644 core/src/main/res/drawable-hdpi/navigation_cancel.png create mode 100755 core/src/main/res/drawable-hdpi/navigation_cancel_dark.png create mode 100755 core/src/main/res/drawable-hdpi/navigation_chapters.png create mode 100755 core/src/main/res/drawable-hdpi/navigation_chapters_dark.png create mode 100755 core/src/main/res/drawable-hdpi/navigation_collapse.png create mode 100755 core/src/main/res/drawable-hdpi/navigation_collapse_dark.png create mode 100644 core/src/main/res/drawable-hdpi/navigation_expand.png create mode 100755 core/src/main/res/drawable-hdpi/navigation_expand_dark.png create mode 100644 core/src/main/res/drawable-hdpi/navigation_refresh.png create mode 100755 core/src/main/res/drawable-hdpi/navigation_refresh_dark.png create mode 100755 core/src/main/res/drawable-hdpi/navigation_shownotes.png create mode 100755 core/src/main/res/drawable-hdpi/navigation_shownotes_dark.png create mode 100755 core/src/main/res/drawable-hdpi/navigation_up.png create mode 100755 core/src/main/res/drawable-hdpi/navigation_up_dark.png create mode 100644 core/src/main/res/drawable-hdpi/social_share.png create mode 100755 core/src/main/res/drawable-hdpi/social_share_dark.png create mode 100644 core/src/main/res/drawable-hdpi/spinner_button.9.png create mode 100644 core/src/main/res/drawable-hdpi/spinner_button_dark.9.png create mode 100644 core/src/main/res/drawable-hdpi/stat_notify_sync.png create mode 100644 core/src/main/res/drawable-hdpi/stat_notify_sync_error.png create mode 100644 core/src/main/res/drawable-hdpi/stat_playlist.png create mode 100644 core/src/main/res/drawable-hdpi/stat_playlist_dark.png create mode 100644 core/src/main/res/drawable-hdpi/type_audio.png create mode 100755 core/src/main/res/drawable-hdpi/type_audio_dark.png create mode 100644 core/src/main/res/drawable-hdpi/type_video.png create mode 100755 core/src/main/res/drawable-hdpi/type_video_dark.png create mode 100644 core/src/main/res/drawable-ldpi-v11/ic_stat_antenna.png create mode 100644 core/src/main/res/drawable-ldpi/action_stream.png create mode 100644 core/src/main/res/drawable-ldpi/action_stream_dark.png create mode 100644 core/src/main/res/drawable-ldpi/ic_launcher.png create mode 100644 core/src/main/res/drawable-ldpi/ic_stat_antenna.png create mode 100644 core/src/main/res/drawable-ldpi/stat_playlist.png create mode 100644 core/src/main/res/drawable-ldpi/stat_playlist_dark.png create mode 100644 core/src/main/res/drawable-mdpi-v11/ic_stat_antenna.png create mode 100755 core/src/main/res/drawable-mdpi-v11/ic_stat_authentication.png create mode 100644 core/src/main/res/drawable-mdpi-v11/stat_notify_sync.png create mode 100644 core/src/main/res/drawable-mdpi-v11/stat_notify_sync_error.png create mode 100644 core/src/main/res/drawable-mdpi/action_about.png create mode 100755 core/src/main/res/drawable-mdpi/action_about_dark.png create mode 100644 core/src/main/res/drawable-mdpi/action_search.png create mode 100755 core/src/main/res/drawable-mdpi/action_search_dark.png create mode 100644 core/src/main/res/drawable-mdpi/action_settings.png create mode 100755 core/src/main/res/drawable-mdpi/action_settings_dark.png create mode 100644 core/src/main/res/drawable-mdpi/action_stream.png create mode 100644 core/src/main/res/drawable-mdpi/action_stream_dark.png create mode 100644 core/src/main/res/drawable-mdpi/av_download.png create mode 100755 core/src/main/res/drawable-mdpi/av_download_dark.png create mode 100644 core/src/main/res/drawable-mdpi/av_fast_forward.png create mode 100755 core/src/main/res/drawable-mdpi/av_fast_forward_dark.png create mode 100644 core/src/main/res/drawable-mdpi/av_pause.png create mode 100755 core/src/main/res/drawable-mdpi/av_pause_dark.png create mode 100644 core/src/main/res/drawable-mdpi/av_play.png create mode 100755 core/src/main/res/drawable-mdpi/av_play_dark.png create mode 100644 core/src/main/res/drawable-mdpi/av_rewind.png create mode 100755 core/src/main/res/drawable-mdpi/av_rewind_dark.png create mode 100644 core/src/main/res/drawable-mdpi/content_discard.png create mode 100755 core/src/main/res/drawable-mdpi/content_discard_dark.png create mode 100644 core/src/main/res/drawable-mdpi/content_new.png create mode 100755 core/src/main/res/drawable-mdpi/content_new_dark.png create mode 100644 core/src/main/res/drawable-mdpi/default_cover.png create mode 100755 core/src/main/res/drawable-mdpi/default_cover_dark.png create mode 100644 core/src/main/res/drawable-mdpi/device_access_time.png create mode 100755 core/src/main/res/drawable-mdpi/device_access_time_dark.png create mode 100644 core/src/main/res/drawable-mdpi/ic_action_overflow.png create mode 100644 core/src/main/res/drawable-mdpi/ic_action_overflow_dark.png create mode 100755 core/src/main/res/drawable-mdpi/ic_action_pause_over_video.png create mode 100755 core/src/main/res/drawable-mdpi/ic_action_play_over_video.png create mode 100755 core/src/main/res/drawable-mdpi/ic_drag_handle.png create mode 100755 core/src/main/res/drawable-mdpi/ic_drag_handle_dark.png create mode 100644 core/src/main/res/drawable-mdpi/ic_drawer.png create mode 100644 core/src/main/res/drawable-mdpi/ic_drawer_dark.png create mode 100644 core/src/main/res/drawable-mdpi/ic_launcher.png create mode 100755 core/src/main/res/drawable-mdpi/ic_new.png create mode 100755 core/src/main/res/drawable-mdpi/ic_new_dark.png create mode 100644 core/src/main/res/drawable-mdpi/ic_stat_antenna.png create mode 100755 core/src/main/res/drawable-mdpi/ic_stat_authentication.png create mode 100644 core/src/main/res/drawable-mdpi/location_web_site.png create mode 100755 core/src/main/res/drawable-mdpi/location_web_site_dark.png create mode 100644 core/src/main/res/drawable-mdpi/navigation_accept.png create mode 100755 core/src/main/res/drawable-mdpi/navigation_accept_dark.png create mode 100644 core/src/main/res/drawable-mdpi/navigation_cancel.png create mode 100755 core/src/main/res/drawable-mdpi/navigation_cancel_dark.png create mode 100755 core/src/main/res/drawable-mdpi/navigation_chapters.png create mode 100755 core/src/main/res/drawable-mdpi/navigation_chapters_dark.png create mode 100755 core/src/main/res/drawable-mdpi/navigation_collapse.png create mode 100755 core/src/main/res/drawable-mdpi/navigation_collapse_dark.png create mode 100644 core/src/main/res/drawable-mdpi/navigation_expand.png create mode 100755 core/src/main/res/drawable-mdpi/navigation_expand_dark.png create mode 100644 core/src/main/res/drawable-mdpi/navigation_refresh.png create mode 100755 core/src/main/res/drawable-mdpi/navigation_refresh_dark.png create mode 100755 core/src/main/res/drawable-mdpi/navigation_shownotes.png create mode 100755 core/src/main/res/drawable-mdpi/navigation_shownotes_dark.png create mode 100755 core/src/main/res/drawable-mdpi/navigation_up.png create mode 100755 core/src/main/res/drawable-mdpi/navigation_up_dark.png create mode 100644 core/src/main/res/drawable-mdpi/social_share.png create mode 100755 core/src/main/res/drawable-mdpi/social_share_dark.png create mode 100644 core/src/main/res/drawable-mdpi/spinner_button.9.png create mode 100644 core/src/main/res/drawable-mdpi/spinner_button_dark.9.png create mode 100644 core/src/main/res/drawable-mdpi/stat_notify_sync.png create mode 100644 core/src/main/res/drawable-mdpi/stat_notify_sync_error.png create mode 100644 core/src/main/res/drawable-mdpi/stat_playlist.png create mode 100644 core/src/main/res/drawable-mdpi/stat_playlist_dark.png create mode 100644 core/src/main/res/drawable-mdpi/type_audio.png create mode 100755 core/src/main/res/drawable-mdpi/type_audio_dark.png create mode 100644 core/src/main/res/drawable-mdpi/type_video.png create mode 100755 core/src/main/res/drawable-mdpi/type_video_dark.png create mode 100644 core/src/main/res/drawable-xhdpi-v11/ic_stat_antenna.png create mode 100755 core/src/main/res/drawable-xhdpi-v11/ic_stat_authentication.png create mode 100644 core/src/main/res/drawable-xhdpi-v11/stat_notify_sync.png create mode 100644 core/src/main/res/drawable-xhdpi-v11/stat_notify_sync_error.png create mode 100644 core/src/main/res/drawable-xhdpi/action_about.png create mode 100755 core/src/main/res/drawable-xhdpi/action_about_dark.png create mode 100644 core/src/main/res/drawable-xhdpi/action_search.png create mode 100755 core/src/main/res/drawable-xhdpi/action_search_dark.png create mode 100644 core/src/main/res/drawable-xhdpi/action_settings.png create mode 100755 core/src/main/res/drawable-xhdpi/action_settings_dark.png create mode 100644 core/src/main/res/drawable-xhdpi/action_stream.png create mode 100644 core/src/main/res/drawable-xhdpi/action_stream_dark.png create mode 100644 core/src/main/res/drawable-xhdpi/av_download.png create mode 100755 core/src/main/res/drawable-xhdpi/av_download_dark.png create mode 100644 core/src/main/res/drawable-xhdpi/av_fast_forward.png create mode 100755 core/src/main/res/drawable-xhdpi/av_fast_forward_dark.png create mode 100644 core/src/main/res/drawable-xhdpi/av_pause.png create mode 100755 core/src/main/res/drawable-xhdpi/av_pause_dark.png create mode 100644 core/src/main/res/drawable-xhdpi/av_play.png create mode 100755 core/src/main/res/drawable-xhdpi/av_play_dark.png create mode 100644 core/src/main/res/drawable-xhdpi/av_rewind.png create mode 100755 core/src/main/res/drawable-xhdpi/av_rewind_dark.png create mode 100644 core/src/main/res/drawable-xhdpi/content_discard.png create mode 100755 core/src/main/res/drawable-xhdpi/content_discard_dark.png create mode 100644 core/src/main/res/drawable-xhdpi/content_new.png create mode 100755 core/src/main/res/drawable-xhdpi/content_new_dark.png create mode 100644 core/src/main/res/drawable-xhdpi/content_remove.png create mode 100755 core/src/main/res/drawable-xhdpi/content_remove_dark.png create mode 100644 core/src/main/res/drawable-xhdpi/default_cover.png create mode 100755 core/src/main/res/drawable-xhdpi/default_cover_dark.png create mode 100644 core/src/main/res/drawable-xhdpi/device_access_time.png create mode 100755 core/src/main/res/drawable-xhdpi/device_access_time_dark.png create mode 100644 core/src/main/res/drawable-xhdpi/ic_action_overflow.png create mode 100644 core/src/main/res/drawable-xhdpi/ic_action_overflow_dark.png create mode 100755 core/src/main/res/drawable-xhdpi/ic_action_pause_over_video.png create mode 100755 core/src/main/res/drawable-xhdpi/ic_action_play_over_video.png create mode 100755 core/src/main/res/drawable-xhdpi/ic_drag_handle.png create mode 100755 core/src/main/res/drawable-xhdpi/ic_drag_handle_dark.png create mode 100644 core/src/main/res/drawable-xhdpi/ic_drawer.png create mode 100644 core/src/main/res/drawable-xhdpi/ic_drawer_dark.png create mode 100644 core/src/main/res/drawable-xhdpi/ic_launcher.png create mode 100755 core/src/main/res/drawable-xhdpi/ic_new.png create mode 100755 core/src/main/res/drawable-xhdpi/ic_new_dark.png create mode 100644 core/src/main/res/drawable-xhdpi/ic_stat_antenna.png create mode 100755 core/src/main/res/drawable-xhdpi/ic_stat_authentication.png create mode 100644 core/src/main/res/drawable-xhdpi/ic_undobar_undo.png create mode 100644 core/src/main/res/drawable-xhdpi/location_web_site.png create mode 100755 core/src/main/res/drawable-xhdpi/location_web_site_dark.png create mode 100644 core/src/main/res/drawable-xhdpi/navigation_accept.png create mode 100755 core/src/main/res/drawable-xhdpi/navigation_accept_dark.png create mode 100644 core/src/main/res/drawable-xhdpi/navigation_cancel.png create mode 100755 core/src/main/res/drawable-xhdpi/navigation_cancel_dark.png create mode 100755 core/src/main/res/drawable-xhdpi/navigation_chapters.png create mode 100755 core/src/main/res/drawable-xhdpi/navigation_chapters_dark.png create mode 100755 core/src/main/res/drawable-xhdpi/navigation_collapse.png create mode 100755 core/src/main/res/drawable-xhdpi/navigation_collapse_dark.png create mode 100644 core/src/main/res/drawable-xhdpi/navigation_expand.png create mode 100755 core/src/main/res/drawable-xhdpi/navigation_expand_dark.png create mode 100644 core/src/main/res/drawable-xhdpi/navigation_refresh.png create mode 100755 core/src/main/res/drawable-xhdpi/navigation_refresh_dark.png create mode 100755 core/src/main/res/drawable-xhdpi/navigation_shownotes.png create mode 100755 core/src/main/res/drawable-xhdpi/navigation_shownotes_dark.png create mode 100755 core/src/main/res/drawable-xhdpi/navigation_up.png create mode 100755 core/src/main/res/drawable-xhdpi/navigation_up_dark.png create mode 100644 core/src/main/res/drawable-xhdpi/social_share.png create mode 100755 core/src/main/res/drawable-xhdpi/social_share_dark.png create mode 100644 core/src/main/res/drawable-xhdpi/spinner_button.9.png create mode 100644 core/src/main/res/drawable-xhdpi/spinner_button_dark.9.png create mode 100644 core/src/main/res/drawable-xhdpi/stat_playlist.png create mode 100644 core/src/main/res/drawable-xhdpi/stat_playlist_dark.png create mode 100644 core/src/main/res/drawable-xhdpi/type_audio.png create mode 100755 core/src/main/res/drawable-xhdpi/type_audio_dark.png create mode 100644 core/src/main/res/drawable-xhdpi/type_video.png create mode 100755 core/src/main/res/drawable-xhdpi/type_video_dark.png create mode 100644 core/src/main/res/drawable-xhdpi/undobar.9.png create mode 100644 core/src/main/res/drawable-xhdpi/undobar_button_focused.9.png create mode 100644 core/src/main/res/drawable-xhdpi/undobar_button_pressed.9.png create mode 100644 core/src/main/res/drawable-xhdpi/undobar_divider.9.png create mode 100644 core/src/main/res/drawable-xxhdpi/ic_action_overflow.png create mode 100644 core/src/main/res/drawable-xxhdpi/ic_action_overflow_dark.png create mode 100755 core/src/main/res/drawable-xxhdpi/ic_action_pause_over_video.png create mode 100755 core/src/main/res/drawable-xxhdpi/ic_action_play_over_video.png create mode 100755 core/src/main/res/drawable-xxhdpi/ic_drag_handle.png create mode 100755 core/src/main/res/drawable-xxhdpi/ic_drag_handle_dark.png create mode 100644 core/src/main/res/drawable-xxhdpi/ic_drawer.png create mode 100644 core/src/main/res/drawable-xxhdpi/ic_drawer_dark.png create mode 100644 core/src/main/res/drawable-xxhdpi/ic_launcher.png create mode 100755 core/src/main/res/drawable-xxhdpi/ic_new.png create mode 100755 core/src/main/res/drawable-xxhdpi/ic_new_dark.png create mode 100755 core/src/main/res/drawable-xxhdpi/ic_stat_authentication.png create mode 100644 core/src/main/res/drawable/badge.xml create mode 100644 core/src/main/res/drawable/borderless_button.xml create mode 100644 core/src/main/res/drawable/borderless_button_dark.xml create mode 100644 core/src/main/res/drawable/horizontal_divider.9.png create mode 100644 core/src/main/res/drawable/overlay_button_circle_background.xml create mode 100644 core/src/main/res/drawable/overlay_drawable.xml create mode 100644 core/src/main/res/drawable/overlay_drawable_dark.xml create mode 100644 core/src/main/res/drawable/type_audio.png create mode 100644 core/src/main/res/drawable/type_video.png create mode 100644 core/src/main/res/drawable/undobar_button.xml create mode 100644 core/src/main/res/drawable/vertical_divider.9.png create mode 100644 core/src/main/res/drawable/white_circle.xml create mode 100644 core/src/main/res/values-az/strings.xml create mode 100644 core/src/main/res/values-ca/strings.xml create mode 100644 core/src/main/res/values-cs-rCZ/strings.xml create mode 100644 core/src/main/res/values-da/strings.xml create mode 100644 core/src/main/res/values-de/strings.xml create mode 100644 core/src/main/res/values-es-rES/strings.xml create mode 100644 core/src/main/res/values-es/strings.xml create mode 100644 core/src/main/res/values-fr/strings.xml create mode 100644 core/src/main/res/values-hi-rIN/strings.xml create mode 100644 core/src/main/res/values-it-rIT/strings.xml create mode 100644 core/src/main/res/values-iw-rIL/strings.xml create mode 100644 core/src/main/res/values-ko/strings.xml create mode 100644 core/src/main/res/values-land/styles.xml create mode 100644 core/src/main/res/values-large/dimens.xml create mode 100644 core/src/main/res/values-nl/strings.xml create mode 100644 core/src/main/res/values-pl-rPL/strings.xml create mode 100644 core/src/main/res/values-pt-rBR/strings.xml create mode 100644 core/src/main/res/values-pt/strings.xml create mode 100644 core/src/main/res/values-ro-rRO/strings.xml create mode 100644 core/src/main/res/values-ru/strings.xml create mode 100644 core/src/main/res/values-sv-rSE/strings.xml create mode 100644 core/src/main/res/values-uk-rUA/strings.xml create mode 100644 core/src/main/res/values-v11/colors.xml create mode 100644 core/src/main/res/values-v14/dimens.xml create mode 100644 core/src/main/res/values-v14/styles.xml create mode 100644 core/src/main/res/values-v16/styles.xml create mode 100644 core/src/main/res/values-v19/colors.xml create mode 100644 core/src/main/res/values-zh-rCN/strings.xml create mode 100644 core/src/main/res/values/arrays.xml create mode 100644 core/src/main/res/values/attrs.xml create mode 100644 core/src/main/res/values/colors.xml create mode 100644 core/src/main/res/values/dimens.xml create mode 100644 core/src/main/res/values/ids.xml create mode 100644 core/src/main/res/values/integers.xml create mode 100644 core/src/main/res/values/strings.xml create mode 100644 core/src/main/res/values/styles.xml diff --git a/app/build.gradle b/app/build.gradle index 1295a81dc..2e4b1da7d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -15,7 +15,7 @@ dependencies { compile 'com.android.support:support-v4:20.0.0' compile 'com.android.support:appcompat-v7:20.0.0' compile 'org.apache.commons:commons-lang3:3.3.2' - compile ('org.shredzone.flattr4j:flattr4j-core:2.10') { + compile('org.shredzone.flattr4j:flattr4j-core:2.10') { exclude group: 'org.apache.httpcomponents', module: 'httpcore' exclude group: 'org.apache.httpcomponents', module: 'httpclient' exclude group: 'org.json', module: 'json' @@ -23,9 +23,8 @@ dependencies { compile 'commons-io:commons-io:2.4' compile 'com.nineoldandroids:library:2.4.0' compile project('dslv:library') - compile 'com.jayway.android.robotium:robotium-solo:5.2.1' - compile ("com.doomonafireball.betterpickers:library:1.5.2") { + compile('com.doomonafireball.betterpickers:library:1.5.2') { exclude group: 'com.android.support', module: 'support-v4' } compile 'org.jsoup:jsoup:1.7.3' @@ -33,6 +32,7 @@ dependencies { compile 'com.squareup.okhttp:okhttp:2.0.0' compile 'com.squareup.okhttp:okhttp-urlconnection:2.0.0' compile 'com.squareup.okio:okio:1.0.0' + compile project(':core') } android { diff --git a/app/core/build.gradle b/app/core/build.gradle deleted file mode 100644 index a552f8313..000000000 --- a/app/core/build.gradle +++ /dev/null @@ -1,83 +0,0 @@ -buildscript { - repositories { - mavenCentral() - } - dependencies { - classpath 'com.android.tools.build:gradle:0.12.2' - } -} -apply plugin: 'android-library' - -repositories { - mavenCentral() -} -dependencies { - compile 'com.android.support:support-v4:20.0.0' - compile 'com.android.support:appcompat-v7:20.0.0' - compile 'org.apache.commons:commons-lang3:3.3.2' - compile ('org.shredzone.flattr4j:flattr4j-core:2.10') { - exclude group: 'org.apache.httpcomponents', module: 'httpcore' - exclude group: 'org.apache.httpcomponents', module: 'httpclient' - exclude group: 'org.json', module: 'json' - } - compile 'commons-io:commons-io:2.4' - compile 'com.nineoldandroids:library:2.4.0' - compile project('dslv:library') - - compile 'com.jayway.android.robotium:robotium-solo:5.2.1' - compile ("com.doomonafireball.betterpickers:library:1.5.2") { - exclude group: 'com.android.support', module: 'support-v4' - } - compile 'org.jsoup:jsoup:1.7.3' - compile 'com.squareup.picasso:picasso:2.3.4' - compile 'com.squareup.okhttp:okhttp:2.0.0' - compile 'com.squareup.okhttp:okhttp-urlconnection:2.0.0' - compile 'com.squareup.okio:okio:1.0.0' -} - -android { - compileSdkVersion 19 - buildToolsVersion "20.0" - - defaultConfig { - minSdkVersion 10 - targetSdkVersion 19 - testApplicationId "de.test.antennapod" - testInstrumentationRunner "de.test.antennapod.AntennaPodTestRunner" - } - - buildTypes { - def STRING = "String" - def FLATTR_APP_KEY = "FLATTR_APP_KEY" - def FLATTR_APP_SECRET = "FLATTR_APP_SECRET" - def mFlattrAppKey = (project.hasProperty('flattrAppKey')) ? flattrAppKey : "\"\"" - def mFlattrAppSecret = (project.hasProperty('flattrAppSecret')) ? flattrAppSecret : "\"\"" - - debug { - applicationIdSuffix ".debug" - buildConfigField STRING, FLATTR_APP_KEY, mFlattrAppKey - buildConfigField STRING, FLATTR_APP_SECRET, mFlattrAppSecret - } - release { - runProguard true - proguardFile 'proguard.cfg' - signingConfig signingConfigs.releaseConfig - buildConfigField STRING, FLATTR_APP_KEY, mFlattrAppKey - buildConfigField STRING, FLATTR_APP_SECRET, mFlattrAppSecret - } - } - - packagingOptions { - exclude 'META-INF/LICENSE.txt' - exclude 'META-INF/NOTICE.txt' - } - - lintOptions { - abortOnError false - } - - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_7 - targetCompatibility JavaVersion.VERSION_1_7 - } -} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 768a4abb0..3410d9d53 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -112,12 +112,12 @@ - + diff --git a/app/src/main/aidl/com/aocate/presto/service/IDeathCallback_0_8.aidl b/app/src/main/aidl/com/aocate/presto/service/IDeathCallback_0_8.aidl deleted file mode 100644 index 6bdc76801..000000000 --- a/app/src/main/aidl/com/aocate/presto/service/IDeathCallback_0_8.aidl +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright 2011, Aocate, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.aocate.presto.service; - -oneway interface IDeathCallback_0_8 { -} \ No newline at end of file diff --git a/app/src/main/aidl/com/aocate/presto/service/IOnBufferingUpdateListenerCallback_0_8.aidl b/app/src/main/aidl/com/aocate/presto/service/IOnBufferingUpdateListenerCallback_0_8.aidl deleted file mode 100644 index 7357e402e..000000000 --- a/app/src/main/aidl/com/aocate/presto/service/IOnBufferingUpdateListenerCallback_0_8.aidl +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright 2011, Aocate, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.aocate.presto.service; - -interface IOnBufferingUpdateListenerCallback_0_8 { - void onBufferingUpdate(int percent); -} \ No newline at end of file diff --git a/app/src/main/aidl/com/aocate/presto/service/IOnCompletionListenerCallback_0_8.aidl b/app/src/main/aidl/com/aocate/presto/service/IOnCompletionListenerCallback_0_8.aidl deleted file mode 100644 index d5edea729..000000000 --- a/app/src/main/aidl/com/aocate/presto/service/IOnCompletionListenerCallback_0_8.aidl +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright 2011, Aocate, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.aocate.presto.service; - -interface IOnCompletionListenerCallback_0_8 { - void onCompletion(); -} \ No newline at end of file diff --git a/app/src/main/aidl/com/aocate/presto/service/IOnErrorListenerCallback_0_8.aidl b/app/src/main/aidl/com/aocate/presto/service/IOnErrorListenerCallback_0_8.aidl deleted file mode 100644 index 2c4f2df3e..000000000 --- a/app/src/main/aidl/com/aocate/presto/service/IOnErrorListenerCallback_0_8.aidl +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright 2011, Aocate, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.aocate.presto.service; - -interface IOnErrorListenerCallback_0_8 { - boolean onError(int what, int extra); -} diff --git a/app/src/main/aidl/com/aocate/presto/service/IOnInfoListenerCallback_0_8.aidl b/app/src/main/aidl/com/aocate/presto/service/IOnInfoListenerCallback_0_8.aidl deleted file mode 100644 index 9dbd1d260..000000000 --- a/app/src/main/aidl/com/aocate/presto/service/IOnInfoListenerCallback_0_8.aidl +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright 2011, Aocate, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.aocate.presto.service; - -interface IOnInfoListenerCallback_0_8 { - boolean onInfo(int what, int extra); -} \ No newline at end of file diff --git a/app/src/main/aidl/com/aocate/presto/service/IOnPitchAdjustmentAvailableChangedListenerCallback_0_8.aidl b/app/src/main/aidl/com/aocate/presto/service/IOnPitchAdjustmentAvailableChangedListenerCallback_0_8.aidl deleted file mode 100644 index 41223a97b..000000000 --- a/app/src/main/aidl/com/aocate/presto/service/IOnPitchAdjustmentAvailableChangedListenerCallback_0_8.aidl +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright 2011, Aocate, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.aocate.presto.service; - -interface IOnPitchAdjustmentAvailableChangedListenerCallback_0_8 { - void onPitchAdjustmentAvailableChanged(boolean pitchAdjustmentAvailable); -} \ No newline at end of file diff --git a/app/src/main/aidl/com/aocate/presto/service/IOnPreparedListenerCallback_0_8.aidl b/app/src/main/aidl/com/aocate/presto/service/IOnPreparedListenerCallback_0_8.aidl deleted file mode 100644 index 7be8f1237..000000000 --- a/app/src/main/aidl/com/aocate/presto/service/IOnPreparedListenerCallback_0_8.aidl +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright 2011, Aocate, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.aocate.presto.service; - -interface IOnPreparedListenerCallback_0_8 { - void onPrepared(); -} \ No newline at end of file diff --git a/app/src/main/aidl/com/aocate/presto/service/IOnSeekCompleteListenerCallback_0_8.aidl b/app/src/main/aidl/com/aocate/presto/service/IOnSeekCompleteListenerCallback_0_8.aidl deleted file mode 100644 index 5bdda98b6..000000000 --- a/app/src/main/aidl/com/aocate/presto/service/IOnSeekCompleteListenerCallback_0_8.aidl +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright 2011, Aocate, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.aocate.presto.service; - -interface IOnSeekCompleteListenerCallback_0_8 { - void onSeekComplete(); -} \ No newline at end of file diff --git a/app/src/main/aidl/com/aocate/presto/service/IOnSpeedAdjustmentAvailableChangedListenerCallback_0_8.aidl b/app/src/main/aidl/com/aocate/presto/service/IOnSpeedAdjustmentAvailableChangedListenerCallback_0_8.aidl deleted file mode 100644 index a69c1cf34..000000000 --- a/app/src/main/aidl/com/aocate/presto/service/IOnSpeedAdjustmentAvailableChangedListenerCallback_0_8.aidl +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright 2011, Aocate, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.aocate.presto.service; - -interface IOnSpeedAdjustmentAvailableChangedListenerCallback_0_8 { - void onSpeedAdjustmentAvailableChanged(boolean speedAdjustmentAvailable); -} \ No newline at end of file diff --git a/app/src/main/aidl/com/aocate/presto/service/IPlayMedia_0_8.aidl b/app/src/main/aidl/com/aocate/presto/service/IPlayMedia_0_8.aidl deleted file mode 100644 index 12a6047de..000000000 --- a/app/src/main/aidl/com/aocate/presto/service/IPlayMedia_0_8.aidl +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright 2011, Aocate, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.aocate.presto.service; - -import com.aocate.presto.service.IDeathCallback_0_8; -import com.aocate.presto.service.IOnBufferingUpdateListenerCallback_0_8; -import com.aocate.presto.service.IOnCompletionListenerCallback_0_8; -import com.aocate.presto.service.IOnErrorListenerCallback_0_8; -import com.aocate.presto.service.IOnPitchAdjustmentAvailableChangedListenerCallback_0_8; -import com.aocate.presto.service.IOnPreparedListenerCallback_0_8; -import com.aocate.presto.service.IOnSeekCompleteListenerCallback_0_8; -import com.aocate.presto.service.IOnSpeedAdjustmentAvailableChangedListenerCallback_0_8; -import com.aocate.presto.service.IOnInfoListenerCallback_0_8; - -interface IPlayMedia_0_8 { - boolean canSetPitch(long sessionId); - boolean canSetSpeed(long sessionId); - float getCurrentPitchStepsAdjustment(long sessionId); - int getCurrentPosition(long sessionId); - float getCurrentSpeedMultiplier(long sessionId); - int getDuration(long sessionId); - float getMaxSpeedMultiplier(long sessionId); - float getMinSpeedMultiplier(long sessionId); - int getVersionCode(); - String getVersionName(); - boolean isLooping(long sessionId); - boolean isPlaying(long sessionId); - void pause(long sessionId); - void prepare(long sessionId); - void prepareAsync(long sessionId); - void registerOnBufferingUpdateCallback(long sessionId, IOnBufferingUpdateListenerCallback_0_8 cb); - void registerOnCompletionCallback(long sessionId, IOnCompletionListenerCallback_0_8 cb); - void registerOnErrorCallback(long sessionId, IOnErrorListenerCallback_0_8 cb); - void registerOnInfoCallback(long sessionId, IOnInfoListenerCallback_0_8 cb); - void registerOnPitchAdjustmentAvailableChangedCallback(long sessionId, IOnPitchAdjustmentAvailableChangedListenerCallback_0_8 cb); - void registerOnPreparedCallback(long sessionId, IOnPreparedListenerCallback_0_8 cb); - void registerOnSeekCompleteCallback(long sessionId, IOnSeekCompleteListenerCallback_0_8 cb); - void registerOnSpeedAdjustmentAvailableChangedCallback(long sessionId, IOnSpeedAdjustmentAvailableChangedListenerCallback_0_8 cb); - void release(long sessionId); - void reset(long sessionId); - void seekTo(long sessionId, int msec); - void setAudioStreamType(long sessionId, int streamtype); - void setDataSourceString(long sessionId, String path); - void setDataSourceUri(long sessionId, in Uri uri); - void setEnableSpeedAdjustment(long sessionId, boolean enableSpeedAdjustment); - void setLooping(long sessionId, boolean looping); - void setPitchStepsAdjustment(long sessionId, float pitchSteps); - void setPlaybackPitch(long sessionId, float f); - void setPlaybackSpeed(long sessionId, float f); - void setSpeedAdjustmentAlgorithm(long sessionId, int algorithm); - void setVolume(long sessionId, float left, float right); - void start(long sessionId); - long startSession(IDeathCallback_0_8 cb); - void stop(long sessionId); - void unregisterOnBufferingUpdateCallback(long sessionId, IOnBufferingUpdateListenerCallback_0_8 cb); - void unregisterOnCompletionCallback(long sessionId, IOnCompletionListenerCallback_0_8 cb); - void unregisterOnErrorCallback(long sessionId, IOnErrorListenerCallback_0_8 cb); - void unregisterOnInfoCallback(long sessionId, IOnInfoListenerCallback_0_8 cb); - void unregisterOnPitchAdjustmentAvailableChangedCallback(long sessionId, IOnPitchAdjustmentAvailableChangedListenerCallback_0_8 cb); - void unregisterOnPreparedCallback(long sessionId, IOnPreparedListenerCallback_0_8 cb); - void unregisterOnSeekCompleteCallback(long sessionId, IOnSeekCompleteListenerCallback_0_8 cb); - void unregisterOnSpeedAdjustmentAvailableChangedCallback(long sessionId, IOnSpeedAdjustmentAvailableChangedListenerCallback_0_8 cb); -} \ No newline at end of file diff --git a/app/src/main/java/com/aocate/media/AndroidMediaPlayer.java b/app/src/main/java/com/aocate/media/AndroidMediaPlayer.java deleted file mode 100644 index 17ee74a13..000000000 --- a/app/src/main/java/com/aocate/media/AndroidMediaPlayer.java +++ /dev/null @@ -1,470 +0,0 @@ -// Copyright 2011, Aocate, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.aocate.media; - -import java.io.IOException; - -import android.content.Context; -import android.media.MediaPlayer; -import android.net.Uri; -import android.util.Log; - -public class AndroidMediaPlayer extends MediaPlayerImpl { - private final static String AMP_TAG = "AocateAndroidMediaPlayer"; - - // private static final long TIMEOUT_DURATION_MS = 500; - - android.media.MediaPlayer mp = null; - - private android.media.MediaPlayer.OnBufferingUpdateListener onBufferingUpdateListener = new android.media.MediaPlayer.OnBufferingUpdateListener() { - public void onBufferingUpdate(android.media.MediaPlayer mp, int percent) { - if (owningMediaPlayer != null) { - owningMediaPlayer.lock.lock(); - try { - if ((owningMediaPlayer.onBufferingUpdateListener != null) - && (owningMediaPlayer.mpi == AndroidMediaPlayer.this)) { - owningMediaPlayer.onBufferingUpdateListener.onBufferingUpdate(owningMediaPlayer, percent); - } - } - finally { - owningMediaPlayer.lock.unlock(); - } - } - - } - }; - - private android.media.MediaPlayer.OnCompletionListener onCompletionListener = new android.media.MediaPlayer.OnCompletionListener() { - public void onCompletion(android.media.MediaPlayer mp) { - Log.d(AMP_TAG, "onCompletionListener being called"); - if (owningMediaPlayer != null) { - owningMediaPlayer.lock.lock(); - try { - if (owningMediaPlayer.onCompletionListener != null) { - owningMediaPlayer.onCompletionListener.onCompletion(owningMediaPlayer); - } - } - finally { - owningMediaPlayer.lock.unlock(); - } - } - } - }; - - private android.media.MediaPlayer.OnErrorListener onErrorListener = new android.media.MediaPlayer.OnErrorListener() { - public boolean onError(android.media.MediaPlayer mp, int what, int extra) { - // Once we're in errored state, any received messages are going to be junked - if (owningMediaPlayer != null) { - owningMediaPlayer.lock.lock(); - try { - if (owningMediaPlayer.onErrorListener != null) { - return owningMediaPlayer.onErrorListener.onError(owningMediaPlayer, what, extra); - } - } - finally { - owningMediaPlayer.lock.unlock(); - } - } - return false; - } - }; - - private android.media.MediaPlayer.OnInfoListener onInfoListener = new android.media.MediaPlayer.OnInfoListener() { - public boolean onInfo(android.media.MediaPlayer mp, int what, int extra) { - if (owningMediaPlayer != null) { - owningMediaPlayer.lock.lock(); - try { - if ((owningMediaPlayer.onInfoListener != null) - && (owningMediaPlayer.mpi == AndroidMediaPlayer.this)) { - return owningMediaPlayer.onInfoListener.onInfo(owningMediaPlayer, what, extra); - } - } - finally { - owningMediaPlayer.lock.unlock(); - } - } - return false; - } - }; - - // We have to assign this.onPreparedListener because the - // onPreparedListener in owningMediaPlayer sets the state - // to PREPARED. Due to prepareAsync, that's the only - // reasonable place to do it - // The others it just didn't make sense to have a setOnXListener that didn't use the parameter - private android.media.MediaPlayer.OnPreparedListener onPreparedListener = new android.media.MediaPlayer.OnPreparedListener() { - public void onPrepared(android.media.MediaPlayer mp) { - Log.d(AMP_TAG, "Calling onPreparedListener.onPrepared()"); - if (AndroidMediaPlayer.this.owningMediaPlayer != null) { - AndroidMediaPlayer.this.lockMuteOnPreparedCount.lock(); - try { - if (AndroidMediaPlayer.this.muteOnPreparedCount > 0) { - AndroidMediaPlayer.this.muteOnPreparedCount--; - } - else { - AndroidMediaPlayer.this.muteOnPreparedCount = 0; - if (AndroidMediaPlayer.this.owningMediaPlayer.onPreparedListener != null) { - Log.d(AMP_TAG, "Invoking AndroidMediaPlayer.this.owningMediaPlayer.onPreparedListener.onPrepared"); - AndroidMediaPlayer.this.owningMediaPlayer.onPreparedListener.onPrepared(AndroidMediaPlayer.this.owningMediaPlayer); - } - } - } - finally { - AndroidMediaPlayer.this.lockMuteOnPreparedCount.unlock(); - } - if (owningMediaPlayer.mpi != AndroidMediaPlayer.this) { - Log.d(AMP_TAG, "owningMediaPlayer has changed implementation"); - } - } - } - }; - - private android.media.MediaPlayer.OnSeekCompleteListener onSeekCompleteListener = new android.media.MediaPlayer.OnSeekCompleteListener() { - public void onSeekComplete(android.media.MediaPlayer mp) { - if (owningMediaPlayer != null) { - owningMediaPlayer.lock.lock(); - try { - lockMuteOnSeekCount.lock(); - try { - if (AndroidMediaPlayer.this.muteOnSeekCount > 0) { - AndroidMediaPlayer.this.muteOnSeekCount--; - } - else { - AndroidMediaPlayer.this.muteOnSeekCount = 0; - if (AndroidMediaPlayer.this.owningMediaPlayer.onSeekCompleteListener != null) { - owningMediaPlayer.onSeekCompleteListener.onSeekComplete(owningMediaPlayer); - } - } - } - finally { - lockMuteOnSeekCount.unlock(); - } - } - finally { - owningMediaPlayer.lock.unlock(); - } - } - } - }; - - public AndroidMediaPlayer(com.aocate.media.MediaPlayer owningMediaPlayer, Context context) { - super(owningMediaPlayer, context); - - mp = new MediaPlayer(); - -// final ReentrantLock lock = new ReentrantLock(); -// Handler handler = new Handler(Looper.getMainLooper()) { -// @Override -// public void handleMessage(Message msg) { -// Log.d(AMP_TAG, "Instantiating new AndroidMediaPlayer from Handler"); -// lock.lock(); -// if (mp == null) { -// mp = new MediaPlayer(); -// } -// lock.unlock(); -// } -// }; -// -// long endTime = System.currentTimeMillis() + TIMEOUT_DURATION_MS; -// -// while (true) { -// // Retry messages until mp isn't null or it's time to give up -// handler.sendMessage(handler.obtainMessage()); -// if ((mp != null) -// || (endTime < System.currentTimeMillis())) { -// break; -// } -// try { -// Thread.sleep(50); -// } catch (InterruptedException e) { -// // TODO Auto-generated catch block -// e.printStackTrace(); -// } -// } - - if (mp == null) { - throw new IllegalStateException("Did not instantiate android.media.MediaPlayer successfully"); - } - - mp.setOnBufferingUpdateListener(this.onBufferingUpdateListener); - mp.setOnCompletionListener(this.onCompletionListener); - mp.setOnErrorListener(this.onErrorListener); - mp.setOnInfoListener(this.onInfoListener); - Log.d(AMP_TAG, " ++++++++++++++++++++++++++++++++ Setting prepared listener to this.onPreparedListener"); - mp.setOnPreparedListener(this.onPreparedListener); - mp.setOnSeekCompleteListener(this.onSeekCompleteListener); - } - - @Override - public boolean canSetPitch() { - return false; - } - - @Override - public boolean canSetSpeed() { - return false; - } - - @Override - public float getCurrentPitchStepsAdjustment() { - return 0; - } - - @Override - public int getCurrentPosition() { - owningMediaPlayer.lock.lock(); - try { - return mp.getCurrentPosition(); - } - finally { - owningMediaPlayer.lock.unlock(); - } - } - - @Override - public float getCurrentSpeedMultiplier() { - return 1f; - } - - @Override - public int getDuration() { - owningMediaPlayer.lock.lock(); - try { - return mp.getDuration(); - } - finally { - owningMediaPlayer.lock.unlock(); - } - } - - @Override - public float getMaxSpeedMultiplier() { - return 1f; - } - - @Override - public float getMinSpeedMultiplier() { - return 1f; - } - - @Override - public boolean isLooping() { - owningMediaPlayer.lock.lock(); - try { - return mp.isLooping(); - } - finally { - owningMediaPlayer.lock.unlock(); - } - } - - @Override - public boolean isPlaying() { - owningMediaPlayer.lock.lock(); - try { - return mp.isPlaying(); - } - finally { - owningMediaPlayer.lock.unlock(); - } - } - - @Override - public void pause() { - owningMediaPlayer.lock.lock(); - try { - mp.pause(); - } - finally { - owningMediaPlayer.lock.unlock(); - } - } - - @Override - public void prepare() throws IllegalStateException, IOException { - owningMediaPlayer.lock.lock(); - Log.d(AMP_TAG, "prepare()"); - try { - mp.prepare(); - Log.d(AMP_TAG, "Finish prepare()"); - } - finally { - owningMediaPlayer.lock.unlock(); - } - } - - @Override - public void prepareAsync() { - mp.prepareAsync(); - } - - @Override - public void release() { - owningMediaPlayer.lock.lock(); - try { - if (mp != null) { - Log.d(AMP_TAG, "mp.release()"); - mp.release(); - } - } - finally { - owningMediaPlayer.lock.unlock(); - } - } - - @Override - public void reset() { - owningMediaPlayer.lock.lock(); - try { - mp.reset(); - } - finally { - owningMediaPlayer.lock.unlock(); - } - } - - @Override - public void seekTo(int msec) throws IllegalStateException { - owningMediaPlayer.lock.lock(); - try { - mp.setOnSeekCompleteListener(this.onSeekCompleteListener); - mp.seekTo(msec); - } - finally { - owningMediaPlayer.lock.unlock(); - } - } - - @Override - public void setAudioStreamType(int streamtype) { - owningMediaPlayer.lock.lock(); - try { - mp.setAudioStreamType(streamtype); - } - finally { - owningMediaPlayer.lock.unlock(); - } - } - - @Override - public void setDataSource(Context context, Uri uri) - throws IllegalArgumentException, IllegalStateException, IOException { - owningMediaPlayer.lock.lock(); - try { - Log.d(AMP_TAG, "setDataSource(context, " + uri.toString() + ")"); - mp.setDataSource(context, uri); - } - finally { - owningMediaPlayer.lock.unlock(); - } - } - - @Override - public void setDataSource(String path) throws IllegalArgumentException, - IllegalStateException, IOException { - owningMediaPlayer.lock.lock(); - try { - Log.d(AMP_TAG, "setDataSource(" + path + ")"); - mp.setDataSource(path); - } - finally { - owningMediaPlayer.lock.unlock(); - } - } - - @Override - public void setEnableSpeedAdjustment(boolean enableSpeedAdjustment) { - // Can't! - } - - @Override - public void setLooping(boolean loop) { - owningMediaPlayer.lock.lock(); - try { - mp.setLooping(loop); - } - finally { - owningMediaPlayer.lock.unlock(); - } - } - - @Override - public void setPitchStepsAdjustment(float pitchSteps) { - // Can't! - } - - @Override - public void setPlaybackPitch(float f) { - // Can't! - } - - @Override - public void setPlaybackSpeed(float f) { - // Can't! - Log.d(AMP_TAG, "setPlaybackSpeed(" + f + ")"); - } - - @Override - public void setSpeedAdjustmentAlgorithm(int algorithm) { - // Can't! - Log.d(AMP_TAG, "setSpeedAdjustmentAlgorithm(" + algorithm + ")"); - } - - @Override - public void setVolume(float leftVolume, float rightVolume) { - owningMediaPlayer.lock.lock(); - try { - mp.setVolume(leftVolume, rightVolume); - } - finally { - owningMediaPlayer.lock.unlock(); - } - } - - @Override - public void setWakeMode(Context context, int mode) { - owningMediaPlayer.lock.lock(); - try { - if (mode != 0) { - mp.setWakeMode(context, mode); - } - } - finally { - owningMediaPlayer.lock.unlock(); - } - } - - @Override - public void start() { - owningMediaPlayer.lock.lock(); - try { - mp.start(); - } - finally { - owningMediaPlayer.lock.unlock(); - } - } - - @Override - public void stop() { - owningMediaPlayer.lock.lock(); - try { - mp.stop(); - } - finally { - owningMediaPlayer.lock.unlock(); - } - } -} diff --git a/app/src/main/java/com/aocate/media/MediaPlayer.java b/app/src/main/java/com/aocate/media/MediaPlayer.java deleted file mode 100644 index 04ecd58a9..000000000 --- a/app/src/main/java/com/aocate/media/MediaPlayer.java +++ /dev/null @@ -1,1296 +0,0 @@ -// Copyright 2011, Aocate, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.aocate.media; - -import java.io.IOException; -import java.util.List; -import java.util.concurrent.locks.ReentrantLock; - -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.ServiceConnection; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; -import android.media.AudioManager; -import android.net.Uri; -import android.os.Handler; -import android.os.IBinder; -import android.os.Message; -import android.os.Handler.Callback; -import android.util.Log; - -import de.danoeh.antennapod.BuildConfig; - -public class MediaPlayer { - public interface OnBufferingUpdateListener { - public abstract void onBufferingUpdate(MediaPlayer arg0, int percent); - } - - public interface OnCompletionListener { - public abstract void onCompletion(MediaPlayer arg0); - } - - public interface OnErrorListener { - public abstract boolean onError(MediaPlayer arg0, int what, int extra); - } - - public interface OnInfoListener { - public abstract boolean onInfo(MediaPlayer arg0, int what, int extra); - } - - public interface OnPitchAdjustmentAvailableChangedListener { - /** - * - * @param arg0 - * The owning media player - * @param pitchAdjustmentAvailable - * True if pitch adjustment is available, false if not - */ - public abstract void onPitchAdjustmentAvailableChanged( - MediaPlayer arg0, boolean pitchAdjustmentAvailable); - } - - public interface OnPreparedListener { - public abstract void onPrepared(MediaPlayer arg0); - } - - public interface OnSeekCompleteListener { - public abstract void onSeekComplete(MediaPlayer arg0); - } - - public interface OnSpeedAdjustmentAvailableChangedListener { - /** - * - * @param arg0 - * The owning media player - * @param speedAdjustmentAvailable - * True if speed adjustment is available, false if not - */ - public abstract void onSpeedAdjustmentAvailableChanged( - MediaPlayer arg0, boolean speedAdjustmentAvailable); - } - - public enum State { - IDLE, INITIALIZED, PREPARED, STARTED, PAUSED, STOPPED, PREPARING, PLAYBACK_COMPLETED, END, ERROR - } - - private static Uri SPEED_ADJUSTMENT_MARKET_URI = Uri - .parse("market://details?id=com.aocate.presto"); - - private static Intent prestoMarketIntent = null; - - public static final int MEDIA_ERROR_SERVER_DIED = android.media.MediaPlayer.MEDIA_ERROR_SERVER_DIED; - public static final int MEDIA_ERROR_UNKNOWN = android.media.MediaPlayer.MEDIA_ERROR_UNKNOWN; - public static final int MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK = android.media.MediaPlayer.MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK; - - /** - * Indicates whether the specified action can be used as an intent. This - * method queries the package manager for installed packages that can - * respond to an intent with the specified action. If no suitable package is - * found, this method returns false. - * - * @param context - * The application's environment. - * @param action - * The Intent action to check for availability. - * - * @return True if an Intent with the specified action can be sent and - * responded to, false otherwise. - */ - public static boolean isIntentAvailable(Context context, String action) { - final PackageManager packageManager = context.getPackageManager(); - final Intent intent = new Intent(action); - List list = packageManager.queryIntentServices(intent, - PackageManager.MATCH_DEFAULT_ONLY); - return list.size() > 0; - } - - /** - * Indicates whether the Presto library is installed - * - * @param context - * The context to use to query the package manager. - * @return True if the Presto library is installed, false if not. - */ - public static boolean isPrestoLibraryInstalled(Context context) { - return isIntentAvailable(context, ServiceBackedMediaPlayer.INTENT_NAME); - } - - /** - * Return an Intent that opens the Android Market page for the speed - * alteration library - * - * @return The Intent for the Presto library on the Android Market - */ - public static Intent getPrestoMarketIntent() { - if (prestoMarketIntent == null) { - prestoMarketIntent = new Intent(Intent.ACTION_VIEW, - SPEED_ADJUSTMENT_MARKET_URI); - } - return prestoMarketIntent; - } - - /** - * Open the Android Market page for the Presto library - * - * @param context - * The context from which to open the Android Market page - */ - public static void openPrestoMarketIntent(Context context) { - context.startActivity(getPrestoMarketIntent()); - } - - private static final String MP_TAG = "AocateReplacementMediaPlayer"; - - private static final double PITCH_STEP_CONSTANT = 1.0594630943593; - - private AndroidMediaPlayer amp = null; - // This is whether speed adjustment should be enabled (by the Service) - // To avoid the Service entirely, set useService to false - protected boolean enableSpeedAdjustment = true; - private int lastKnownPosition = 0; - // In some cases, we're going to have to replace the - // android.media.MediaPlayer on the fly, and we don't want to touch the - // wrong media player, so lock it way too much. - ReentrantLock lock = new ReentrantLock(); - private int mAudioStreamType = AudioManager.STREAM_MUSIC; - private Context mContext; - private boolean mIsLooping = false; - private float mLeftVolume = 1f; - private float mPitchStepsAdjustment = 0f; - private float mRightVolume = 1f; - private float mSpeedMultiplier = 1f; - private int mWakeMode = 0; - MediaPlayerImpl mpi = null; - protected boolean pitchAdjustmentAvailable = false; - private ServiceBackedMediaPlayer sbmp = null; - protected boolean speedAdjustmentAvailable = false; - - private Handler mServiceDisconnectedHandler = null; - - // Some parts of state cannot be found by calling MediaPlayerImpl functions, - // so store our own state. This also helps copy state when changing - // implementations - State state = State.INITIALIZED; - String stringDataSource = null; - Uri uriDataSource = null; - private boolean useService = false; - - // Naming Convention for Listeners - // Most listeners can both be set by clients and called by MediaPlayImpls - // There are a few that have to do things in this class as well as calling - // the function. In all cases, onX is what is called by MediaPlayerImpl - // If there is work to be done in this class, then the listener that is - // set by setX is X (with the first letter lowercase). - OnBufferingUpdateListener onBufferingUpdateListener = null; - OnCompletionListener onCompletionListener = null; - OnErrorListener onErrorListener = null; - OnInfoListener onInfoListener = null; - - // Special case. Pitch adjustment ceases to be available when we switch - // to the android.media.MediaPlayer (though it is not guaranteed to be - // available when using the ServiceBackedMediaPlayer) - OnPitchAdjustmentAvailableChangedListener onPitchAdjustmentAvailableChangedListener = new OnPitchAdjustmentAvailableChangedListener() { - public void onPitchAdjustmentAvailableChanged(MediaPlayer arg0, - boolean pitchAdjustmentAvailable) { - lock.lock(); - try { - Log - .d( - MP_TAG, - "onPitchAdjustmentAvailableChangedListener.onPitchAdjustmentAvailableChanged being called"); - if (MediaPlayer.this.pitchAdjustmentAvailable != pitchAdjustmentAvailable) { - Log.d(MP_TAG, "Pitch adjustment state has changed from " - + MediaPlayer.this.pitchAdjustmentAvailable - + " to " + pitchAdjustmentAvailable); - MediaPlayer.this.pitchAdjustmentAvailable = pitchAdjustmentAvailable; - if (MediaPlayer.this.pitchAdjustmentAvailableChangedListener != null) { - MediaPlayer.this.pitchAdjustmentAvailableChangedListener - .onPitchAdjustmentAvailableChanged(arg0, - pitchAdjustmentAvailable); - } - } - } finally { - lock.unlock(); - } - } - }; - OnPitchAdjustmentAvailableChangedListener pitchAdjustmentAvailableChangedListener = null; - - MediaPlayer.OnPreparedListener onPreparedListener = new MediaPlayer.OnPreparedListener() { - public void onPrepared(MediaPlayer arg0) { - Log.d(MP_TAG, "onPreparedListener 242 setting state to PREPARED"); - MediaPlayer.this.state = State.PREPARED; - if (MediaPlayer.this.preparedListener != null) { - Log.d(MP_TAG, "Calling preparedListener"); - MediaPlayer.this.preparedListener.onPrepared(arg0); - } - Log.d(MP_TAG, "Wrap up onPreparedListener"); - } - }; - - OnPreparedListener preparedListener = null; - OnSeekCompleteListener onSeekCompleteListener = null; - - // Special case. Speed adjustment ceases to be available when we switch - // to the android.media.MediaPlayer (though it is not guaranteed to be - // available when using the ServiceBackedMediaPlayer) - OnSpeedAdjustmentAvailableChangedListener onSpeedAdjustmentAvailableChangedListener = new OnSpeedAdjustmentAvailableChangedListener() { - public void onSpeedAdjustmentAvailableChanged(MediaPlayer arg0, - boolean speedAdjustmentAvailable) { - lock.lock(); - try { - Log - .d( - MP_TAG, - "onSpeedAdjustmentAvailableChangedListener.onSpeedAdjustmentAvailableChanged being called"); - if (MediaPlayer.this.speedAdjustmentAvailable != speedAdjustmentAvailable) { - Log.d(MP_TAG, "Speed adjustment state has changed from " - + MediaPlayer.this.speedAdjustmentAvailable - + " to " + speedAdjustmentAvailable); - MediaPlayer.this.speedAdjustmentAvailable = speedAdjustmentAvailable; - if (MediaPlayer.this.speedAdjustmentAvailableChangedListener != null) { - MediaPlayer.this.speedAdjustmentAvailableChangedListener - .onSpeedAdjustmentAvailableChanged(arg0, - speedAdjustmentAvailable); - } - } - } finally { - lock.unlock(); - } - } - }; - OnSpeedAdjustmentAvailableChangedListener speedAdjustmentAvailableChangedListener = null; - - private int speedAdjustmentAlgorithm = SpeedAdjustmentAlgorithm.SONIC; - - public MediaPlayer(final Context context) { - this(context, true); - } - - public MediaPlayer(final Context context, boolean useService) { - this.mContext = context; - this.useService = useService; - - // So here's the major problem - // Sometimes the service won't exist or won't be connected, - // so start with an android.media.MediaPlayer, and when - // the service is connected, use that from then on - this.mpi = this.amp = new AndroidMediaPlayer(this, context); - - // setupMpi will go get the Service, if it can, then bring that - // implementation into sync - Log.d(MP_TAG, "setupMpi"); - setupMpi(context); - } - - private boolean invalidServiceConnectionConfiguration() { - if (!(this.mpi instanceof ServiceBackedMediaPlayer)) { - if (this.useService && isPrestoLibraryInstalled()) { - // In this case, the Presto library has been installed - // or something while playing sound - // We could be using the service, but we're not - Log.d(MP_TAG, "We could be using the service, but we're not 316"); - return true; - } - // If useService is false, then we shouldn't be using the SBMP - // If the Presto library isn't installed, ditto - Log.d(MP_TAG, "this.mpi is not a ServiceBackedMediaPlayer, but we couldn't use it anyway 321"); - return false; - } else { - if (BuildConfig.DEBUG && !(this.mpi instanceof ServiceBackedMediaPlayer)) throw new AssertionError(); - if (this.useService && isPrestoLibraryInstalled()) { - // We should be using the service, and we are. Great! - Log.d(MP_TAG, "We could be using a ServiceBackedMediaPlayer and we are 327"); - return false; - } - // We're trying to use the service when we shouldn't, - // that's an invalid configuration - Log.d(MP_TAG, "We're trying to use a ServiceBackedMediaPlayer but we shouldn't be 332"); - return true; - } - } - - private void setupMpi(final Context context) { - lock.lock(); - try { - Log.d(MP_TAG, "setupMpi 336"); - // Check if the client wants to use the service at all, - // then if we're already using the right kind of media player - if (this.useService && isPrestoLibraryInstalled()) { - if ((this.mpi != null) - && (this.mpi instanceof ServiceBackedMediaPlayer)) { - Log.d(MP_TAG, "Already using ServiceBackedMediaPlayer"); - return; - } - if (this.sbmp == null) { - Log.d(MP_TAG, "Instantiating new ServiceBackedMediaPlayer 346"); - this.sbmp = new ServiceBackedMediaPlayer(this, context, - new ServiceConnection() { - public void onServiceConnected( - ComponentName className, - final IBinder service) { - Thread t = new Thread(new Runnable() { - public void run() { - // This lock probably isn't granular - // enough - MediaPlayer.this.lock.lock(); - Log.d(MP_TAG, - "onServiceConnected 257"); - try { - MediaPlayer.this - .switchMediaPlayerImpl( - MediaPlayer.this.amp, - MediaPlayer.this.sbmp); - Log.d(MP_TAG, "End onServiceConnected 362"); - } finally { - MediaPlayer.this.lock.unlock(); - } - } - }); - t.start(); - } - - public void onServiceDisconnected( - ComponentName className) { - MediaPlayer.this.lock.lock(); - try { - // Can't get any more useful information - // out of sbmp - if (MediaPlayer.this.sbmp != null) { - MediaPlayer.this.sbmp.release(); - } - // Unlike most other cases, sbmp gets set - // to null since there's nothing useful - // backing it now - MediaPlayer.this.sbmp = null; - - if (mServiceDisconnectedHandler == null) { - mServiceDisconnectedHandler = new Handler(new Callback() { - public boolean handleMessage(Message msg) { - // switchMediaPlayerImpl won't try to - // clone anything from null - lock.lock(); - try { - if (MediaPlayer.this.amp == null) { - // This should never be in this state - MediaPlayer.this.amp = new AndroidMediaPlayer( - MediaPlayer.this, - MediaPlayer.this.mContext); - } - // Use sbmp instead of null in case by some miracle it's - // been restored in the meantime - MediaPlayer.this.switchMediaPlayerImpl( - MediaPlayer.this.sbmp, - MediaPlayer.this.amp); - return true; - } - finally { - lock.unlock(); - } - } - }); - } - - // This code needs to execute on the - // original thread to instantiate - // the new object in the right place - mServiceDisconnectedHandler - .sendMessage( - mServiceDisconnectedHandler - .obtainMessage()); - // Note that we do NOT want to set - // useService. useService is about - // what the user wants, not what they - // get - } finally { - MediaPlayer.this.lock.unlock(); - } - } - } - ); - } - switchMediaPlayerImpl(this.amp, this.sbmp); - } else { - if ((this.mpi != null) - && (this.mpi instanceof AndroidMediaPlayer)) { - Log.d(MP_TAG, "Already using AndroidMediaPlayer"); - return; - } - if (this.amp == null) { - Log.d(MP_TAG, "Instantiating new AndroidMediaPlayer (this should be impossible)"); - this.amp = new AndroidMediaPlayer(this, context); - } - switchMediaPlayerImpl(this.sbmp, this.amp); - } - } finally { - lock.unlock(); - } - } - - private void switchMediaPlayerImpl(MediaPlayerImpl from, MediaPlayerImpl to) { - lock.lock(); - try { - Log.d(MP_TAG, "switchMediaPlayerImpl"); - if ((from == to) - // Same object, nothing to synchronize - || (to == null) - // Nothing to copy to (maybe this should throw an error?) - || ((to instanceof ServiceBackedMediaPlayer) && !((ServiceBackedMediaPlayer) to).isConnected()) - // ServiceBackedMediaPlayer hasn't yet connected, onServiceConnected will take care of the transition - || (MediaPlayer.this.state == State.END)) { - // State.END is after a release(), no further functions should - // be called on this class and from is likely to have problems - // retrieving state that won't be used anyway - return; - } - // Extract all that we can from the existing implementation - // and copy it to the new implementation - - Log.d(MP_TAG, "switchMediaPlayerImpl(), current state is " - + this.state.toString()); - - to.reset(); - - // Do this first so we don't have to prepare the same - // data file twice - to.setEnableSpeedAdjustment(MediaPlayer.this.enableSpeedAdjustment); - - // This is a reasonable place to set all of these, - // none of them require prepare() or the like first - to.setAudioStreamType(this.mAudioStreamType); - to.setSpeedAdjustmentAlgorithm(this.speedAdjustmentAlgorithm); - to.setLooping(this.mIsLooping); - to.setPitchStepsAdjustment(this.mPitchStepsAdjustment); - Log.d(MP_TAG, "Setting playback speed to " + this.mSpeedMultiplier); - to.setPlaybackSpeed(this.mSpeedMultiplier); - to.setVolume(MediaPlayer.this.mLeftVolume, - MediaPlayer.this.mRightVolume); - to.setWakeMode(this.mContext, this.mWakeMode); - - Log.d(MP_TAG, "asserting at least one data source is null"); - assert ((MediaPlayer.this.stringDataSource == null) || (MediaPlayer.this.uriDataSource == null)); - - if (uriDataSource != null) { - Log.d(MP_TAG, "switchMediaPlayerImpl(): uriDataSource != null"); - try { - to.setDataSource(this.mContext, uriDataSource); - } catch (IllegalArgumentException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } catch (IllegalStateException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } catch (IOException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - } - if (stringDataSource != null) { - Log.d(MP_TAG, - "switchMediaPlayerImpl(): stringDataSource != null"); - try { - to.setDataSource(stringDataSource); - } catch (IllegalArgumentException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } catch (IllegalStateException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } catch (IOException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - } - if ((this.state == State.PREPARED) - || (this.state == State.PREPARING) - || (this.state == State.PAUSED) - || (this.state == State.STOPPED) - || (this.state == State.STARTED) - || (this.state == State.PLAYBACK_COMPLETED)) { - Log.d(MP_TAG, "switchMediaPlayerImpl(): prepare and seek"); - // Use prepare here instead of prepareAsync so that - // we wait for it to be ready before we try to use it - try { - to.muteNextOnPrepare(); - to.prepare(); - } catch (IllegalStateException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } catch (IOException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - - int seekPos = 0; - if (from != null) { - seekPos = from.getCurrentPosition(); - } else if (this.lastKnownPosition < to.getDuration()) { - // This can happen if the Service unexpectedly - // disconnected. Because it would result in too much - // information being passed around, we don't constantly - // poll for the lastKnownPosition, but we'll save it - // when getCurrentPosition is called - seekPos = this.lastKnownPosition; - } - to.muteNextSeek(); - to.seekTo(seekPos); - } - if ((from != null) - && from.isPlaying()) { - from.pause(); - } - if ((this.state == State.STARTED) - || (this.state == State.PAUSED) - || (this.state == State.STOPPED)) { - Log.d(MP_TAG, "switchMediaPlayerImpl(): start"); - if (to != null) { - to.start(); - } - } - - if (this.state == State.PAUSED) { - Log.d(MP_TAG, "switchMediaPlayerImpl(): paused"); - if (to != null) { - to.pause(); - } - } else if (this.state == State.STOPPED) { - Log.d(MP_TAG, "switchMediaPlayerImpl(): stopped"); - if (to != null) { - to.stop(); - } - } - - this.mpi = to; - - // Cheating here by relying on the side effect in - // on(Pitch|Speed)AdjustmentAvailableChanged - if ((to.canSetPitch() != this.pitchAdjustmentAvailable) - && (this.onPitchAdjustmentAvailableChangedListener != null)) { - this.onPitchAdjustmentAvailableChangedListener - .onPitchAdjustmentAvailableChanged(this, to - .canSetPitch()); - } - if ((to.canSetSpeed() != this.speedAdjustmentAvailable) - && (this.onSpeedAdjustmentAvailableChangedListener != null)) { - this.onSpeedAdjustmentAvailableChangedListener - .onSpeedAdjustmentAvailableChanged(this, to - .canSetSpeed()); - } - Log.d(MP_TAG, "switchMediaPlayerImpl() 625 " + this.state.toString()); - } finally { - lock.unlock(); - } - } - - /** - * Returns true if pitch can be changed at this moment - * - * @return True if pitch can be changed - */ - public boolean canSetPitch() { - lock.lock(); - try { - return this.mpi.canSetPitch(); - } finally { - lock.unlock(); - } - } - - /** - * Returns true if speed can be changed at this moment - * - * @return True if speed can be changed - */ - public boolean canSetSpeed() { - lock.lock(); - try { - return this.mpi.canSetSpeed(); - } finally { - lock.unlock(); - } - } - - protected void finalize() throws Throwable { - lock.lock(); - try { - Log.d(MP_TAG, "finalize() 626"); - this.release(); - } finally { - lock.unlock(); - } - } - - /** - * Returns the number of steps (in a musical scale) by which playback is - * currently shifted. When greater than zero, pitch is shifted up. When less - * than zero, pitch is shifted down. - * - * @return The number of steps pitch is currently shifted by - */ - public float getCurrentPitchStepsAdjustment() { - lock.lock(); - try { - return this.mpi.getCurrentPitchStepsAdjustment(); - } finally { - lock.unlock(); - } - } - - /** - * Functions identically to android.media.MediaPlayer.getCurrentPosition() - * Accurate only to frame size of encoded data (26 ms for MP3s) - * - * @return Current position (in milliseconds) - */ - public int getCurrentPosition() { - lock.lock(); - try { - return (this.lastKnownPosition = this.mpi.getCurrentPosition()); - } finally { - lock.unlock(); - } - } - - /** - * Returns the current speed multiplier. Defaults to 1.0 (normal speed) - * - * @return The current speed multiplier - */ - public float getCurrentSpeedMultiplier() { - lock.lock(); - try { - return this.mpi.getCurrentSpeedMultiplier(); - } finally { - lock.unlock(); - } - } - - /** - * Functions identically to android.media.MediaPlayer.getDuration() - * - * @return Length of the track (in milliseconds) - */ - public int getDuration() { - lock.lock(); - try { - return this.mpi.getDuration(); - } finally { - lock.unlock(); - } - } - - /** - * Get the maximum value that can be passed to setPlaybackSpeed - * - * @return The maximum speed multiplier - */ - public float getMaxSpeedMultiplier() { - lock.lock(); - try { - return this.mpi.getMaxSpeedMultiplier(); - } finally { - lock.unlock(); - } - } - - /** - * Get the minimum value that can be passed to setPlaybackSpeed - * - * @return The minimum speed multiplier - */ - public float getMinSpeedMultiplier() { - lock.lock(); - try { - return this.mpi.getMinSpeedMultiplier(); - } finally { - lock.unlock(); - } - } - - /** - * Gets the version code of the backing service - * @return -1 if ServiceBackedMediaPlayer is not used, 0 if the service is not - * connected, otherwise the version code retrieved from the service - */ - public int getServiceVersionCode() { - lock.lock(); - try { - if (this.mpi instanceof ServiceBackedMediaPlayer) { - return ((ServiceBackedMediaPlayer) this.mpi).getServiceVersionCode(); - } - else { - return -1; - } - } - finally { - lock.unlock(); - } - } - - /** - * Gets the version name of the backing service - * @return null if ServiceBackedMediaPlayer is not used, empty string if - * the service is not connected, otherwise the version name retrieved from - * the service - */ - public String getServiceVersionName() { - lock.lock(); - try { - if (this.mpi instanceof ServiceBackedMediaPlayer) { - return ((ServiceBackedMediaPlayer) this.mpi).getServiceVersionName(); - } - else { - return null; - } - } - finally { - lock.unlock(); - } - } - - /** - * Functions identically to android.media.MediaPlayer.isLooping() - * - * @return True if the track is looping - */ - public boolean isLooping() { - lock.lock(); - try { - return this.mpi.isLooping(); - } finally { - lock.unlock(); - } - } - - /** - * Functions identically to android.media.MediaPlayer.isPlaying() - * - * @return True if the track is playing - */ - public boolean isPlaying() { - lock.lock(); - try { - return this.mpi.isPlaying(); - } finally { - lock.unlock(); - } - } - - /** - * Returns true if this MediaPlayer has access to the Presto - * library - * - * @return True if the Presto library is installed - */ - public boolean isPrestoLibraryInstalled() { - if ((this.mpi == null) || (this.mpi.mContext == null)) { - return false; - } - return isPrestoLibraryInstalled(this.mpi.mContext); - } - - /** - * Open the Android Market page in the same context as this MediaPlayer - */ - public void openPrestoMarketIntent() { - if ((this.mpi != null) && (this.mpi.mContext != null)) { - openPrestoMarketIntent(this.mpi.mContext); - } - } - - /** - * Functions identically to android.media.MediaPlayer.pause() Pauses the - * track - */ - public void pause() { - lock.lock(); - try { - if (invalidServiceConnectionConfiguration()) { - setupMpi(this.mpi.mContext); - } - this.state = State.PAUSED; - this.mpi.pause(); - } finally { - lock.unlock(); - } - } - - /** - * Functions identically to android.media.MediaPlayer.prepare() Prepares the - * track. This or prepareAsync must be called before start() - */ - public void prepare() throws IllegalStateException, IOException { - lock.lock(); - try { - Log.d(MP_TAG, "prepare() 746 using " + ((this.mpi == null) ? "null (this shouldn't happen)" : this.mpi.getClass().toString()) + " state " + this.state.toString()); - Log.d(MP_TAG, "onPreparedListener is: " + ((this.onPreparedListener == null) ? "null" : "non-null")); - Log.d(MP_TAG, "preparedListener is: " + ((this.preparedListener == null) ? "null" : "non-null")); - if (invalidServiceConnectionConfiguration()) { - setupMpi(this.mpi.mContext); - } - this.mpi.prepare(); - this.state = State.PREPARED; - Log.d(MP_TAG, "prepare() finished 778"); - } finally { - lock.unlock(); - } - } - - /** - * Functions identically to android.media.MediaPlayer.prepareAsync() - * Prepares the track. This or prepare must be called before start() - */ - public void prepareAsync() { - lock.lock(); - try { - Log.d(MP_TAG, "prepareAsync() 779"); - if (invalidServiceConnectionConfiguration()) { - setupMpi(this.mpi.mContext); - } - this.state = State.PREPARING; - this.mpi.prepareAsync(); - } finally { - lock.unlock(); - } - } - - /** - * Functions identically to android.media.MediaPlayer.release() Releases the - * underlying resources used by the media player. - */ - public void release() { - lock.lock(); - try { - Log.d(MP_TAG, "Releasing MediaPlayer 791"); - - this.state = State.END; - if (this.amp != null) { - this.amp.release(); - } - if (this.sbmp != null) { - this.sbmp.release(); - } - - this.onBufferingUpdateListener = null; - this.onCompletionListener = null; - this.onErrorListener = null; - this.onInfoListener = null; - this.preparedListener = null; - this.onPitchAdjustmentAvailableChangedListener = null; - this.pitchAdjustmentAvailableChangedListener = null; - Log.d(MP_TAG, "Setting onSeekCompleteListener to null 871"); - this.onSeekCompleteListener = null; - this.onSpeedAdjustmentAvailableChangedListener = null; - this.speedAdjustmentAvailableChangedListener = null; - } finally { - lock.unlock(); - } - } - - /** - * Functions identically to android.media.MediaPlayer.reset() Resets the - * track to idle state - */ - public void reset() { - lock.lock(); - try { - this.state = State.IDLE; - this.stringDataSource = null; - this.uriDataSource = null; - this.mpi.reset(); - } finally { - lock.unlock(); - } - } - - /** - * Functions identically to android.media.MediaPlayer.seekTo(int msec) Seeks - * to msec in the track - */ - public void seekTo(int msec) throws IllegalStateException { - lock.lock(); - try { - this.mpi.seekTo(msec); - } finally { - lock.unlock(); - } - } - - /** - * Functions identically to android.media.MediaPlayer.setAudioStreamType(int - * streamtype) Sets the audio stream type. - */ - public void setAudioStreamType(int streamtype) { - lock.lock(); - try { - this.mAudioStreamType = streamtype; - this.mpi.setAudioStreamType(streamtype); - } finally { - lock.unlock(); - } - } - - /** - * Functions identically to android.media.MediaPlayer.setDataSource(Context - * context, Uri uri) Sets uri as data source in the context given - */ - public void setDataSource(Context context, Uri uri) - throws IllegalArgumentException, IllegalStateException, IOException { - lock.lock(); - try { - Log.d(MP_TAG, "In setDataSource(context, " + uri.toString() + "), using " + this.mpi.getClass().toString()); - if (invalidServiceConnectionConfiguration()) { - setupMpi(this.mpi.mContext); - } - this.state = State.INITIALIZED; - this.stringDataSource = null; - this.uriDataSource = uri; - this.mpi.setDataSource(context, uri); - } finally { - lock.unlock(); - } - } - - /** - * Functions identically to android.media.MediaPlayer.setDataSource(String - * path) Sets the data source of the track to a file given. - */ - public void setDataSource(String path) throws IllegalArgumentException, - IllegalStateException, IOException { - lock.lock(); - try { - Log.d(MP_TAG, "In setDataSource(context, " + path + ")"); - if (invalidServiceConnectionConfiguration()) { - setupMpi(this.mpi.mContext); - } - this.state = State.INITIALIZED; - this.stringDataSource = path; - this.uriDataSource = null; - this.mpi.setDataSource(path); - } finally { - lock.unlock(); - } - } - - /** - * Sets whether to use speed adjustment or not. Speed adjustment on is more - * computation-intensive than with it off. - * - * @param enableSpeedAdjustment - * Whether speed adjustment should be supported. - */ - public void setEnableSpeedAdjustment(boolean enableSpeedAdjustment) { - lock.lock(); - try { - this.enableSpeedAdjustment = enableSpeedAdjustment; - this.mpi.setEnableSpeedAdjustment(enableSpeedAdjustment); - } finally { - lock.unlock(); - } - } - - /** - * Functions identically to android.media.MediaPlayer.setLooping(boolean - * loop) Sets the track to loop infinitely if loop is true, play once if - * loop is false - */ - public void setLooping(boolean loop) { - lock.lock(); - try { - this.mIsLooping = loop; - this.mpi.setLooping(loop); - } finally { - lock.unlock(); - } - } - - /** - * Sets the number of steps (in a musical scale) by which playback is - * currently shifted. When greater than zero, pitch is shifted up. When less - * than zero, pitch is shifted down. - * - * @param pitchSteps - * The number of steps by which to shift playback - */ - public void setPitchStepsAdjustment(float pitchSteps) { - lock.lock(); - try { - this.mPitchStepsAdjustment = pitchSteps; - this.mpi.setPitchStepsAdjustment(pitchSteps); - } finally { - lock.unlock(); - } - } - - /** - * Set the algorithm to use for changing the speed and pitch of audio - * See SpeedAdjustmentAlgorithm constants for more details - * @param algorithm The algorithm to use. - */ - public void setSpeedAdjustmentAlgorithm(int algorithm) { - lock.lock(); - try { - this.speedAdjustmentAlgorithm = algorithm; - if (this.mpi != null) { - this.mpi.setSpeedAdjustmentAlgorithm(algorithm); - } - } - finally { - lock.unlock(); - } - } - - private static float getPitchStepsAdjustment(float pitch) { - return (float) (Math.log(pitch) / (2 * Math.log(PITCH_STEP_CONSTANT))); - } - - /** - * Sets the percentage by which pitch is currently shifted. When greater - * than zero, pitch is shifted up. When less than zero, pitch is shifted - * down - * - * @param f - * The percentage to shift pitch - */ - public void setPlaybackPitch(float pitch) { - lock.lock(); - try { - this.mPitchStepsAdjustment = getPitchStepsAdjustment(pitch); - this.mpi.setPlaybackPitch(pitch); - } finally { - lock.unlock(); - } - } - - /** - * Set playback speed. 1.0 is normal speed, 2.0 is double speed, and so on. - * Speed should never be set to 0 or below. - * - * @param f - * The speed multiplier to use for further playback - */ - public void setPlaybackSpeed(float f) { - lock.lock(); - try { - this.mSpeedMultiplier = f; - this.mpi.setPlaybackSpeed(f); - } finally { - lock.unlock(); - } - } - - /** - * Sets whether to use speed adjustment or not. Speed adjustment on is more - * computation-intensive than with it off. - * - * @param enableSpeedAdjustment - * Whether speed adjustment should be supported. - */ - public void setUseService(boolean useService) { - lock.lock(); - try { - this.useService = useService; - setupMpi(this.mpi.mContext); - } finally { - lock.unlock(); - } - } - - /** - * Functions identically to android.media.MediaPlayer.setVolume(float - * leftVolume, float rightVolume) Sets the stereo volume - */ - public void setVolume(float leftVolume, float rightVolume) { - lock.lock(); - try { - this.mLeftVolume = leftVolume; - this.mRightVolume = rightVolume; - this.mpi.setVolume(leftVolume, rightVolume); - } finally { - lock.unlock(); - } - } - - /** - * Functions identically to android.media.MediaPlayer.setWakeMode(Context - * context, int mode) Acquires a wake lock in the context given. You must - * request the appropriate permissions in your AndroidManifest.xml file. - */ - public void setWakeMode(Context context, int mode) { - lock.lock(); - try { - this.mWakeMode = mode; - this.mpi.setWakeMode(context, mode); - } finally { - lock.unlock(); - } - } - - /** - * Functions identically to - * android.media.MediaPlayer.setOnCompletionListener(OnCompletionListener - * listener) Sets a listener to be used when a track completes playing. - */ - public void setOnBufferingUpdateListener(OnBufferingUpdateListener listener) { - lock.lock(); - try { - this.onBufferingUpdateListener = listener; - } finally { - lock.unlock(); - } - } - - /** - * Functions identically to - * android.media.MediaPlayer.setOnCompletionListener(OnCompletionListener - * listener) Sets a listener to be used when a track completes playing. - */ - public void setOnCompletionListener(OnCompletionListener listener) { - lock.lock(); - try { - this.onCompletionListener = listener; - } finally { - lock.unlock(); - } - } - - /** - * Functions identically to - * android.media.MediaPlayer.setOnErrorListener(OnErrorListener listener) - * Sets a listener to be used when a track encounters an error. - */ - public void setOnErrorListener(OnErrorListener listener) { - lock.lock(); - try { - this.onErrorListener = listener; - } finally { - lock.unlock(); - } - } - - /** - * Functions identically to - * android.media.MediaPlayer.setOnInfoListener(OnInfoListener listener) Sets - * a listener to be used when a track has info. - */ - public void setOnInfoListener(OnInfoListener listener) { - lock.lock(); - try { - this.onInfoListener = listener; - } finally { - lock.unlock(); - } - } - - /** - * Sets a listener that will fire when pitch adjustment becomes available or - * stops being available - */ - public void setOnPitchAdjustmentAvailableChangedListener( - OnPitchAdjustmentAvailableChangedListener listener) { - lock.lock(); - try { - this.pitchAdjustmentAvailableChangedListener = listener; - } finally { - lock.unlock(); - } - } - - /** - * Functions identically to - * android.media.MediaPlayer.setOnPreparedListener(OnPreparedListener - * listener) Sets a listener to be used when a track finishes preparing. - */ - public void setOnPreparedListener(OnPreparedListener listener) { - lock.lock(); - Log.d(MP_TAG, " ++++++++++++++++++++++++++++++++++++++++++++ setOnPreparedListener"); - try { - this.preparedListener = listener; - // For this one, we do not explicitly set the MediaPlayer or the - // Service listener. This is because in addition to calling the - // listener provided by the client, it's necessary to change - // state to PREPARED. See prepareAsync for implementation details - } finally { - lock.unlock(); - } - } - - /** - * Functions identically to - * android.media.MediaPlayer.setOnSeekCompleteListener - * (OnSeekCompleteListener listener) Sets a listener to be used when a track - * finishes seeking. - */ - public void setOnSeekCompleteListener(OnSeekCompleteListener listener) { - lock.lock(); - try { - this.onSeekCompleteListener = listener; - } finally { - lock.unlock(); - } - } - - /** - * Sets a listener that will fire when speed adjustment becomes available or - * stops being available - */ - public void setOnSpeedAdjustmentAvailableChangedListener( - OnSpeedAdjustmentAvailableChangedListener listener) { - lock.lock(); - try { - this.speedAdjustmentAvailableChangedListener = listener; - } finally { - lock.unlock(); - } - } - - /** - * Functions identically to android.media.MediaPlayer.start() Starts a track - * playing - */ - public void start() { - lock.lock(); - try { - Log.d(MP_TAG, "start() 1149"); - if (invalidServiceConnectionConfiguration()) { - setupMpi(this.mpi.mContext); - } - this.state = State.STARTED; - Log.d(MP_TAG, "start() 1154"); - this.mpi.start(); - } finally { - lock.unlock(); - } - } - - /** - * Functions identically to android.media.MediaPlayer.stop() Stops a track - * playing and resets its position to the start. - */ - public void stop() { - lock.lock(); - try { - if (invalidServiceConnectionConfiguration()) { - setupMpi(this.mpi.mContext); - } - this.state = State.STOPPED; - this.mpi.stop(); - } finally { - lock.unlock(); - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/aocate/media/MediaPlayerImpl.java b/app/src/main/java/com/aocate/media/MediaPlayerImpl.java deleted file mode 100644 index 856ab47ce..000000000 --- a/app/src/main/java/com/aocate/media/MediaPlayerImpl.java +++ /dev/null @@ -1,118 +0,0 @@ -// Copyright 2011, Aocate, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.aocate.media; - -import java.io.IOException; -import java.util.concurrent.locks.ReentrantLock; - -import android.content.Context; -import android.net.Uri; -import android.util.Log; - -public abstract class MediaPlayerImpl { - private static final String MPI_TAG = "AocateMediaPlayerImpl"; - protected final MediaPlayer owningMediaPlayer; - protected final Context mContext; - protected int muteOnPreparedCount = 0; - protected int muteOnSeekCount = 0; - - public MediaPlayerImpl(MediaPlayer owningMediaPlayer, Context context) { - this.owningMediaPlayer = owningMediaPlayer; - - this.mContext = context; - } - - public abstract boolean canSetPitch(); - - public abstract boolean canSetSpeed(); - - public abstract float getCurrentPitchStepsAdjustment(); - - public abstract int getCurrentPosition(); - - public abstract float getCurrentSpeedMultiplier(); - - public abstract int getDuration(); - - public abstract float getMaxSpeedMultiplier(); - - public abstract float getMinSpeedMultiplier(); - - public abstract boolean isLooping(); - - public abstract boolean isPlaying(); - - public abstract void pause(); - - public abstract void prepare() throws IllegalStateException, IOException; - - public abstract void prepareAsync(); - - public abstract void release(); - - public abstract void reset(); - - public abstract void seekTo(int msec) throws IllegalStateException; - - public abstract void setAudioStreamType(int streamtype); - - public abstract void setDataSource(Context context, Uri uri) throws IllegalArgumentException, IllegalStateException, IOException; - - public abstract void setDataSource(String path) throws IllegalArgumentException, IllegalStateException, IOException; - - public abstract void setEnableSpeedAdjustment(boolean enableSpeedAdjustment); - - public abstract void setLooping(boolean loop); - - public abstract void setPitchStepsAdjustment(float pitchSteps); - - public abstract void setPlaybackPitch(float f); - - public abstract void setPlaybackSpeed(float f); - - public abstract void setSpeedAdjustmentAlgorithm(int algorithm); - - public abstract void setVolume(float leftVolume, float rightVolume); - - public abstract void setWakeMode(Context context, int mode); - - public abstract void start(); - - public abstract void stop(); - - protected ReentrantLock lockMuteOnPreparedCount = new ReentrantLock(); - public void muteNextOnPrepare() { - lockMuteOnPreparedCount.lock(); - Log.d(MPI_TAG, "muteNextOnPrepare()"); - try { - this.muteOnPreparedCount++; - } - finally { - lockMuteOnPreparedCount.unlock(); - } - } - - protected ReentrantLock lockMuteOnSeekCount = new ReentrantLock(); - public void muteNextSeek() { - lockMuteOnSeekCount.lock(); - Log.d(MPI_TAG, "muteNextOnSeek()"); - try { - this.muteOnSeekCount++; - } - finally { - lockMuteOnSeekCount.unlock(); - } - } -} diff --git a/app/src/main/java/com/aocate/media/ServiceBackedMediaPlayer.java b/app/src/main/java/com/aocate/media/ServiceBackedMediaPlayer.java deleted file mode 100644 index ef4572d33..000000000 --- a/app/src/main/java/com/aocate/media/ServiceBackedMediaPlayer.java +++ /dev/null @@ -1,1170 +0,0 @@ -// Copyright 2011, Aocate, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.aocate.media; - -import java.io.IOException; - -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.ServiceConnection; -import android.media.AudioManager; -import android.net.Uri; -import android.os.IBinder; -import android.os.PowerManager; -import android.os.RemoteException; -import android.os.PowerManager.WakeLock; -import android.util.Log; - -import com.aocate.media.MediaPlayer.State; -import com.aocate.presto.service.IDeathCallback_0_8; -import com.aocate.presto.service.IOnBufferingUpdateListenerCallback_0_8; -import com.aocate.presto.service.IOnCompletionListenerCallback_0_8; -import com.aocate.presto.service.IOnErrorListenerCallback_0_8; -import com.aocate.presto.service.IOnInfoListenerCallback_0_8; -import com.aocate.presto.service.IOnPitchAdjustmentAvailableChangedListenerCallback_0_8; -import com.aocate.presto.service.IOnPreparedListenerCallback_0_8; -import com.aocate.presto.service.IOnSeekCompleteListenerCallback_0_8; -import com.aocate.presto.service.IOnSpeedAdjustmentAvailableChangedListenerCallback_0_8; -import com.aocate.presto.service.IPlayMedia_0_8; - -/** - * Class for connecting to remote speed-altering, media playing Service - * Note that there is unusually high coupling between MediaPlayer and this - * class. This is an unfortunate compromise, since the alternative was to - * track state in two different places in this code (plus the internal state - * of the remote media player). - * @author aocate - * - */ -public class ServiceBackedMediaPlayer extends MediaPlayerImpl { - static final String INTENT_NAME = "com.aocate.intent.PLAY_AUDIO_ADJUST_SPEED_0_8"; - - private static final String SBMP_TAG = "AocateServiceBackedMediaPlayer"; - - private ServiceConnection mPlayMediaServiceConnection = null; - protected IPlayMedia_0_8 pmInterface = null; - private Intent playMediaServiceIntent = null; - // In some cases, we're going to have to replace the - // android.media.MediaPlayer on the fly, and we don't want to touch the - // wrong media player. - - private long sessionId = 0; - private boolean isErroring = false; - private int mAudioStreamType = AudioManager.STREAM_MUSIC; - - private WakeLock mWakeLock = null; - - // So here's the major problem - // Sometimes the service won't exist or won't be connected, - // so start with an android.media.MediaPlayer, and when - // the service is connected, use that from then on - public ServiceBackedMediaPlayer(MediaPlayer owningMediaPlayer, final Context context, final ServiceConnection serviceConnection) { - super(owningMediaPlayer, context); - Log.d(SBMP_TAG, "Instantiating ServiceBackedMediaPlayer 87"); - this.playMediaServiceIntent = - new Intent(INTENT_NAME); - this.mPlayMediaServiceConnection = new ServiceConnection() { - public void onServiceConnected(ComponentName name, IBinder service) { - IPlayMedia_0_8 tmpPlayMediaInterface = IPlayMedia_0_8.Stub.asInterface((IBinder) service); - - Log.d(SBMP_TAG, "Setting up pmInterface 94"); - if (ServiceBackedMediaPlayer.this.sessionId == 0) { - try { - // The IDeathCallback isn't a conventional callback. - // It exists so that if the client ceases to exist, - // the Service becomes aware of that and can shut - // down whatever it needs to shut down - ServiceBackedMediaPlayer.this.sessionId = tmpPlayMediaInterface.startSession(new IDeathCallback_0_8.Stub() { - }); - // This is really bad if this fails - } catch (RemoteException e) { - e.printStackTrace(); - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - - Log.d(SBMP_TAG, "Assigning pmInterface"); - - ServiceBackedMediaPlayer.this.setOnBufferingUpdateCallback(tmpPlayMediaInterface); - ServiceBackedMediaPlayer.this.setOnCompletionCallback(tmpPlayMediaInterface); - ServiceBackedMediaPlayer.this.setOnErrorCallback(tmpPlayMediaInterface); - ServiceBackedMediaPlayer.this.setOnInfoCallback(tmpPlayMediaInterface); - ServiceBackedMediaPlayer.this.setOnPitchAdjustmentAvailableChangedListener(tmpPlayMediaInterface); - ServiceBackedMediaPlayer.this.setOnPreparedCallback(tmpPlayMediaInterface); - ServiceBackedMediaPlayer.this.setOnSeekCompleteCallback(tmpPlayMediaInterface); - ServiceBackedMediaPlayer.this.setOnSpeedAdjustmentAvailableChangedCallback(tmpPlayMediaInterface); - - // In order to avoid race conditions from the sessionId or listener not being assigned - pmInterface = tmpPlayMediaInterface; - - Log.d(SBMP_TAG, "Invoking onServiceConnected"); - serviceConnection.onServiceConnected(name, service); - } - - public void onServiceDisconnected(ComponentName name) { - Log.d(SBMP_TAG, "onServiceDisconnected 114"); - - pmInterface = null; - - sessionId = 0; - - serviceConnection.onServiceDisconnected(name); - } - }; - - Log.d(SBMP_TAG, "Connecting PlayMediaService 124"); - if (!ConnectPlayMediaService()) { - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - - private boolean ConnectPlayMediaService() { - Log.d(SBMP_TAG, "ConnectPlayMediaService()"); - - if (MediaPlayer.isIntentAvailable(mContext, INTENT_NAME)) { - Log.d(SBMP_TAG, INTENT_NAME + " is available"); - if (pmInterface == null) { - try { - Log.d(SBMP_TAG, "Binding service"); - return mContext.bindService(playMediaServiceIntent, mPlayMediaServiceConnection, Context.BIND_AUTO_CREATE); - } catch (Exception e) { - return false; - } - } else { - Log.d(SBMP_TAG, "Service already bound"); - return true; - } - } - else { - Log.d(SBMP_TAG, INTENT_NAME + " is not available"); - return false; - } - } - - /** - * Returns true if pitch can be changed at this moment - * @return True if pitch can be changed - */ - @Override - public boolean canSetPitch() { - Log.d(SBMP_TAG, "canSetPitch() 155"); - - if (pmInterface == null) { - if (!ConnectPlayMediaService()) { - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - if (pmInterface != null) { - // Can't set pitch if the service isn't connected - try { - return pmInterface.canSetPitch(ServiceBackedMediaPlayer.this.sessionId); - } catch (RemoteException e) { - e.printStackTrace(); - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - return false; - } - - /** - * Returns true if speed can be changed at this moment - * @return True if speed can be changed - */ - @Override - public boolean canSetSpeed() { - Log.d(SBMP_TAG, "canSetSpeed() 180"); - if (pmInterface == null) { - if (!ConnectPlayMediaService()) { - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - if (pmInterface != null) { - // Can't set speed if the service isn't connected - try { - return pmInterface.canSetSpeed(ServiceBackedMediaPlayer.this.sessionId); - } catch (RemoteException e) { - e.printStackTrace(); - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - return false; - } - - void error(int what, int extra) { - owningMediaPlayer.lock.lock(); - Log.e(SBMP_TAG, "error(" + what + ", " + extra + ")"); - try { - if (!this.isErroring) { - this.isErroring = true; - owningMediaPlayer.state = State.ERROR; - if (owningMediaPlayer.onErrorListener != null) { - if (owningMediaPlayer.onErrorListener.onError(owningMediaPlayer, what, extra)) { - return; - } - } - if (owningMediaPlayer.onCompletionListener != null) { - owningMediaPlayer.onCompletionListener.onCompletion(owningMediaPlayer); - } - } - } - finally { - this.isErroring = false; - owningMediaPlayer.lock.unlock(); - } - } - - protected void finalize() throws Throwable { - owningMediaPlayer.lock.lock(); - try { - Log.d(SBMP_TAG, "finalize() 224"); - this.release(); - } - finally { - owningMediaPlayer.lock.unlock(); - } - } - - /** - * Returns the number of steps (in a musical scale) by which playback is - * currently shifted. When greater than zero, pitch is shifted up. - * When less than zero, pitch is shifted down. - * @return The number of steps pitch is currently shifted by - */ - @Override - public float getCurrentPitchStepsAdjustment() { - Log.d(SBMP_TAG, "getCurrentPitchStepsAdjustment() 240"); - if (pmInterface == null) { - if (!ConnectPlayMediaService()) { - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - if (pmInterface != null) { - // Can't set pitch if the service isn't connected - try { - return pmInterface.getCurrentPitchStepsAdjustment( - ServiceBackedMediaPlayer.this.sessionId); - } catch (RemoteException e) { - e.printStackTrace(); - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - return 0f; - } - - /** - * Functions identically to android.media.MediaPlayer.getCurrentPosition() - * @return Current position (in milliseconds) - */ - @Override - public int getCurrentPosition() { - if (pmInterface == null) { - if (!ConnectPlayMediaService()) { - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - try { - return pmInterface.getCurrentPosition( - ServiceBackedMediaPlayer.this.sessionId); - } catch (RemoteException e) { - e.printStackTrace(); - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - return 0; - } - - /** - * Returns the current speed multiplier. Defaults to 1.0 (normal speed) - * @return The current speed multiplier - */ - @Override - public float getCurrentSpeedMultiplier() { - Log.d(SBMP_TAG, "getCurrentSpeedMultiplier() 286"); - if (pmInterface == null) { - if (!ConnectPlayMediaService()) { - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - if (pmInterface != null) { - // Can't set speed if the service isn't connected - try { - return pmInterface.getCurrentSpeedMultiplier( - ServiceBackedMediaPlayer.this.sessionId); - } catch (RemoteException e) { - e.printStackTrace(); - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - return 1; - } - - /** - * Functions identically to android.media.MediaPlayer.getDuration() - * @return Length of the track (in milliseconds) - */ - @Override - public int getDuration() { - Log.d(SBMP_TAG, "getDuration() 311"); - if (pmInterface == null) { - if (!ConnectPlayMediaService()) { - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - try { - return pmInterface.getDuration(ServiceBackedMediaPlayer.this.sessionId); - } catch (RemoteException e) { - e.printStackTrace(); - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - return 0; - } - - /** - * Get the maximum value that can be passed to setPlaybackSpeed - * @return The maximum speed multiplier - */ - @Override - public float getMaxSpeedMultiplier() { - Log.d(SBMP_TAG, "getMaxSpeedMultiplier() 332"); - if (pmInterface == null) { - if (!ConnectPlayMediaService()) { - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - if (pmInterface != null) { - // Can't set speed if the Service isn't connected - try { - return pmInterface.getMaxSpeedMultiplier( - ServiceBackedMediaPlayer.this.sessionId); - } catch (RemoteException e) { - e.printStackTrace(); - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - return 1f; - } - - /** - * Get the minimum value that can be passed to setPlaybackSpeed - * @return The minimum speed multiplier - */ - @Override - public float getMinSpeedMultiplier() { - Log.d(SBMP_TAG, "getMinSpeedMultiplier() 357"); - if (pmInterface == null) { - if (!ConnectPlayMediaService()) { - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - if (pmInterface != null) { - // Can't set speed if the Service isn't connected - try { - return pmInterface.getMinSpeedMultiplier( - ServiceBackedMediaPlayer.this.sessionId); - } catch (RemoteException e) { - e.printStackTrace(); - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - return 1f; - } - - public int getServiceVersionCode() { - Log.d(SBMP_TAG, "getVersionCode"); - if (pmInterface == null) { - if (!ConnectPlayMediaService()) { - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - try { - return pmInterface.getVersionCode(); - } catch (RemoteException e) { - e.printStackTrace(); - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - return 0; - } - - public String getServiceVersionName() { - Log.d(SBMP_TAG, "getVersionName"); - if (pmInterface == null) { - if (!ConnectPlayMediaService()) { - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - try { - return pmInterface.getVersionName(); - } catch (RemoteException e) { - e.printStackTrace(); - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - return ""; - } - - public boolean isConnected() { - return (pmInterface != null); - } - - /** - * Functions identically to android.media.MediaPlayer.isLooping() - * @return True if the track is looping - */ - @Override - public boolean isLooping() { - Log.d(SBMP_TAG, "isLooping() 382"); - if (pmInterface == null) { - if (!ConnectPlayMediaService()) { - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - try { - return pmInterface.isLooping(ServiceBackedMediaPlayer.this.sessionId); - } catch (RemoteException e) { - e.printStackTrace(); - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - return false; - } - - /** - * Functions identically to android.media.MediaPlayer.isPlaying() - * @return True if the track is playing - */ - @Override - public boolean isPlaying() { - if (pmInterface == null) { - if (!ConnectPlayMediaService()) { - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - if (pmInterface != null) { - try { - return pmInterface.isPlaying(ServiceBackedMediaPlayer.this.sessionId); - } catch (RemoteException e) { - e.printStackTrace(); - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - return false; - } - - /** - * Functions identically to android.media.MediaPlayer.pause() - * Pauses the track - */ - @Override - public void pause() { - Log.d(SBMP_TAG, "pause() 424"); - if (pmInterface == null) { - if (!ConnectPlayMediaService()) { - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - try { - pmInterface.pause(ServiceBackedMediaPlayer.this.sessionId); - } catch (RemoteException e) { - e.printStackTrace(); - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - - /** - * Functions identically to android.media.MediaPlayer.prepare() - * Prepares the track. This or prepareAsync must be called before start() - */ - @Override - public void prepare() throws IllegalStateException, IOException { - Log.d(SBMP_TAG, "prepare() 444"); - Log.d(SBMP_TAG, "onPreparedCallback is: " + ((this.mOnPreparedCallback == null) ? "null" : "non-null")); - if (pmInterface == null) { - Log.d(SBMP_TAG, "prepare: pmInterface is null"); - if (!ConnectPlayMediaService()) { - Log.d(SBMP_TAG, "prepare: Failed to connect play media service"); - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - if (pmInterface != null) { - Log.d(SBMP_TAG, "prepare: pmInterface isn't null"); - try { - Log.d(SBMP_TAG, "prepare: Remote invoke pmInterface.prepare(" + ServiceBackedMediaPlayer.this.sessionId + ")"); - pmInterface.prepare(ServiceBackedMediaPlayer.this.sessionId); - Log.d(SBMP_TAG, "prepare: prepared"); - } catch (RemoteException e) { - Log.d(SBMP_TAG, "prepare: RemoteException"); - e.printStackTrace(); - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - Log.d(SBMP_TAG, "Done with prepare()"); - } - - /** - * Functions identically to android.media.MediaPlayer.prepareAsync() - * Prepares the track. This or prepare must be called before start() - */ - @Override - public void prepareAsync() { - Log.d(SBMP_TAG, "prepareAsync() 469"); - if (pmInterface == null) { - if (!ConnectPlayMediaService()) { - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - try { - pmInterface.prepareAsync(ServiceBackedMediaPlayer.this.sessionId); - } catch (RemoteException e) { - e.printStackTrace(); - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - - /** - * Functions identically to android.media.MediaPlayer.release() - * Releases the underlying resources used by the media player. - */ - @Override - public void release() { - Log.d(SBMP_TAG, "release() 492"); - if (pmInterface == null) { - if (!ConnectPlayMediaService()) { - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - if (pmInterface != null) { - Log.d(SBMP_TAG, "release() 500"); - try { - pmInterface.release(ServiceBackedMediaPlayer.this.sessionId); - } catch (RemoteException e) { - e.printStackTrace(); - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - mContext.unbindService(this.mPlayMediaServiceConnection); - // Don't try to keep awake (if we were) - this.setWakeMode(mContext, 0); - pmInterface = null; - this.sessionId = 0; - } - - if ((this.mWakeLock != null) && this.mWakeLock.isHeld()) { - Log.d(SBMP_TAG, "Releasing wakelock"); - this.mWakeLock.release(); - } - } - - /** - * Functions identically to android.media.MediaPlayer.reset() - * Resets the track to idle state - */ - @Override - public void reset() { - Log.d(SBMP_TAG, "reset() 523"); - if (pmInterface == null) { - if (!ConnectPlayMediaService()) { - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - try { - pmInterface.reset(ServiceBackedMediaPlayer.this.sessionId); - } catch (RemoteException e) { - e.printStackTrace(); - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - - /** - * Functions identically to android.media.MediaPlayer.seekTo(int msec) - * Seeks to msec in the track - */ - @Override - public void seekTo(int msec) throws IllegalStateException { - Log.d(SBMP_TAG, "seekTo(" + msec + ")"); - if (pmInterface == null) { - if (!ConnectPlayMediaService()) { - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - try { - pmInterface.seekTo(ServiceBackedMediaPlayer.this.sessionId, msec); - } catch (RemoteException e) { - e.printStackTrace(); - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - - /** - * Functions identically to android.media.MediaPlayer.setAudioStreamType(int streamtype) - * Sets the audio stream type. - */ - @Override - public void setAudioStreamType(int streamtype) { - Log.d(SBMP_TAG, "setAudioStreamType(" + streamtype + ")"); - if (pmInterface == null) { - if (!ConnectPlayMediaService()) { - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - try { - pmInterface.setAudioStreamType( - ServiceBackedMediaPlayer.this.sessionId, - this.mAudioStreamType); - } catch (RemoteException e) { - e.printStackTrace(); - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - - - /** - * Functions identically to android.media.MediaPlayer.setDataSource(Context context, Uri uri) - * Sets uri as data source in the context given - */ - @Override - public void setDataSource(Context context, Uri uri) throws IllegalArgumentException, IllegalStateException, IOException { - Log.d(SBMP_TAG, "setDataSource(context, uri)"); - if (pmInterface == null) { - if (!ConnectPlayMediaService()) { - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - try { - pmInterface.setDataSourceUri( - ServiceBackedMediaPlayer.this.sessionId, - uri); - } catch (RemoteException e) { - e.printStackTrace(); - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - - /** - * Functions identically to android.media.MediaPlayer.setDataSource(String path) - * Sets the data source of the track to a file given. - */ - @Override - public void setDataSource(String path) throws IllegalArgumentException, IllegalStateException, IOException { - Log.d(SBMP_TAG, "setDataSource(path)"); - if (pmInterface == null) { - if (!ConnectPlayMediaService()) { - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - if (pmInterface == null) { - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - else { - try { - pmInterface.setDataSourceString( - ServiceBackedMediaPlayer.this.sessionId, - path); - } catch (RemoteException e) { - e.printStackTrace(); - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - } - - /** - * Sets whether to use speed adjustment or not. Speed adjustment on is - * more computation-intensive than with it off. - * @param enableSpeedAdjustment Whether speed adjustment should be supported. - */ - @Override - public void setEnableSpeedAdjustment(boolean enableSpeedAdjustment) { - // TODO: This has no business being here, I think - owningMediaPlayer.lock.lock(); - Log.d(SBMP_TAG, "setEnableSpeedAdjustment(enableSpeedAdjustment)"); - try { - if (pmInterface == null) { - if (!ConnectPlayMediaService()) { - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - if (pmInterface != null) { - // Can't set speed if the Service isn't connected - try { - pmInterface.setEnableSpeedAdjustment( - ServiceBackedMediaPlayer.this.sessionId, - enableSpeedAdjustment); - } catch (RemoteException e) { - e.printStackTrace(); - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - } - finally { - owningMediaPlayer.lock.unlock(); - } - } - - - /** - * Functions identically to android.media.MediaPlayer.setLooping(boolean loop) - * Sets the track to loop infinitely if loop is true, play once if loop is false - */ - @Override - public void setLooping(boolean loop) { - Log.d(SBMP_TAG, "setLooping(" + loop + ")"); - if (pmInterface == null) { - if (!ConnectPlayMediaService()) { - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - try { - pmInterface.setLooping(ServiceBackedMediaPlayer.this.sessionId, loop); - } catch (RemoteException e) { - e.printStackTrace(); - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - - /** - * Sets the number of steps (in a musical scale) by which playback is - * currently shifted. When greater than zero, pitch is shifted up. - * When less than zero, pitch is shifted down. - * - * @param pitchSteps The number of steps by which to shift playback - */ - @Override - public void setPitchStepsAdjustment(float pitchSteps) { - Log.d(SBMP_TAG, "setPitchStepsAdjustment(" + pitchSteps + ")"); - if (pmInterface == null) { - if (!ConnectPlayMediaService()) { - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - if (pmInterface != null) { - // Can't set speed if the Service isn't connected - try { - pmInterface.setPitchStepsAdjustment( - ServiceBackedMediaPlayer.this.sessionId, - pitchSteps); - } catch (RemoteException e) { - e.printStackTrace(); - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - } - - /** - * Sets the percentage by which pitch is currently shifted. When - * greater than zero, pitch is shifted up. When less than zero, pitch - * is shifted down - * @param f The percentage to shift pitch - */ - @Override - public void setPlaybackPitch(float f) { - Log.d(SBMP_TAG, "setPlaybackPitch(" + f + ")"); - if (pmInterface == null) { - if (!ConnectPlayMediaService()) { - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - if (pmInterface != null) { - // Can't set speed if the Service isn't connected - try { - pmInterface.setPlaybackPitch( - ServiceBackedMediaPlayer.this.sessionId, - f); - } catch (RemoteException e) { - e.printStackTrace(); - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - } - - /** - * Set playback speed. 1.0 is normal speed, 2.0 is double speed, and so - * on. Speed should never be set to 0 or below. - * @param f The speed multiplier to use for further playback - */ - @Override - public void setPlaybackSpeed(float f) { - Log.d(SBMP_TAG, "setPlaybackSpeed(" + f + ")"); - if (pmInterface == null) { - if (!ConnectPlayMediaService()) { - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - if (pmInterface != null) { - // Can't set speed if the Service isn't connected - try { - pmInterface.setPlaybackSpeed( - ServiceBackedMediaPlayer.this.sessionId, - f); - } catch (RemoteException e) { - e.printStackTrace(); - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - } - - @Override - public void setSpeedAdjustmentAlgorithm(int algorithm) { - if (pmInterface == null) { - if (!ConnectPlayMediaService()) { - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - try { - pmInterface.setSpeedAdjustmentAlgorithm( - ServiceBackedMediaPlayer.this.sessionId, - algorithm); - } catch (RemoteException e) { - e.printStackTrace(); - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - - /** - * Functions identically to android.media.MediaPlayer.setVolume(float leftVolume, float rightVolume) - * Sets the stereo volume - */ - @Override - public void setVolume(float leftVolume, float rightVolume) { - Log.d(SBMP_TAG, "setVolume(" + leftVolume + ", " + rightVolume + ")"); - if (pmInterface == null) { - if (!ConnectPlayMediaService()) { - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - try { - pmInterface.setVolume( - ServiceBackedMediaPlayer.this.sessionId, - leftVolume, - rightVolume); - } catch (RemoteException e) { - e.printStackTrace(); - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - - /** - * Functions identically to android.media.MediaPlayer.setWakeMode(Context context, int mode) - * Acquires a wake lock in the context given. You must request the appropriate permissions - * in your AndroidManifest.xml file. - */ - @Override - // This does not just call .setWakeMode() in the Service because doing so - // would add a permission requirement to the Service. Do it here, and it's - // the client app's responsibility to request that permission - public void setWakeMode(Context context, int mode) { - Log.d(SBMP_TAG, "setWakeMode(context, " + mode + ")"); - if ((this.mWakeLock != null) - && (this.mWakeLock.isHeld())) { - this.mWakeLock.release(); - } - if (mode != 0) { - if (this.mWakeLock == null) { - PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE); - // Since mode can't be changed on the fly, we have to allocate a new one - this.mWakeLock = pm.newWakeLock(mode, this.getClass().getName()); - } - - this.mWakeLock.acquire(); - } - } - - private IOnBufferingUpdateListenerCallback_0_8.Stub mOnBufferingUpdateCallback = null; - private void setOnBufferingUpdateCallback(IPlayMedia_0_8 iface) { - try { - if (this.mOnBufferingUpdateCallback == null) { - mOnBufferingUpdateCallback = new IOnBufferingUpdateListenerCallback_0_8.Stub() { - public void onBufferingUpdate(int percent) - throws RemoteException { - owningMediaPlayer.lock.lock(); - try { - if ((owningMediaPlayer.onBufferingUpdateListener != null) - && (owningMediaPlayer.mpi == ServiceBackedMediaPlayer.this)) { - owningMediaPlayer.onBufferingUpdateListener.onBufferingUpdate(owningMediaPlayer, percent); - } - } - finally { - owningMediaPlayer.lock.unlock(); - } - } - }; - } - iface.registerOnBufferingUpdateCallback( - ServiceBackedMediaPlayer.this.sessionId, - mOnBufferingUpdateCallback); - } catch (RemoteException e) { - e.printStackTrace(); - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - - private IOnCompletionListenerCallback_0_8.Stub mOnCompletionCallback = null; - private void setOnCompletionCallback(IPlayMedia_0_8 iface) { - try { - if (this.mOnCompletionCallback == null) { - this.mOnCompletionCallback = new IOnCompletionListenerCallback_0_8.Stub() { - public void onCompletion() throws RemoteException { - owningMediaPlayer.lock.lock(); - Log.d(SBMP_TAG, "onCompletionListener being called"); - try { - if (owningMediaPlayer.onCompletionListener != null) { - owningMediaPlayer.onCompletionListener.onCompletion(owningMediaPlayer); - } - } - finally { - owningMediaPlayer.lock.unlock(); - } - } - }; - } - iface.registerOnCompletionCallback( - ServiceBackedMediaPlayer.this.sessionId, - this.mOnCompletionCallback); - } catch (RemoteException e) { - e.printStackTrace(); - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - - private IOnErrorListenerCallback_0_8.Stub mOnErrorCallback = null; - private void setOnErrorCallback(IPlayMedia_0_8 iface) { - try { - if (this.mOnErrorCallback == null) { - this.mOnErrorCallback = new IOnErrorListenerCallback_0_8.Stub() { - public boolean onError(int what, int extra) throws RemoteException { - owningMediaPlayer.lock.lock(); - try { - if (owningMediaPlayer.onErrorListener != null) { - return owningMediaPlayer.onErrorListener.onError(owningMediaPlayer, what, extra); - } - return false; - } - finally { - owningMediaPlayer.lock.unlock(); - } - } - }; - } - iface.registerOnErrorCallback( - ServiceBackedMediaPlayer.this.sessionId, - this.mOnErrorCallback); - } catch (RemoteException e) { - e.printStackTrace(); - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - - private IOnInfoListenerCallback_0_8.Stub mOnInfoCallback = null; - private void setOnInfoCallback(IPlayMedia_0_8 iface) { - try { - if (this.mOnInfoCallback == null) { - this.mOnInfoCallback = new IOnInfoListenerCallback_0_8.Stub() { - public boolean onInfo(int what, int extra) throws RemoteException { - owningMediaPlayer.lock.lock(); - try { - if ((owningMediaPlayer.onInfoListener != null) - && (owningMediaPlayer.mpi == ServiceBackedMediaPlayer.this)) { - return owningMediaPlayer.onInfoListener.onInfo(owningMediaPlayer, what, extra); - } - } - finally { - owningMediaPlayer.lock.unlock(); - } - return false; - } - }; - } - iface.registerOnInfoCallback( - ServiceBackedMediaPlayer.this.sessionId, - this.mOnInfoCallback); - } catch (RemoteException e) { - e.printStackTrace(); - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - - private IOnPitchAdjustmentAvailableChangedListenerCallback_0_8.Stub mOnPitchAdjustmentAvailableChangedCallback = null; - private void setOnPitchAdjustmentAvailableChangedListener(IPlayMedia_0_8 iface) { - try { - if (this.mOnPitchAdjustmentAvailableChangedCallback == null) { - this.mOnPitchAdjustmentAvailableChangedCallback = new IOnPitchAdjustmentAvailableChangedListenerCallback_0_8.Stub() { - public void onPitchAdjustmentAvailableChanged( - boolean pitchAdjustmentAvailable) - throws RemoteException { - owningMediaPlayer.lock.lock(); - try { - if (owningMediaPlayer.onPitchAdjustmentAvailableChangedListener != null) { - owningMediaPlayer.onPitchAdjustmentAvailableChangedListener.onPitchAdjustmentAvailableChanged(owningMediaPlayer, pitchAdjustmentAvailable); - } - } - finally { - owningMediaPlayer.lock.unlock(); - } - } - }; - } - iface.registerOnPitchAdjustmentAvailableChangedCallback( - ServiceBackedMediaPlayer.this.sessionId, - this.mOnPitchAdjustmentAvailableChangedCallback); - } catch (RemoteException e) { - e.printStackTrace(); - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - - private IOnPreparedListenerCallback_0_8.Stub mOnPreparedCallback = null; - private void setOnPreparedCallback(IPlayMedia_0_8 iface) { - try { - if (this.mOnPreparedCallback == null) { - this.mOnPreparedCallback = new IOnPreparedListenerCallback_0_8.Stub() { - public void onPrepared() throws RemoteException { - owningMediaPlayer.lock.lock(); - Log.d(SBMP_TAG, "setOnPreparedCallback.mOnPreparedCallback.onPrepared 1050"); - try { - Log.d(SBMP_TAG, "owningMediaPlayer.onPreparedListener is " + ((owningMediaPlayer.onPreparedListener == null) ? "null" : "non-null")); - Log.d(SBMP_TAG, "owningMediaPlayer.mpi is " + ((owningMediaPlayer.mpi == ServiceBackedMediaPlayer.this) ? "this" : "not this")); - ServiceBackedMediaPlayer.this.lockMuteOnPreparedCount.lock(); - try { - if (ServiceBackedMediaPlayer.this.muteOnPreparedCount > 0) { - ServiceBackedMediaPlayer.this.muteOnPreparedCount--; - } - else { - ServiceBackedMediaPlayer.this.muteOnPreparedCount = 0; - if (ServiceBackedMediaPlayer.this.owningMediaPlayer.onPreparedListener != null) { - owningMediaPlayer.onPreparedListener.onPrepared(owningMediaPlayer); - } - } - } - finally { - ServiceBackedMediaPlayer.this.lockMuteOnPreparedCount.unlock(); - } - } - finally { - owningMediaPlayer.lock.unlock(); - } - } - }; - } - iface.registerOnPreparedCallback( - ServiceBackedMediaPlayer.this.sessionId, - this.mOnPreparedCallback); - } catch (RemoteException e) { - e.printStackTrace(); - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - - private IOnSeekCompleteListenerCallback_0_8.Stub mOnSeekCompleteCallback = null; - private void setOnSeekCompleteCallback(IPlayMedia_0_8 iface) { - try { - if (this.mOnSeekCompleteCallback == null) { - this.mOnSeekCompleteCallback = new IOnSeekCompleteListenerCallback_0_8.Stub() { - public void onSeekComplete() throws RemoteException { - Log.d(SBMP_TAG, "onSeekComplete() 941"); - owningMediaPlayer.lock.lock(); - try { - if (ServiceBackedMediaPlayer.this.muteOnSeekCount > 0) { - Log.d(SBMP_TAG, "The next " + ServiceBackedMediaPlayer.this.muteOnSeekCount + " seek events are muted (counting this one)"); - ServiceBackedMediaPlayer.this.muteOnSeekCount--; - } - else { - ServiceBackedMediaPlayer.this.muteOnSeekCount = 0; - Log.d(SBMP_TAG, "Attempting to invoke next seek event"); - if (ServiceBackedMediaPlayer.this.owningMediaPlayer.onSeekCompleteListener != null) { - Log.d(SBMP_TAG, "Invoking onSeekComplete"); - owningMediaPlayer.onSeekCompleteListener.onSeekComplete(owningMediaPlayer); - } - } - } - finally { - owningMediaPlayer.lock.unlock(); - } - } - }; - } - iface.registerOnSeekCompleteCallback( - ServiceBackedMediaPlayer.this.sessionId, - this.mOnSeekCompleteCallback); - } catch (RemoteException e) { - e.printStackTrace(); - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - - private IOnSpeedAdjustmentAvailableChangedListenerCallback_0_8.Stub mOnSpeedAdjustmentAvailableChangedCallback = null; - private void setOnSpeedAdjustmentAvailableChangedCallback(IPlayMedia_0_8 iface) { - try { - Log.d(SBMP_TAG, "Setting the service of on speed adjustment available changed"); - if (this.mOnSpeedAdjustmentAvailableChangedCallback == null) { - this.mOnSpeedAdjustmentAvailableChangedCallback = new IOnSpeedAdjustmentAvailableChangedListenerCallback_0_8.Stub() { - public void onSpeedAdjustmentAvailableChanged( - boolean speedAdjustmentAvailable) - throws RemoteException { - owningMediaPlayer.lock.lock(); - try { - if (owningMediaPlayer.onSpeedAdjustmentAvailableChangedListener != null) { - owningMediaPlayer.onSpeedAdjustmentAvailableChangedListener.onSpeedAdjustmentAvailableChanged(owningMediaPlayer, speedAdjustmentAvailable); - } - } - finally { - owningMediaPlayer.lock.unlock(); - } - } - }; - } - iface.registerOnSpeedAdjustmentAvailableChangedCallback( - ServiceBackedMediaPlayer.this.sessionId, - this.mOnSpeedAdjustmentAvailableChangedCallback); - } catch (RemoteException e) { - e.printStackTrace(); - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - - /** - * Functions identically to android.media.MediaPlayer.start() - * Starts a track playing - */ - @Override - public void start() { - Log.d(SBMP_TAG, "start()"); - if (pmInterface == null) { - if (!ConnectPlayMediaService()) { - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - try { - pmInterface.start(ServiceBackedMediaPlayer.this.sessionId); - } catch (RemoteException e) { - e.printStackTrace(); - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - - /** - * Functions identically to android.media.MediaPlayer.stop() - * Stops a track playing and resets its position to the start. - */ - @Override - public void stop() { - Log.d(SBMP_TAG, "stop()"); - if (pmInterface == null) { - if (!ConnectPlayMediaService()) { - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - try { - pmInterface.stop(ServiceBackedMediaPlayer.this.sessionId); - } catch (RemoteException e) { - e.printStackTrace(); - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/aocate/media/SpeedAdjustmentAlgorithm.java b/app/src/main/java/com/aocate/media/SpeedAdjustmentAlgorithm.java deleted file mode 100644 index d337a0452..000000000 --- a/app/src/main/java/com/aocate/media/SpeedAdjustmentAlgorithm.java +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright 2011, Aocate, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.aocate.media; - -public class SpeedAdjustmentAlgorithm { - /** - * Use this to use the user-specified algorithm - */ - public static int DEFAULT = 0; - - /** - * Better for voice audio - */ - public static int SONIC = 1; - /** - * Better for music audio - */ - public static int WSOLA = 2; -} diff --git a/app/src/main/java/de/danoeh/antennapod/PodcastApp.java b/app/src/main/java/de/danoeh/antennapod/PodcastApp.java index 25aa9fb68..12213d6f9 100644 --- a/app/src/main/java/de/danoeh/antennapod/PodcastApp.java +++ b/app/src/main/java/de/danoeh/antennapod/PodcastApp.java @@ -12,7 +12,6 @@ import de.danoeh.antennapod.spa.SPAUtil; public class PodcastApp extends Application { private static final String TAG = "PodcastApp"; - public static final String EXPORT_DIR = "export/"; private static float LOGICAL_DENSITY; 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 5622bc987..f9001adad 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/AudioplayerActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/AudioplayerActivity.java @@ -43,8 +43,8 @@ import de.danoeh.antennapod.fragment.ItemDescriptionFragment; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.service.playback.PlaybackService; import de.danoeh.antennapod.core.storage.DBReader; -import de.danoeh.antennapod.core.util.menuhandler.MenuItemUtils; -import de.danoeh.antennapod.core.util.menuhandler.NavDrawerActivity; +import de.danoeh.antennapod.menuhandler.MenuItemUtils; +import de.danoeh.antennapod.menuhandler.NavDrawerActivity; import de.danoeh.antennapod.core.util.playback.ExternalMedia; import de.danoeh.antennapod.core.util.playback.Playable; import de.danoeh.antennapod.core.util.playback.PlaybackController; 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 80484df37..3000cfaeb 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/FeedInfoActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/FeedInfoActivity.java @@ -21,7 +21,7 @@ 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; +import de.danoeh.antennapod.menuhandler.FeedMenuHandler; /** * Displays information about a feed. 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 2c660019c..7029fd32c 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java @@ -31,7 +31,7 @@ import de.danoeh.antennapod.fragment.*; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.util.StorageUtils; -import de.danoeh.antennapod.core.util.menuhandler.NavDrawerActivity; +import de.danoeh.antennapod.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 249a3c5c3..14cb2727f 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/MediaplayerActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/MediaplayerActivity.java @@ -22,7 +22,7 @@ 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.core.dialog.TimeDialog; +import de.danoeh.antennapod.dialog.TimeDialog; import de.danoeh.antennapod.core.feed.FeedItem; import de.danoeh.antennapod.core.feed.FeedMedia; import de.danoeh.antennapod.core.preferences.UserPreferences; 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 2e66978fd..d974e0e1b 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/OpmlImportBaseActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/OpmlImportBaseActivity.java @@ -4,8 +4,8 @@ import android.content.Intent; import android.support.v7.app.ActionBarActivity; import android.util.Log; import de.danoeh.antennapod.BuildConfig; -import de.danoeh.antennapod.core.asynctask.OpmlFeedQueuer; -import de.danoeh.antennapod.core.asynctask.OpmlImportWorker; +import de.danoeh.antennapod.asynctask.OpmlFeedQueuer; +import de.danoeh.antennapod.asynctask.OpmlImportWorker; import de.danoeh.antennapod.core.opml.OpmlElement; import java.io.Reader; 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 65efcc230..484550a6a 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/PreferenceActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/PreferenceActivity.java @@ -22,7 +22,7 @@ import android.widget.Toast; import de.danoeh.antennapod.BuildConfig; import de.danoeh.antennapod.R; import de.danoeh.antennapod.core.asynctask.FlattrClickWorker; -import de.danoeh.antennapod.core.asynctask.OpmlExportWorker; +import de.danoeh.antennapod.asynctask.OpmlExportWorker; import de.danoeh.antennapod.dialog.AuthenticationDialog; import de.danoeh.antennapod.dialog.AutoFlattrPreferenceDialog; import de.danoeh.antennapod.dialog.GpodnetSetHostnameDialog; diff --git a/app/src/main/java/de/danoeh/antennapod/asynctask/OpmlExportWorker.java b/app/src/main/java/de/danoeh/antennapod/asynctask/OpmlExportWorker.java new file mode 100644 index 000000000..6bba956a6 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/asynctask/OpmlExportWorker.java @@ -0,0 +1,118 @@ +package de.danoeh.antennapod.asynctask; + +import android.annotation.SuppressLint; +import android.app.AlertDialog; +import android.app.ProgressDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.os.AsyncTask; +import android.util.Log; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; + +import de.danoeh.antennapod.core.R; +import de.danoeh.antennapod.core.opml.OpmlWriter; +import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.core.util.LangUtils; + +/** + * Writes an OPML file into the export directory in the background. + */ +public class OpmlExportWorker extends AsyncTask { + private static final String TAG = "OpmlExportWorker"; + private static final String DEFAULT_OUTPUT_NAME = "antennapod-feeds.opml"; + public static final String EXPORT_DIR = "export/"; + + private Context context; + private File output; + + private ProgressDialog progDialog; + private Exception exception; + + public OpmlExportWorker(Context context, File output) { + this.context = context; + this.output = output; + } + + public OpmlExportWorker(Context context) { + this.context = context; + } + + @Override + protected Void doInBackground(Void... params) { + OpmlWriter opmlWriter = new OpmlWriter(); + if (output == null) { + output = new File( + UserPreferences.getDataFolder(context, EXPORT_DIR), + DEFAULT_OUTPUT_NAME); + if (output.exists()) { + Log.w(TAG, "Overwriting previously exported file."); + output.delete(); + } + } + OutputStreamWriter writer = null; + try { + writer = new OutputStreamWriter(new FileOutputStream(output), LangUtils.UTF_8); + opmlWriter.writeDocument(DBReader.getFeedList(context), writer); + } catch (IOException e) { + e.printStackTrace(); + exception = e; + } finally { + if (writer != null) { + try { + writer.close(); + } catch (IOException ioe) { + exception = ioe; + } + } + } + return null; + } + + @Override + protected void onPostExecute(Void result) { + progDialog.dismiss(); + AlertDialog.Builder alert = new AlertDialog.Builder(context) + .setNeutralButton(android.R.string.ok, + new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, + int which) { + dialog.dismiss(); + } + }); + if (exception != null) { + alert.setTitle(R.string.export_error_label); + alert.setMessage(exception.getMessage()); + } else { + alert.setTitle(R.string.opml_export_success_title); + alert.setMessage(context + .getString(R.string.opml_export_success_sum) + + output.toString()); + } + alert.create().show(); + } + + @Override + protected void onPreExecute() { + progDialog = new ProgressDialog(context); + progDialog.setMessage(context.getString(R.string.exporting_label)); + progDialog.setIndeterminate(true); + progDialog.show(); + } + + @SuppressLint("NewApi") + public void executeAsync() { + if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.GINGERBREAD_MR1) { + executeOnExecutor(THREAD_POOL_EXECUTOR); + } else { + execute(); + } + } + +} diff --git a/app/src/main/java/de/danoeh/antennapod/asynctask/OpmlFeedQueuer.java b/app/src/main/java/de/danoeh/antennapod/asynctask/OpmlFeedQueuer.java new file mode 100644 index 000000000..cb9197b8e --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/asynctask/OpmlFeedQueuer.java @@ -0,0 +1,69 @@ +package de.danoeh.antennapod.asynctask; + +import android.annotation.SuppressLint; +import android.app.ProgressDialog; +import android.content.Context; +import android.os.AsyncTask; +import de.danoeh.antennapod.core.R; +import de.danoeh.antennapod.activity.OpmlImportHolder; +import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.core.opml.OpmlElement; +import de.danoeh.antennapod.core.storage.DownloadRequestException; +import de.danoeh.antennapod.core.storage.DownloadRequester; + +import java.util.Arrays; +import java.util.Date; + +/** Queues items for download in the background. */ +public class OpmlFeedQueuer extends AsyncTask { + private Context context; + private ProgressDialog progDialog; + private int[] selection; + + public OpmlFeedQueuer(Context context, int[] selection) { + super(); + this.context = context; + this.selection = Arrays.copyOf(selection, selection.length); + } + + @Override + protected void onPostExecute(Void result) { + progDialog.dismiss(); + } + + @Override + protected void onPreExecute() { + progDialog = new ProgressDialog(context); + progDialog.setMessage(context.getString(R.string.processing_label)); + progDialog.setCancelable(false); + progDialog.setIndeterminate(true); + progDialog.show(); + } + + @Override + protected Void doInBackground(Void... params) { + DownloadRequester requester = DownloadRequester.getInstance(); + for (int idx = 0; idx < selection.length; idx++) { + OpmlElement element = OpmlImportHolder.getReadElements().get( + selection[idx]); + Feed feed = new Feed(element.getXmlUrl(), new Date(), + element.getText()); + try { + requester.downloadFeed(context.getApplicationContext(), feed); + } catch (DownloadRequestException e) { + e.printStackTrace(); + } + } + return null; + } + + @SuppressLint("NewApi") + public void executeAsync() { + if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.GINGERBREAD_MR1) { + executeOnExecutor(THREAD_POOL_EXECUTOR); + } else { + execute(); + } + } + +} diff --git a/app/src/main/java/de/danoeh/antennapod/asynctask/OpmlImportWorker.java b/app/src/main/java/de/danoeh/antennapod/asynctask/OpmlImportWorker.java new file mode 100644 index 000000000..cfe0703ca --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/asynctask/OpmlImportWorker.java @@ -0,0 +1,116 @@ +package de.danoeh.antennapod.asynctask; + +import android.annotation.SuppressLint; +import android.app.AlertDialog; +import android.app.ProgressDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; +import android.os.AsyncTask; +import android.util.Log; +import de.danoeh.antennapod.core.BuildConfig; +import de.danoeh.antennapod.core.R; +import de.danoeh.antennapod.core.opml.OpmlElement; +import de.danoeh.antennapod.core.opml.OpmlReader; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; +import java.io.Reader; +import java.util.ArrayList; + +public class OpmlImportWorker extends + AsyncTask> { + 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/config/ApplicationCallbacksImpl.java b/app/src/main/java/de/danoeh/antennapod/config/ApplicationCallbacksImpl.java new file mode 100644 index 000000000..fdbb2139d --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/config/ApplicationCallbacksImpl.java @@ -0,0 +1,23 @@ +package de.danoeh.antennapod.config; + + +import android.app.Application; +import android.content.Context; +import android.content.Intent; + +import de.danoeh.antennapod.PodcastApp; +import de.danoeh.antennapod.activity.StorageErrorActivity; +import de.danoeh.antennapod.core.ApplicationCallbacks; + +public class ApplicationCallbacksImpl implements ApplicationCallbacks { + + @Override + public Application getApplicationInstance() { + return PodcastApp.getInstance(); + } + + @Override + public Intent getStorageErrorActivity(Context context) { + return new Intent(context, StorageErrorActivity.class); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/config/ClientConfigurator.java b/app/src/main/java/de/danoeh/antennapod/config/ClientConfigurator.java new file mode 100644 index 000000000..5dc3416c6 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/config/ClientConfigurator.java @@ -0,0 +1,19 @@ +package de.danoeh.antennapod.config; + +import de.danoeh.antennapod.core.ClientConfig; + +/** + * Configures the ClientConfig class of the core package. + */ +public class ClientConfigurator { + + static { + ClientConfig.USER_AGENT = "AntennaPod/0.9.9.3"; + ClientConfig.applicationCallbacks = new ApplicationCallbacksImpl(); + ClientConfig.downloadServiceCallbacks = new DownloadServiceCallbacksImpl(); + ClientConfig.gpodnetCallbacks = new GpodnetCallbacksImpl(); + ClientConfig.playbackServiceCallbacks = new PlaybackServiceCallbacksImpl(); + ClientConfig.storageCallbacks = new StorageCallbacksImpl(); + ClientConfig.flattrCallbacks = new FlattrCallbacksImpl(); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/config/DownloadServiceCallbacksImpl.java b/app/src/main/java/de/danoeh/antennapod/config/DownloadServiceCallbacksImpl.java new file mode 100644 index 000000000..0f180e9c5 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/config/DownloadServiceCallbacksImpl.java @@ -0,0 +1,49 @@ +package de.danoeh.antennapod.config; + +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; + +import de.danoeh.antennapod.activity.DownloadAuthenticationActivity; +import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.adapter.NavListAdapter; +import de.danoeh.antennapod.core.DownloadServiceCallbacks; +import de.danoeh.antennapod.core.service.download.DownloadRequest; +import de.danoeh.antennapod.fragment.DownloadsFragment; + + +public class DownloadServiceCallbacksImpl implements DownloadServiceCallbacks { + + @Override + public PendingIntent getNotificationContentIntent(Context context) { + Intent intent = new Intent(context, MainActivity.class); + intent.putExtra(MainActivity.EXTRA_NAV_TYPE, NavListAdapter.VIEW_TYPE_NAV); + intent.putExtra(MainActivity.EXTRA_NAV_INDEX, MainActivity.POS_DOWNLOADS); + Bundle args = new Bundle(); + args.putInt(DownloadsFragment.ARG_SELECTED_TAB, DownloadsFragment.POS_RUNNING); + intent.putExtra(MainActivity.EXTRA_FRAGMENT_ARGS, args); + + return PendingIntent.getActivity(context, 0, intent, + PendingIntent.FLAG_UPDATE_CURRENT); + } + + @Override + public PendingIntent getAuthentificationNotificationContentIntent(Context context, DownloadRequest request) { + final Intent activityIntent = new Intent(context.getApplicationContext(), DownloadAuthenticationActivity.class); + activityIntent.putExtra(DownloadAuthenticationActivity.ARG_DOWNLOAD_REQUEST, request); + activityIntent.putExtra(DownloadAuthenticationActivity.ARG_SEND_TO_DOWNLOAD_REQUESTER_BOOL, true); + return PendingIntent.getActivity(context.getApplicationContext(), 0, activityIntent, PendingIntent.FLAG_ONE_SHOT); + } + + @Override + public PendingIntent getReportNotificationContentIntent(Context context) { + Intent intent = new Intent(context, MainActivity.class); + intent.putExtra(MainActivity.EXTRA_NAV_TYPE, NavListAdapter.VIEW_TYPE_NAV); + intent.putExtra(MainActivity.EXTRA_NAV_INDEX, MainActivity.POS_DOWNLOADS); + Bundle args = new Bundle(); + args.putInt(DownloadsFragment.ARG_SELECTED_TAB, DownloadsFragment.POS_LOG); + intent.putExtra(MainActivity.EXTRA_FRAGMENT_ARGS, args); + return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/config/FlattrCallbacksImpl.java b/app/src/main/java/de/danoeh/antennapod/config/FlattrCallbacksImpl.java new file mode 100644 index 000000000..3817db6de --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/config/FlattrCallbacksImpl.java @@ -0,0 +1,53 @@ +package de.danoeh.antennapod.config; + + +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.util.Log; + +import org.shredzone.flattr4j.oauth.AccessToken; + +import de.danoeh.antennapod.BuildConfig; +import de.danoeh.antennapod.activity.FlattrAuthActivity; +import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.core.FlattrCallbacks; + +public class FlattrCallbacksImpl implements FlattrCallbacks { + private static final String TAG = "FlattrCallbacksImpl"; + + @Override + public boolean flattrEnabled() { + return true; + } + + @Override + public Intent getFlattrAuthenticationActivityIntent(Context context) { + return new Intent(context, FlattrAuthActivity.class); + } + + @Override + public PendingIntent getFlattrFailedNotificationContentIntent(Context context) { + return PendingIntent.getActivity(context, 0, new Intent(context, MainActivity.class), 0); + } + + @Override + public String getFlattrAppKey() { + return BuildConfig.FLATTR_APP_KEY; + } + + @Override + public String getFlattrAppSecret() { + return BuildConfig.FLATTR_APP_SECRET; + } + + @Override + public void handleFlattrAuthenticationSuccess(AccessToken token) { + FlattrAuthActivity instance = FlattrAuthActivity.getInstance(); + if (instance != null) { + instance.handleAuthenticationSuccess(); + } else { + Log.e(TAG, "FlattrAuthActivity instance was null"); + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/config/GpodnetCallbacksImpl.java b/app/src/main/java/de/danoeh/antennapod/config/GpodnetCallbacksImpl.java new file mode 100644 index 000000000..5f8da6894 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/config/GpodnetCallbacksImpl.java @@ -0,0 +1,22 @@ +package de.danoeh.antennapod.config; + +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; + +import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.core.GpodnetCallbacks; + + +public class GpodnetCallbacksImpl implements GpodnetCallbacks { + @Override + public boolean gpodnetEnabled() { + return true; + } + + @Override + public PendingIntent getGpodnetSyncServiceErrorNotificationPendingIntent(Context context) { + return PendingIntent.getActivity(context, 0, new Intent(context, MainActivity.class), + PendingIntent.FLAG_UPDATE_CURRENT); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/config/PlaybackServiceCallbacksImpl.java b/app/src/main/java/de/danoeh/antennapod/config/PlaybackServiceCallbacksImpl.java new file mode 100644 index 000000000..d1e3a8379 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/config/PlaybackServiceCallbacksImpl.java @@ -0,0 +1,21 @@ +package de.danoeh.antennapod.config; + +import android.content.Context; +import android.content.Intent; + +import de.danoeh.antennapod.activity.AudioplayerActivity; +import de.danoeh.antennapod.activity.VideoplayerActivity; +import de.danoeh.antennapod.core.PlaybackServiceCallbacks; +import de.danoeh.antennapod.core.feed.MediaType; + + +public class PlaybackServiceCallbacksImpl implements PlaybackServiceCallbacks { + @Override + public Intent getPlayerActivityIntent(Context context, MediaType mediaType) { + if (mediaType == MediaType.VIDEO) { + return new Intent(context, VideoplayerActivity.class); + } else { + return new Intent(context, AudioplayerActivity.class); + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/config/StorageCallbacksImpl.java b/app/src/main/java/de/danoeh/antennapod/config/StorageCallbacksImpl.java new file mode 100644 index 000000000..ec133aed1 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/config/StorageCallbacksImpl.java @@ -0,0 +1,107 @@ +package de.danoeh.antennapod.config; + + +import android.content.ContentValues; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.util.Log; + +import de.danoeh.antennapod.core.StorageCallbacks; +import de.danoeh.antennapod.core.storage.PodDBAdapter; + +public class StorageCallbacksImpl implements StorageCallbacks { + + @Override + public int getDatabaseVersion() { + return 12; + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + Log.w("DBAdapter", "Upgrading from version " + oldVersion + " to " + + newVersion + "."); + if (oldVersion <= 1) { + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS + " ADD COLUMN " + + PodDBAdapter.KEY_TYPE + " TEXT"); + } + if (oldVersion <= 2) { + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_SIMPLECHAPTERS + + " ADD COLUMN " + PodDBAdapter.KEY_LINK + " TEXT"); + } + if (oldVersion <= 3) { + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEED_ITEMS + + " ADD COLUMN " + PodDBAdapter.KEY_ITEM_IDENTIFIER + " TEXT"); + } + if (oldVersion <= 4) { + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS + " ADD COLUMN " + + PodDBAdapter.KEY_FEED_IDENTIFIER + " TEXT"); + } + if (oldVersion <= 5) { + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_DOWNLOAD_LOG + + " ADD COLUMN " + PodDBAdapter.KEY_REASON_DETAILED + " TEXT"); + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_DOWNLOAD_LOG + + " ADD COLUMN " + PodDBAdapter.KEY_DOWNLOADSTATUS_TITLE + " TEXT"); + } + if (oldVersion <= 6) { + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_SIMPLECHAPTERS + + " ADD COLUMN " + PodDBAdapter.KEY_CHAPTER_TYPE + " INTEGER"); + } + if (oldVersion <= 7) { + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEED_MEDIA + + " ADD COLUMN " + PodDBAdapter.KEY_PLAYBACK_COMPLETION_DATE + + " INTEGER"); + } + if (oldVersion <= 8) { + final int KEY_ID_POSITION = 0; + final int KEY_MEDIA_POSITION = 1; + + // Add feeditem column to feedmedia table + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEED_MEDIA + + " ADD COLUMN " + PodDBAdapter.KEY_FEEDITEM + + " INTEGER"); + Cursor feeditemCursor = db.query(PodDBAdapter.TABLE_NAME_FEED_ITEMS, + new String[]{PodDBAdapter.KEY_ID, PodDBAdapter.KEY_MEDIA}, "? > 0", + new String[]{PodDBAdapter.KEY_MEDIA}, null, null, null); + if (feeditemCursor.moveToFirst()) { + db.beginTransaction(); + ContentValues contentValues = new ContentValues(); + do { + long mediaId = feeditemCursor.getLong(KEY_MEDIA_POSITION); + contentValues.put(PodDBAdapter.KEY_FEEDITEM, feeditemCursor.getLong(KEY_ID_POSITION)); + db.update(PodDBAdapter.TABLE_NAME_FEED_MEDIA, contentValues, PodDBAdapter.KEY_ID + "=?", new String[]{String.valueOf(mediaId)}); + contentValues.clear(); + } while (feeditemCursor.moveToNext()); + db.setTransactionSuccessful(); + db.endTransaction(); + } + feeditemCursor.close(); + } + if (oldVersion <= 9) { + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS + + " ADD COLUMN " + PodDBAdapter.KEY_AUTO_DOWNLOAD + + " INTEGER DEFAULT 1"); + } + if (oldVersion <= 10) { + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS + + " ADD COLUMN " + PodDBAdapter.KEY_FLATTR_STATUS + + " INTEGER"); + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEED_ITEMS + + " ADD COLUMN " + PodDBAdapter.KEY_FLATTR_STATUS + + " INTEGER"); + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEED_MEDIA + + " ADD COLUMN " + PodDBAdapter.KEY_PLAYED_DURATION + + " INTEGER"); + } + if (oldVersion <= 11) { + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS + + " ADD COLUMN " + PodDBAdapter.KEY_USERNAME + + " TEXT"); + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS + + " ADD COLUMN " + PodDBAdapter.KEY_PASSWORD + + " TEXT"); + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEED_ITEMS + + " ADD COLUMN " + PodDBAdapter.KEY_IMAGE + + " INTEGER"); + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/core/ClientConfig.java b/app/src/main/java/de/danoeh/antennapod/core/ClientConfig.java deleted file mode 100644 index bf28c17ea..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/ClientConfig.java +++ /dev/null @@ -1,21 +0,0 @@ -package de.danoeh.antennapod.core; - -/** - * Stores callbacks for core classes like Services, DB classes etc. and other configuration variables. - * Apps using the core module of AntennaPod should register implementations of all interfaces here. - */ -public class ClientConfig { - - /** - * Should be used when setting User-Agent header for HTTP-requests. - */ - public static String USER_AGENT; - - public static DownloadServiceCallbacks downloadServiceCallbacks; - - public static PlaybackServiceCallbacks playbackServiceCallbacks; - - public static GpodnetCallbacks gpodnetCallbacks; - - public static StorageCallbacks storageCallbacks; -} diff --git a/app/src/main/java/de/danoeh/antennapod/core/DownloadServiceCallbacks.java b/app/src/main/java/de/danoeh/antennapod/core/DownloadServiceCallbacks.java deleted file mode 100644 index 9e4ed8e2b..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/DownloadServiceCallbacks.java +++ /dev/null @@ -1,43 +0,0 @@ -package de.danoeh.antennapod.core; - -import android.app.PendingIntent; - -import de.danoeh.antennapod.core.service.download.DownloadRequest; - -/** - * Callbacks for the DownloadService of the core module - */ -public interface DownloadServiceCallbacks { - - /** - * Returns a PendingIntent for a notification the main notification of the DownloadService. - *

- * The PendingIntent takes the users to a screen where they can observe all currently running - * downloads. - * - * @return A non-null PendingIntent for the notification. - */ - public PendingIntent getNotificationContentIntent(); - - /** - * Returns a PendingIntent for a notification that tells the user to enter a username - * or a password for a requested download. - *

- * The PendingIntent takes users to an Activity that lets the user enter their username - * and password to retry the download. - * - * @return A non-null PendingIntent for the notification. - */ - public PendingIntent getAuthentificationNotificationContentIntent(DownloadRequest request); - - /** - * Returns a PendingIntent for notification that notifies the user about the completion of downloads - * along with information about failed and successful downloads. - *

- * The PendingIntent takes users to an activity where they can look at all successful and failed downloads. - * - * @return A non-null PendingIntent for the notification. - */ - public PendingIntent getReportNotificationContentIntent(); -} - diff --git a/app/src/main/java/de/danoeh/antennapod/core/FlattrCallbacks.java b/app/src/main/java/de/danoeh/antennapod/core/FlattrCallbacks.java deleted file mode 100644 index 2dde4d8f3..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/FlattrCallbacks.java +++ /dev/null @@ -1,24 +0,0 @@ -package de.danoeh.antennapod.core; - -import android.content.Intent; - -/** - * Callbacks for the flattr integration of the app. - */ -public interface FlattrCallbacks { - - /** - * Returns if true if the flattr integration should be activated, - * false otherwise. - */ - public boolean flattrEnabled(); - - /** - * Returns an intent that starts the activity that is responsible for - * letting users log into their flattr account. - * - * @return The intent that starts the authentication activity or null - * if flattr integration is disabled (i.e. flattrEnabled() == false). - */ - public Intent getFlattrAuthenticationActivityIntent(); -} diff --git a/app/src/main/java/de/danoeh/antennapod/core/GpodnetCallbacks.java b/app/src/main/java/de/danoeh/antennapod/core/GpodnetCallbacks.java deleted file mode 100644 index e937bf35c..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/GpodnetCallbacks.java +++ /dev/null @@ -1,26 +0,0 @@ -package de.danoeh.antennapod.core; - -import android.app.PendingIntent; - -/** - * Callbacks related to the gpodder.net integration of the core module - */ -public interface GpodnetCallbacks { - - - /** - * Returns if true if the gpodder.net integration should be activated, - * false otherwise. - */ - public boolean gpodnetEnabled(); - - /** - * Returns a PendingIntent for the error notification of the GpodnetSyncService. - *

- * What the PendingIntent does may be implementation-specific. - * - * @return A PendingIntent for the notification or null if gpodder.net integration - * has been disabled (i.e. gpodnetEnabled() == false). - */ - public PendingIntent getGpodnetSyncServiceErrorNotificationPendingIntent(); -} diff --git a/app/src/main/java/de/danoeh/antennapod/core/PlaybackServiceCallbacks.java b/app/src/main/java/de/danoeh/antennapod/core/PlaybackServiceCallbacks.java deleted file mode 100644 index a74c441c4..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/PlaybackServiceCallbacks.java +++ /dev/null @@ -1,20 +0,0 @@ -package de.danoeh.antennapod.core; - -import android.content.Intent; - -import de.danoeh.antennapod.core.feed.MediaType; - -/** - * Callbacks for the PlaybackService of the core module - */ -public interface PlaybackServiceCallbacks { - - /** - * Returns an intent which starts an audio- or videoplayer, depending on the - * type of media that is being played. - * - * @param mediaType The type of media that is being played. - * @return A non-null activity intent. - */ - public Intent getPlayerActivityIntent(MediaType mediaType); -} diff --git a/app/src/main/java/de/danoeh/antennapod/core/StorageCallbacks.java b/app/src/main/java/de/danoeh/antennapod/core/StorageCallbacks.java deleted file mode 100644 index 5d1a0fffc..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/StorageCallbacks.java +++ /dev/null @@ -1,27 +0,0 @@ -package de.danoeh.antennapod.core; - -import android.database.sqlite.SQLiteDatabase; - -/** - * Callbacks for the classes in the storage package of the core module. - */ -public interface StorageCallbacks { - - /** - * Returns the current version of the database. - * - * @return The non-negative version number of the database. - */ - public int getDatabaseVersion(); - - /** - * Upgrades the given database from an old version to a newer version. - * - * @param db The database that is supposed to be upgraded. - * @param oldVersion The old version of the database. - * @param newVersion The version that the database is supposed to be upgraded to. - */ - public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion); - - -} diff --git a/app/src/main/java/de/danoeh/antennapod/core/asynctask/DownloadObserver.java b/app/src/main/java/de/danoeh/antennapod/core/asynctask/DownloadObserver.java deleted file mode 100644 index 8b3635af8..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/asynctask/DownloadObserver.java +++ /dev/null @@ -1,177 +0,0 @@ -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 deleted file mode 100644 index 2201dfbe7..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/asynctask/FeedRemover.java +++ /dev/null @@ -1,74 +0,0 @@ -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 deleted file mode 100644 index 44ad91981..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/asynctask/FlattrClickWorker.java +++ /dev/null @@ -1,238 +0,0 @@ -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 deleted file mode 100644 index ddc4370e6..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/asynctask/FlattrStatusFetcher.java +++ /dev/null @@ -1,47 +0,0 @@ -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 deleted file mode 100644 index 6f8319c7d..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/asynctask/FlattrTokenFetcher.java +++ /dev/null @@ -1,95 +0,0 @@ -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 deleted file mode 100644 index 9f887bda6..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/asynctask/OpmlExportWorker.java +++ /dev/null @@ -1,114 +0,0 @@ -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 deleted file mode 100644 index 13144faa9..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/asynctask/OpmlFeedQueuer.java +++ /dev/null @@ -1,69 +0,0 @@ -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 deleted file mode 100644 index a4308be9b..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/asynctask/OpmlImportWorker.java +++ /dev/null @@ -1,116 +0,0 @@ -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 deleted file mode 100644 index c0d8049db..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/asynctask/PicassoImageResource.java +++ /dev/null @@ -1,37 +0,0 @@ -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 deleted file mode 100644 index 6ace92800..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/asynctask/PicassoProvider.java +++ /dev/null @@ -1,152 +0,0 @@ -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 deleted file mode 100644 index 72b5066b3..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/backup/OpmlBackupAgent.java +++ /dev/null @@ -1,211 +0,0 @@ -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 deleted file mode 100644 index e51d70708..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/dialog/ConfirmationDialog.java +++ /dev/null @@ -1,64 +0,0 @@ -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 deleted file mode 100644 index a1c3a4c6a..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/dialog/DownloadRequestErrorDialogCreator.java +++ /dev/null @@ -1,30 +0,0 @@ -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 deleted file mode 100644 index a95e8c6c5..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/dialog/TimeDialog.java +++ /dev/null @@ -1,138 +0,0 @@ -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 deleted file mode 100644 index ce3352ed6..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/feed/Chapter.java +++ /dev/null @@ -1,55 +0,0 @@ -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 deleted file mode 100644 index 65c55a361..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/feed/EventDistributor.java +++ /dev/null @@ -1,140 +0,0 @@ -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 deleted file mode 100644 index 3f83ab8b6..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/feed/Feed.java +++ /dev/null @@ -1,445 +0,0 @@ -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 deleted file mode 100644 index 05115c1ea..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/feed/FeedComponent.java +++ /dev/null @@ -1,66 +0,0 @@ -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 deleted file mode 100644 index 3dc58654b..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/feed/FeedFile.java +++ /dev/null @@ -1,105 +0,0 @@ -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 deleted file mode 100644 index 51605691d..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/feed/FeedImage.java +++ /dev/null @@ -1,71 +0,0 @@ -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 deleted file mode 100644 index 55143b13b..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/feed/FeedItem.java +++ /dev/null @@ -1,332 +0,0 @@ -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 deleted file mode 100644 index ab87e822d..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/feed/FeedMedia.java +++ /dev/null @@ -1,408 +0,0 @@ -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 deleted file mode 100644 index 2f0304182..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/feed/FeedPreferences.java +++ /dev/null @@ -1,89 +0,0 @@ -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 deleted file mode 100644 index f0ff03a93..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/feed/ID3Chapter.java +++ /dev/null @@ -1,36 +0,0 @@ -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 deleted file mode 100644 index 7b3cb829d..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/feed/MediaType.java +++ /dev/null @@ -1,5 +0,0 @@ -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 deleted file mode 100644 index 9aa8d3170..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/feed/SearchResult.java +++ /dev/null @@ -1,34 +0,0 @@ -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 deleted file mode 100644 index 2dadd3ec8..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/feed/SimpleChapter.java +++ /dev/null @@ -1,25 +0,0 @@ -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 deleted file mode 100644 index 5b54a2d59..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/feed/VorbisCommentChapter.java +++ /dev/null @@ -1,109 +0,0 @@ -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 deleted file mode 100644 index 117cbf96b..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetService.java +++ /dev/null @@ -1,718 +0,0 @@ -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 deleted file mode 100644 index 8bd56218c..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetServiceAuthenticationException.java +++ /dev/null @@ -1,21 +0,0 @@ -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 deleted file mode 100644 index 16f01f0f4..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetServiceBadStatusCodeException.java +++ /dev/null @@ -1,12 +0,0 @@ -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 deleted file mode 100644 index ce704f7e3..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetServiceException.java +++ /dev/null @@ -1,19 +0,0 @@ -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 deleted file mode 100644 index 4885a243a..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetDevice.java +++ /dev/null @@ -1,72 +0,0 @@ -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 deleted file mode 100644 index afebf66ac..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetPodcast.java +++ /dev/null @@ -1,65 +0,0 @@ -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 deleted file mode 100644 index a5cb8c0f0..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetSubscriptionChange.java +++ /dev/null @@ -1,41 +0,0 @@ -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 deleted file mode 100644 index 7178f4be5..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetTag.java +++ /dev/null @@ -1,46 +0,0 @@ -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 deleted file mode 100644 index 5a37efa5e..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetUploadChangesResponse.java +++ /dev/null @@ -1,56 +0,0 @@ -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 deleted file mode 100644 index 8d0a4a842..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/opml/OpmlElement.java +++ /dev/null @@ -1,46 +0,0 @@ -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 deleted file mode 100644 index aa484954d..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/opml/OpmlReader.java +++ /dev/null @@ -1,87 +0,0 @@ -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 deleted file mode 100644 index 2b831ca2a..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/opml/OpmlSymbols.java +++ /dev/null @@ -1,21 +0,0 @@ -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 deleted file mode 100644 index fe14b4954..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/opml/OpmlWriter.java +++ /dev/null @@ -1,65 +0,0 @@ -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 deleted file mode 100644 index 716a74f53..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/preferences/GpodnetPreferences.java +++ /dev/null @@ -1,246 +0,0 @@ -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 deleted file mode 100644 index 756b4067c..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/preferences/PlaybackPreferences.java +++ /dev/null @@ -1,146 +0,0 @@ -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 deleted file mode 100644 index 1669fc601..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/preferences/UserPreferences.java +++ /dev/null @@ -1,577 +0,0 @@ -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 deleted file mode 100644 index 2057b0881..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/receiver/AlarmUpdateReceiver.java +++ /dev/null @@ -1,33 +0,0 @@ -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 deleted file mode 100644 index e6b1a1b49..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/receiver/ConnectivityActionReceiver.java +++ /dev/null @@ -1,46 +0,0 @@ -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 deleted file mode 100644 index ec63bc2ae..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/receiver/FeedUpdateReceiver.java +++ /dev/null @@ -1,46 +0,0 @@ -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 deleted file mode 100644 index be54148cf..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/receiver/MediaButtonReceiver.java +++ /dev/null @@ -1,32 +0,0 @@ -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 deleted file mode 100644 index 3dcfecdbd..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/receiver/PlayerWidget.java +++ /dev/null @@ -1,50 +0,0 @@ -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 deleted file mode 100644 index 8a2659029..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/service/GpodnetSyncService.java +++ /dev/null @@ -1,245 +0,0 @@ -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 deleted file mode 100644 index 1c62eaa77..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/service/download/APRedirectHandler.java +++ /dev/null @@ -1,54 +0,0 @@ -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 deleted file mode 100644 index e9381d509..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/service/download/DownloadService.java +++ /dev/null @@ -1,1230 +0,0 @@ -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 deleted file mode 100644 index d05650d10..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/service/download/DownloadStatus.java +++ /dev/null @@ -1,181 +0,0 @@ -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 deleted file mode 100644 index 5af9c2d05..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/service/download/Downloader.java +++ /dev/null @@ -1,69 +0,0 @@ -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 deleted file mode 100644 index 2d9347b0a..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/service/download/DownloaderCallback.java +++ /dev/null @@ -1,10 +0,0 @@ -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 deleted file mode 100644 index cba59be01..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/service/download/HttpDownloader.java +++ /dev/null @@ -1,246 +0,0 @@ -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 deleted file mode 100644 index c191c9521..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java +++ /dev/null @@ -1,1080 +0,0 @@ -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 deleted file mode 100644 index 62ad59166..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceMediaPlayer.java +++ /dev/null @@ -1,979 +0,0 @@ -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 deleted file mode 100644 index 1b33e8667..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceTaskManager.java +++ /dev/null @@ -1,384 +0,0 @@ -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 deleted file mode 100644 index 1ad0c25d9..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/service/playback/PlayerStatus.java +++ /dev/null @@ -1,14 +0,0 @@ -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 deleted file mode 100644 index 495e2c0f2..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/service/playback/PlayerWidgetService.java +++ /dev/null @@ -1,190 +0,0 @@ -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 deleted file mode 100644 index 1b93e6ea2..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java +++ /dev/null @@ -1,908 +0,0 @@ -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 deleted file mode 100644 index 28cab29b9..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java +++ /dev/null @@ -1,895 +0,0 @@ -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 deleted file mode 100644 index 225f74c96..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java +++ /dev/null @@ -1,974 +0,0 @@ -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 deleted file mode 100644 index c85559e20..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/storage/DownloadRequestException.java +++ /dev/null @@ -1,25 +0,0 @@ -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 deleted file mode 100644 index c313055a5..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/storage/DownloadRequester.java +++ /dev/null @@ -1,366 +0,0 @@ -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 deleted file mode 100644 index f6a59836b..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/storage/FeedItemStatistics.java +++ /dev/null @@ -1,70 +0,0 @@ -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 deleted file mode 100644 index 41b379471..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/storage/FeedSearcher.java +++ /dev/null @@ -1,57 +0,0 @@ -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 deleted file mode 100644 index eb6592510..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/storage/PodDBAdapter.java +++ /dev/null @@ -1,1391 +0,0 @@ -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 deleted file mode 100644 index 9efc5888f..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/syndication/handler/FeedHandler.java +++ /dev/null @@ -1,34 +0,0 @@ -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 deleted file mode 100644 index 45d1413bf..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/syndication/handler/FeedHandlerResult.java +++ /dev/null @@ -1,19 +0,0 @@ -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 deleted file mode 100644 index 4fe8e1aff..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/syndication/handler/HandlerState.java +++ /dev/null @@ -1,98 +0,0 @@ -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 deleted file mode 100644 index 573c873eb..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/syndication/handler/SyndHandler.java +++ /dev/null @@ -1,126 +0,0 @@ -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 deleted file mode 100644 index e1ebd63a5..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/syndication/handler/TypeGetter.java +++ /dev/null @@ -1,111 +0,0 @@ -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 deleted file mode 100644 index 3da9251d9..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/syndication/handler/UnsupportedFeedtypeException.java +++ /dev/null @@ -1,38 +0,0 @@ -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 deleted file mode 100644 index 71bf69ffa..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSContent.java +++ /dev/null @@ -1,25 +0,0 @@ -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 deleted file mode 100644 index fb794d7e0..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSITunes.java +++ /dev/null @@ -1,51 +0,0 @@ -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 deleted file mode 100644 index 15c377f79..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSMedia.java +++ /dev/null @@ -1,68 +0,0 @@ -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 deleted file mode 100644 index fd8f6176b..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSRSS20.java +++ /dev/null @@ -1,141 +0,0 @@ -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 deleted file mode 100644 index 2b4a2767d..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSSimpleChapters.java +++ /dev/null @@ -1,42 +0,0 @@ -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 deleted file mode 100644 index cf118d202..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/syndication/namespace/Namespace.java +++ /dev/null @@ -1,21 +0,0 @@ -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 deleted file mode 100644 index 8adcd2086..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/syndication/namespace/SyndElement.java +++ /dev/null @@ -1,22 +0,0 @@ -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 deleted file mode 100644 index 43fe0edb7..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/syndication/namespace/atom/AtomText.java +++ /dev/null @@ -1,46 +0,0 @@ -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 deleted file mode 100644 index 1547dc222..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/syndication/namespace/atom/NSAtom.java +++ /dev/null @@ -1,194 +0,0 @@ -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 deleted file mode 100644 index 977d92304..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/syndication/util/SyndDateUtils.java +++ /dev/null @@ -1,153 +0,0 @@ -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 deleted file mode 100644 index 8d1d8ffde..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/syndication/util/SyndTypeUtils.java +++ /dev/null @@ -1,42 +0,0 @@ -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 deleted file mode 100644 index d6046026f..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/util/ChapterUtils.java +++ /dev/null @@ -1,261 +0,0 @@ -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 deleted file mode 100644 index a0b514bd6..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/util/Converter.java +++ /dev/null @@ -1,103 +0,0 @@ -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 deleted file mode 100644 index 447e7d256..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/util/DownloadError.java +++ /dev/null @@ -1,52 +0,0 @@ -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 deleted file mode 100644 index 5d2803b84..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/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.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 deleted file mode 100644 index 4c23b161b..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/util/EpisodeFilter.java +++ /dev/null @@ -1,49 +0,0 @@ -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 deleted file mode 100644 index bf14cd23e..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/util/FeedtitleComparator.java +++ /dev/null @@ -1,15 +0,0 @@ -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 deleted file mode 100644 index 00c023b64..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/util/FileNameGenerator.java +++ /dev/null @@ -1,36 +0,0 @@ -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 deleted file mode 100644 index c98c2d82a..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/util/InvalidFeedException.java +++ /dev/null @@ -1,21 +0,0 @@ -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 deleted file mode 100644 index 07432d28a..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/util/LangUtils.java +++ /dev/null @@ -1,120 +0,0 @@ -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 deleted file mode 100644 index 89bba290c..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/util/NetworkUtils.java +++ /dev/null @@ -1,69 +0,0 @@ -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 deleted file mode 100644 index 8e40ae184..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/util/QueueAccess.java +++ /dev/null @@ -1,93 +0,0 @@ -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 deleted file mode 100644 index 85f32ed50..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/util/ShareUtils.java +++ /dev/null @@ -1,34 +0,0 @@ -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 deleted file mode 100644 index 7e7c6c08b..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/util/ShownotesProvider.java +++ /dev/null @@ -1,16 +0,0 @@ -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 deleted file mode 100644 index f899c211f..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/util/StorageUtils.java +++ /dev/null @@ -1,66 +0,0 @@ -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 deleted file mode 100644 index 72d73138d..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/util/ThemeUtils.java +++ /dev/null @@ -1,22 +0,0 @@ -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 deleted file mode 100644 index c614abbc1..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/util/URIUtil.java +++ /dev/null @@ -1,35 +0,0 @@ -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 deleted file mode 100644 index c707e55bc..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/util/URLChecker.java +++ /dev/null @@ -1,51 +0,0 @@ -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 deleted file mode 100644 index d0721ac23..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/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.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 deleted file mode 100644 index 5274ffc9e..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/util/comparator/ChapterStartTimeComparator.java +++ /dev/null @@ -1,20 +0,0 @@ -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 deleted file mode 100644 index ebdbfe2a5..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/util/comparator/DownloadStatusComparator.java +++ /dev/null @@ -1,15 +0,0 @@ -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 deleted file mode 100644 index a1f3ec699..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/util/comparator/FeedItemPubdateComparator.java +++ /dev/null @@ -1,19 +0,0 @@ -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 deleted file mode 100644 index 84d244660..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/util/comparator/PlaybackCompletionDateComparator.java +++ /dev/null @@ -1,19 +0,0 @@ -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 deleted file mode 100644 index b16e0949d..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/util/comparator/SearchResultValueComparator.java +++ /dev/null @@ -1,14 +0,0 @@ -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 deleted file mode 100644 index 287fe1100..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/util/exception/MediaFileNotFoundException.java +++ /dev/null @@ -1,23 +0,0 @@ -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 deleted file mode 100644 index 5a7cfa47f..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/util/flattr/FlattrServiceCreator.java +++ /dev/null @@ -1,25 +0,0 @@ -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 deleted file mode 100644 index d82171d1a..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/util/flattr/FlattrStatus.java +++ /dev/null @@ -1,68 +0,0 @@ -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 deleted file mode 100644 index 515028ab6..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/util/flattr/FlattrThing.java +++ /dev/null @@ -1,7 +0,0 @@ -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 deleted file mode 100644 index e07ed11e9..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/util/flattr/FlattrUtils.java +++ /dev/null @@ -1,305 +0,0 @@ -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 deleted file mode 100644 index 2c178496e..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/util/flattr/SimpleFlattrThing.java +++ /dev/null @@ -1,30 +0,0 @@ -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 deleted file mode 100644 index 17581d3e9..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/util/gui/FeedItemUndoToken.java +++ /dev/null @@ -1,55 +0,0 @@ -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 deleted file mode 100644 index a0bce1c79..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/util/id3reader/ChapterReader.java +++ /dev/null @@ -1,118 +0,0 @@ -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 deleted file mode 100644 index a238c11e9..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/util/id3reader/ID3Reader.java +++ /dev/null @@ -1,250 +0,0 @@ -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 deleted file mode 100644 index 0c746d7e5..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/util/id3reader/ID3ReaderException.java +++ /dev/null @@ -1,20 +0,0 @@ -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 deleted file mode 100644 index 89eab1398..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/util/id3reader/model/FrameHeader.java +++ /dev/null @@ -1,17 +0,0 @@ -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 deleted file mode 100644 index 346e2893f..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/util/id3reader/model/Header.java +++ /dev/null @@ -1,29 +0,0 @@ -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 deleted file mode 100644 index 0a6b8357f..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/util/id3reader/model/TagHeader.java +++ /dev/null @@ -1,26 +0,0 @@ -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 deleted file mode 100644 index f85ac412d..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/util/menuhandler/FeedItemMenuHandler.java +++ /dev/null @@ -1,191 +0,0 @@ -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 deleted file mode 100644 index 757cc5f56..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/util/menuhandler/FeedMenuHandler.java +++ /dev/null @@ -1,86 +0,0 @@ -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 deleted file mode 100644 index 4258c4d22..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/util/menuhandler/MenuItemUtils.java +++ /dev/null @@ -1,31 +0,0 @@ -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 deleted file mode 100644 index 61bf9960f..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/util/menuhandler/NavDrawerActivity.java +++ /dev/null @@ -1,9 +0,0 @@ -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 deleted file mode 100644 index aafcea307..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/util/playback/AudioPlayer.java +++ /dev/null @@ -1,34 +0,0 @@ -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 deleted file mode 100644 index 49769f4f0..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/util/playback/ExternalMedia.java +++ /dev/null @@ -1,235 +0,0 @@ -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 deleted file mode 100644 index 147c7848d..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/util/playback/IPlayer.java +++ /dev/null @@ -1,69 +0,0 @@ -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 deleted file mode 100644 index a3a907e48..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/util/playback/MediaPlayerError.java +++ /dev/null @@ -1,23 +0,0 @@ -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 deleted file mode 100644 index 7ebd580f7..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/util/playback/Playable.java +++ /dev/null @@ -1,207 +0,0 @@ -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 deleted file mode 100644 index 35bd27057..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java +++ /dev/null @@ -1,784 +0,0 @@ -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 deleted file mode 100644 index 5177bbca3..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/util/playback/Timeline.java +++ /dev/null @@ -1,161 +0,0 @@ -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 deleted file mode 100644 index dc5270d8f..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/util/playback/VideoPlayer.java +++ /dev/null @@ -1,67 +0,0 @@ -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 deleted file mode 100644 index 9588265b8..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/util/syndication/FeedDiscoverer.java +++ /dev/null @@ -1,78 +0,0 @@ -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 deleted file mode 100644 index 4799d3881..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/OggInputStream.java +++ /dev/null @@ -1,81 +0,0 @@ -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 deleted file mode 100644 index a6934c60e..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/VorbisCommentChapterReader.java +++ /dev/null @@ -1,101 +0,0 @@ -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 deleted file mode 100644 index 5f9dd0faf..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/VorbisCommentHeader.java +++ /dev/null @@ -1,26 +0,0 @@ -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 deleted file mode 100644 index 9639b9c42..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/VorbisCommentReader.java +++ /dev/null @@ -1,194 +0,0 @@ -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 deleted file mode 100644 index 89ab20db0..000000000 --- a/app/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/VorbisCommentReaderException.java +++ /dev/null @@ -1,24 +0,0 @@ -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/FeedItemDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/FeedItemDialog.java index e62daa08b..8cdddc121 100644 --- a/app/src/main/java/de/danoeh/antennapod/dialog/FeedItemDialog.java +++ b/app/src/main/java/de/danoeh/antennapod/dialog/FeedItemDialog.java @@ -42,7 +42,7 @@ 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; +import de.danoeh.antennapod.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/TimeDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/TimeDialog.java new file mode 100644 index 000000000..6561d501e --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/dialog/TimeDialog.java @@ -0,0 +1,138 @@ +package de.danoeh.antennapod.dialog; + +import android.app.Dialog; +import android.content.Context; +import android.os.Bundle; +import android.text.Editable; +import android.text.TextWatcher; +import android.util.Log; +import android.view.View; +import android.view.Window; +import android.view.inputmethod.InputMethodManager; +import android.widget.*; +import de.danoeh.antennapod.core.BuildConfig; +import de.danoeh.antennapod.R; + +import java.util.concurrent.TimeUnit; + +public abstract class TimeDialog extends Dialog { + private static final String TAG = "TimeDialog"; + + private static final int DEFAULT_SPINNER_POSITION = 1; + + private Context context; + + private EditText etxtTime; + private Spinner spTimeUnit; + private Button butConfirm; + private Button butCancel; + + private TimeUnit[] units = {TimeUnit.SECONDS, TimeUnit.MINUTES, + TimeUnit.HOURS}; + + public TimeDialog(Context context, int titleTextId, int leftButtonTextId) { + super(context); + this.context = context; + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + requestWindowFeature(Window.FEATURE_NO_TITLE); + String[] spinnerContent = new String[]{context.getString(R.string.time_unit_seconds), + context.getString(R.string.time_unit_minutes), + context.getString(R.string.time_unit_hours)}; + + setContentView(R.layout.time_dialog); + etxtTime = (EditText) findViewById(R.id.etxtTime); + spTimeUnit = (Spinner) findViewById(R.id.spTimeUnit); + butConfirm = (Button) findViewById(R.id.butConfirm); + butCancel = (Button) findViewById(R.id.butCancel); + + butConfirm.setText(R.string.set_sleeptimer_label); + butCancel.setText(R.string.cancel_label); + setTitle(R.string.set_sleeptimer_label); + ArrayAdapter 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/fragment/ItemlistFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/ItemlistFragment.java index b16e4f930..9eaeb56dd 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/ItemlistFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/ItemlistFragment.java @@ -48,9 +48,9 @@ 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; +import de.danoeh.antennapod.menuhandler.FeedMenuHandler; +import de.danoeh.antennapod.menuhandler.MenuItemUtils; +import de.danoeh.antennapod.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 4f37f4613..d126f2980 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/NewEpisodesFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/NewEpisodesFragment.java @@ -33,8 +33,8 @@ 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 de.danoeh.antennapod.menuhandler.MenuItemUtils; +import de.danoeh.antennapod.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 61e4ae1bb..4a07ce2b7 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/PlaybackHistoryFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/PlaybackHistoryFragment.java @@ -25,8 +25,8 @@ 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 de.danoeh.antennapod.menuhandler.MenuItemUtils; +import de.danoeh.antennapod.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 c1191d933..3192a84de 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java @@ -37,8 +37,8 @@ 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 de.danoeh.antennapod.menuhandler.MenuItemUtils; +import de.danoeh.antennapod.menuhandler.NavDrawerActivity; /** * Shows all items in the queue 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 23cc1d0b8..c16ba426e 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/SearchFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/SearchFragment.java @@ -20,8 +20,8 @@ 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 de.danoeh.antennapod.menuhandler.MenuItemUtils; +import de.danoeh.antennapod.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 14b3a9c40..15a0b55b1 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 @@ -18,8 +18,8 @@ import de.danoeh.antennapod.adapter.gpodnet.PodcastListAdapter; import de.danoeh.antennapod.core.gpoddernet.GpodnetService; import de.danoeh.antennapod.core.gpoddernet.GpodnetServiceException; import de.danoeh.antennapod.core.gpoddernet.model.GpodnetPodcast; -import de.danoeh.antennapod.core.util.menuhandler.MenuItemUtils; -import de.danoeh.antennapod.core.util.menuhandler.NavDrawerActivity; +import de.danoeh.antennapod.menuhandler.MenuItemUtils; +import de.danoeh.antennapod.menuhandler.NavDrawerActivity; 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 b099953a8..635842196 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 @@ -13,8 +13,8 @@ import de.danoeh.antennapod.R; import de.danoeh.antennapod.core.gpoddernet.GpodnetService; import de.danoeh.antennapod.core.gpoddernet.GpodnetServiceException; import de.danoeh.antennapod.core.gpoddernet.model.GpodnetPodcast; -import de.danoeh.antennapod.core.util.menuhandler.MenuItemUtils; -import de.danoeh.antennapod.core.util.menuhandler.NavDrawerActivity; +import de.danoeh.antennapod.menuhandler.MenuItemUtils; +import de.danoeh.antennapod.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/TagListFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/TagListFragment.java index 819a28c2d..24e0e4caa 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 @@ -21,8 +21,8 @@ import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.core.gpoddernet.GpodnetService; import de.danoeh.antennapod.core.gpoddernet.GpodnetServiceException; import de.danoeh.antennapod.core.gpoddernet.model.GpodnetTag; -import de.danoeh.antennapod.core.util.menuhandler.MenuItemUtils; -import de.danoeh.antennapod.core.util.menuhandler.NavDrawerActivity; +import de.danoeh.antennapod.menuhandler.MenuItemUtils; +import de.danoeh.antennapod.menuhandler.NavDrawerActivity; public class TagListFragment extends ListFragment { private static final String TAG = "TagListFragment"; diff --git a/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedItemMenuHandler.java b/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedItemMenuHandler.java new file mode 100644 index 000000000..8ccbdafc6 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedItemMenuHandler.java @@ -0,0 +1,191 @@ +package de.danoeh.antennapod.menuhandler; + +import android.content.Context; +import android.content.Intent; +import android.net.Uri; + +import de.danoeh.antennapod.core.BuildConfig; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.service.playback.PlaybackService; +import de.danoeh.antennapod.core.storage.DBTasks; +import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.core.storage.DownloadRequestException; +import de.danoeh.antennapod.core.storage.DownloadRequester; +import de.danoeh.antennapod.core.util.QueueAccess; +import de.danoeh.antennapod.core.util.ShareUtils; + +/** + * Handles interactions with the FeedItemMenu. + */ +public class FeedItemMenuHandler { + private static final String TAG = "FeedItemMenuHandler"; + + private FeedItemMenuHandler() { + + } + + /** + * Used by the MenuHandler to access different types of menus through one + * interface + */ + public interface MenuInterface { + /** + * Implementations of this method should call findItem(id) on their + * menu-object and call setVisibility(visibility) on the returned + * MenuItem object. + */ + abstract void setItemVisibility(int id, boolean visible); + } + + /** + * This method should be called in the prepare-methods of menus. It changes + * the visibility of the menu items depending on a FeedItem's attributes. + * + * @param mi An instance of MenuInterface that the method uses to change a + * MenuItem's visibility + * @param selectedItem The FeedItem for which the menu is supposed to be prepared + * @param showExtendedMenu True if MenuItems that let the user share information about + * the FeedItem and visit its website should be set visible. This + * parameter should be set to false if the menu space is limited. + * @param queueAccess Used for testing if the queue contains the selected item + * @return Returns true if selectedItem is not null. + */ + public static boolean onPrepareMenu(MenuInterface mi, + FeedItem selectedItem, boolean showExtendedMenu, QueueAccess queueAccess) { + if (selectedItem == null) { + return false; + } + DownloadRequester requester = DownloadRequester.getInstance(); + boolean hasMedia = selectedItem.getMedia() != null; + boolean downloaded = hasMedia && selectedItem.getMedia().isDownloaded(); + boolean downloading = hasMedia + && requester.isDownloadingFile(selectedItem.getMedia()); + boolean notLoadedAndNotLoading = hasMedia && (!downloaded) + && (!downloading); + boolean isPlaying = hasMedia + && selectedItem.getState() == FeedItem.State.PLAYING; + + FeedItem.State state = selectedItem.getState(); + + if (!isPlaying) { + mi.setItemVisibility(R.id.skip_episode_item, false); + } + if (!downloaded || isPlaying) { + mi.setItemVisibility(R.id.play_item, false); + mi.setItemVisibility(R.id.remove_item, false); + } + if (!notLoadedAndNotLoading) { + mi.setItemVisibility(R.id.download_item, false); + } + if (!(notLoadedAndNotLoading | downloading) | isPlaying) { + mi.setItemVisibility(R.id.stream_item, false); + } + if (!downloading) { + mi.setItemVisibility(R.id.cancel_download_item, false); + } + + boolean isInQueue = queueAccess.contains(selectedItem.getId()); + if (!isInQueue || isPlaying) { + mi.setItemVisibility(R.id.remove_from_queue_item, false); + } + if (!(!isInQueue && selectedItem.getMedia() != null)) { + mi.setItemVisibility(R.id.add_to_queue_item, false); + } + if (!showExtendedMenu || selectedItem.getLink() == null) { + mi.setItemVisibility(R.id.share_link_item, false); + } + + if (!BuildConfig.DEBUG + || !(state == FeedItem.State.IN_PROGRESS || state == FeedItem.State.READ)) { + mi.setItemVisibility(R.id.mark_unread_item, false); + } + if (!(state == FeedItem.State.NEW || state == FeedItem.State.IN_PROGRESS)) { + mi.setItemVisibility(R.id.mark_read_item, false); + } + + if (!showExtendedMenu || selectedItem.getLink() == null) { + mi.setItemVisibility(R.id.visit_website_item, false); + } + + if (selectedItem.getPaymentLink() == null || !selectedItem.getFlattrStatus().flattrable()) { + mi.setItemVisibility(R.id.support_item, false); + } + return true; + } + + /** + * The same method as onPrepareMenu(MenuInterface, FeedItem, boolean, QueueAccess), but lets the + * caller also specify a list of menu items that should not be shown. + * + * @param excludeIds Menu item that should be excluded + * @return true if selectedItem is not null. + */ + public static boolean onPrepareMenu(MenuInterface mi, + FeedItem selectedItem, boolean showExtendedMenu, QueueAccess queueAccess, int... excludeIds) { + boolean rc = onPrepareMenu(mi, selectedItem, showExtendedMenu, queueAccess); + if (rc && excludeIds != null) { + for (int id : excludeIds) { + mi.setItemVisibility(id, false); + } + } + + return rc; + } + + public static boolean onMenuItemClicked(Context context, int menuItemId, + FeedItem selectedItem) throws DownloadRequestException { + DownloadRequester requester = DownloadRequester.getInstance(); + switch (menuItemId) { + case R.id.skip_episode_item: + context.sendBroadcast(new Intent( + PlaybackService.ACTION_SKIP_CURRENT_EPISODE)); + break; + case R.id.download_item: + DBTasks.downloadFeedItems(context, selectedItem); + break; + case R.id.play_item: + DBTasks.playMedia(context, selectedItem.getMedia(), true, true, + false); + break; + case R.id.remove_item: + DBWriter.deleteFeedMediaOfItem(context, selectedItem.getMedia().getId()); + break; + case R.id.cancel_download_item: + requester.cancelDownload(context, selectedItem.getMedia()); + break; + case R.id.mark_read_item: + DBWriter.markItemRead(context, selectedItem, true, true); + break; + case R.id.mark_unread_item: + DBWriter.markItemRead(context, selectedItem, false, true); + break; + case R.id.add_to_queue_item: + DBWriter.addQueueItem(context, selectedItem.getId()); + break; + case R.id.remove_from_queue_item: + DBWriter.removeQueueItem(context, selectedItem.getId(), true); + break; + case R.id.stream_item: + DBTasks.playMedia(context, selectedItem.getMedia(), true, true, + true); + break; + case R.id.visit_website_item: + Uri uri = Uri.parse(selectedItem.getLink()); + context.startActivity(new Intent(Intent.ACTION_VIEW, uri)); + break; + case R.id.support_item: + DBTasks.flattrItemIfLoggedIn(context, selectedItem); + break; + case R.id.share_link_item: + ShareUtils.shareFeedItemLink(context, selectedItem); + break; + default: + return false; + } + // Refresh menu state + + return true; + } + +} diff --git a/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedMenuHandler.java b/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedMenuHandler.java new file mode 100644 index 000000000..62ae28820 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedMenuHandler.java @@ -0,0 +1,86 @@ +package de.danoeh.antennapod.menuhandler; + +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.util.Log; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import de.danoeh.antennapod.core.BuildConfig; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.core.service.download.DownloadService; +import de.danoeh.antennapod.core.storage.DBTasks; +import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.core.storage.DownloadRequestException; +import de.danoeh.antennapod.core.storage.DownloadRequester; +import de.danoeh.antennapod.core.util.ShareUtils; + +/** Handles interactions with the FeedItemMenu. */ +public class FeedMenuHandler { + private static final String TAG = "FeedMenuHandler"; + + public static boolean onCreateOptionsMenu(MenuInflater inflater, Menu menu) { + inflater.inflate(R.menu.feedlist, menu); + return true; + } + + public static boolean onPrepareOptionsMenu(Menu menu, Feed selectedFeed) { + if (selectedFeed == null) { + return true; + } + + if (BuildConfig.DEBUG) + Log.d(TAG, "Preparing options menu"); + menu.findItem(R.id.mark_all_read_item).setVisible( + selectedFeed.hasNewItems(true)); + if (selectedFeed.getPaymentLink() != null && selectedFeed.getFlattrStatus().flattrable()) + menu.findItem(R.id.support_item).setVisible(true); + else + menu.findItem(R.id.support_item).setVisible(false); + MenuItem refresh = menu.findItem(R.id.refresh_item); + if (DownloadService.isRunning + && DownloadRequester.getInstance().isDownloadingFile( + selectedFeed)) { + refresh.setVisible(false); + } else { + refresh.setVisible(true); + } + + return true; + } + + /** + * NOTE: This method does not handle clicks on the 'remove feed' - item. + * + * @throws DownloadRequestException + */ + public static boolean onOptionsItemClicked(Context context, MenuItem item, + Feed selectedFeed) throws DownloadRequestException { + switch (item.getItemId()) { + case R.id.refresh_item: + DBTasks.refreshFeed(context, selectedFeed); + break; + case R.id.mark_all_read_item: + DBWriter.markFeedRead(context, selectedFeed.getId()); + break; + case R.id.visit_website_item: + Uri uri = Uri.parse(selectedFeed.getLink()); + context.startActivity(new Intent(Intent.ACTION_VIEW, uri)); + break; + case R.id.support_item: + DBTasks.flattrFeedIfLoggedIn(context, selectedFeed); + break; + case R.id.share_link_item: + ShareUtils.shareFeedlink(context, selectedFeed); + break; + case R.id.share_source_item: + ShareUtils.shareFeedDownloadLink(context, selectedFeed); + break; + default: + return false; + } + return true; + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/menuhandler/MenuItemUtils.java b/app/src/main/java/de/danoeh/antennapod/menuhandler/MenuItemUtils.java new file mode 100644 index 000000000..c4a96ac3f --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/menuhandler/MenuItemUtils.java @@ -0,0 +1,31 @@ +package de.danoeh.antennapod.menuhandler; + +import android.support.v4.view.MenuItemCompat; +import android.support.v7.widget.SearchView; +import android.view.Menu; +import android.view.MenuItem; + +import de.danoeh.antennapod.core.R; + +/** + * Utilities for menu items + */ +public class MenuItemUtils { + + public static MenuItem addSearchItem(Menu menu, SearchView searchView) { + MenuItem item = menu.add(Menu.NONE, R.id.search_item, Menu.NONE, R.string.search_label); + MenuItemCompat.setShowAsAction(item, MenuItemCompat.SHOW_AS_ACTION_IF_ROOM); + MenuItemCompat.setActionView(item, searchView); + return item; + } + + /** + * Checks if the navigation drawer of the DrawerActivity is opened. This can be useful for Fragments + * that hide their menu if the navigation drawer is open. + * + * @return True if the drawer is open, false otherwise (also if the parameter is null) + */ + public static boolean isActivityDrawerOpen(NavDrawerActivity activity) { + return activity != null && activity.isDrawerOpen(); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/menuhandler/NavDrawerActivity.java b/app/src/main/java/de/danoeh/antennapod/menuhandler/NavDrawerActivity.java new file mode 100644 index 000000000..6ceaaada4 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/menuhandler/NavDrawerActivity.java @@ -0,0 +1,9 @@ +package de.danoeh.antennapod.menuhandler; + +/** + * Defines useful methods for activities that have a navigation drawer + */ +public interface NavDrawerActivity { + + public boolean isDrawerOpen(); +} diff --git a/app/src/main/java/de/danoeh/antennapod/receiver/PlayerWidget.java b/app/src/main/java/de/danoeh/antennapod/receiver/PlayerWidget.java new file mode 100644 index 000000000..7ab386edf --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/receiver/PlayerWidget.java @@ -0,0 +1,49 @@ +package de.danoeh.antennapod.receiver; + +import android.appwidget.AppWidgetManager; +import android.appwidget.AppWidgetProvider; +import android.content.Context; +import android.content.Intent; +import android.util.Log; + +import org.apache.commons.lang3.StringUtils; + +import de.danoeh.antennapod.core.BuildConfig; +import de.danoeh.antennapod.core.service.playback.PlaybackService; +import de.danoeh.antennapod.service.PlayerWidgetService; + +public class PlayerWidget extends AppWidgetProvider { + private static final String TAG = "PlayerWidget"; + + @Override + public void onReceive(Context context, Intent intent) { + if (StringUtils.equals(intent.getAction(), PlaybackService.FORCE_WIDGET_UPDATE)) { + startUpdate(context); + } else if (StringUtils.equals(intent.getAction(), PlaybackService.STOP_WIDGET_UPDATE)) { + stopUpdate(context); + } + + } + + @Override + public void onEnabled(Context context) { + super.onEnabled(context); + if (BuildConfig.DEBUG) + Log.d(TAG, "Widget enabled"); + } + + @Override + public void onUpdate(Context context, AppWidgetManager appWidgetManager, + int[] appWidgetIds) { + startUpdate(context); + } + + private void startUpdate(Context context) { + context.startService(new Intent(context, PlayerWidgetService.class)); + } + + private void stopUpdate(Context context) { + context.stopService(new Intent(context, PlayerWidgetService.class)); + } + +} diff --git a/app/src/main/java/de/danoeh/antennapod/service/PlayerWidgetService.java b/app/src/main/java/de/danoeh/antennapod/service/PlayerWidgetService.java new file mode 100644 index 000000000..514cbb74e --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/service/PlayerWidgetService.java @@ -0,0 +1,192 @@ +package de.danoeh.antennapod.service; + +import android.app.PendingIntent; +import android.app.Service; +import android.appwidget.AppWidgetManager; +import android.content.ComponentName; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.Build; +import android.os.IBinder; +import android.util.Log; +import android.view.KeyEvent; +import android.view.View; +import android.widget.RemoteViews; +import de.danoeh.antennapod.core.BuildConfig; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.receiver.MediaButtonReceiver; +import de.danoeh.antennapod.receiver.PlayerWidget; +import de.danoeh.antennapod.core.service.playback.PlaybackService; +import de.danoeh.antennapod.core.service.playback.PlayerStatus; +import de.danoeh.antennapod.core.util.Converter; +import de.danoeh.antennapod.core.util.playback.Playable; + +/** Updates the state of the player widget */ +public class PlayerWidgetService extends Service { + private static final String TAG = "PlayerWidgetService"; + + private PlaybackService playbackService; + /** 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/res/drawable-hdpi-v11/ic_stat_antenna.png b/app/src/main/res/drawable-hdpi-v11/ic_stat_antenna.png deleted file mode 100644 index 37d73c734..000000000 Binary files a/app/src/main/res/drawable-hdpi-v11/ic_stat_antenna.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi-v11/ic_stat_authentication.png b/app/src/main/res/drawable-hdpi-v11/ic_stat_authentication.png deleted file mode 100755 index ad148cc6b..000000000 Binary files a/app/src/main/res/drawable-hdpi-v11/ic_stat_authentication.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi-v11/stat_notify_sync.png b/app/src/main/res/drawable-hdpi-v11/stat_notify_sync.png deleted file mode 100644 index 90b39c958..000000000 Binary files a/app/src/main/res/drawable-hdpi-v11/stat_notify_sync.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi-v11/stat_notify_sync_error.png b/app/src/main/res/drawable-hdpi-v11/stat_notify_sync_error.png deleted file mode 100644 index 074cdee27..000000000 Binary files a/app/src/main/res/drawable-hdpi-v11/stat_notify_sync_error.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/action_about.png b/app/src/main/res/drawable-hdpi/action_about.png deleted file mode 100644 index 8f39c428a..000000000 Binary files a/app/src/main/res/drawable-hdpi/action_about.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/action_about_dark.png b/app/src/main/res/drawable-hdpi/action_about_dark.png deleted file mode 100755 index 6eaf08aec..000000000 Binary files a/app/src/main/res/drawable-hdpi/action_about_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/action_search.png b/app/src/main/res/drawable-hdpi/action_search.png deleted file mode 100644 index e6b704518..000000000 Binary files a/app/src/main/res/drawable-hdpi/action_search.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/action_search_dark.png b/app/src/main/res/drawable-hdpi/action_search_dark.png deleted file mode 100755 index f12e005eb..000000000 Binary files a/app/src/main/res/drawable-hdpi/action_search_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/action_settings.png b/app/src/main/res/drawable-hdpi/action_settings.png deleted file mode 100644 index cc32e2d1d..000000000 Binary files a/app/src/main/res/drawable-hdpi/action_settings.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/action_settings_dark.png b/app/src/main/res/drawable-hdpi/action_settings_dark.png deleted file mode 100755 index 3e4580e05..000000000 Binary files a/app/src/main/res/drawable-hdpi/action_settings_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/action_stream.png b/app/src/main/res/drawable-hdpi/action_stream.png deleted file mode 100644 index 8fc7a7b1e..000000000 Binary files a/app/src/main/res/drawable-hdpi/action_stream.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/action_stream_dark.png b/app/src/main/res/drawable-hdpi/action_stream_dark.png deleted file mode 100644 index 97b752cea..000000000 Binary files a/app/src/main/res/drawable-hdpi/action_stream_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/av_download.png b/app/src/main/res/drawable-hdpi/av_download.png deleted file mode 100644 index 5bceafb1e..000000000 Binary files a/app/src/main/res/drawable-hdpi/av_download.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/av_download_dark.png b/app/src/main/res/drawable-hdpi/av_download_dark.png deleted file mode 100755 index d5bfa457c..000000000 Binary files a/app/src/main/res/drawable-hdpi/av_download_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/av_fast_forward.png b/app/src/main/res/drawable-hdpi/av_fast_forward.png deleted file mode 100644 index 58ee5c26c..000000000 Binary files a/app/src/main/res/drawable-hdpi/av_fast_forward.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/av_fast_forward_dark.png b/app/src/main/res/drawable-hdpi/av_fast_forward_dark.png deleted file mode 100755 index 237c4f846..000000000 Binary files a/app/src/main/res/drawable-hdpi/av_fast_forward_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/av_pause.png b/app/src/main/res/drawable-hdpi/av_pause.png deleted file mode 100644 index 9661cfbb0..000000000 Binary files a/app/src/main/res/drawable-hdpi/av_pause.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/av_pause_dark.png b/app/src/main/res/drawable-hdpi/av_pause_dark.png deleted file mode 100755 index 6b435bb0f..000000000 Binary files a/app/src/main/res/drawable-hdpi/av_pause_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/av_play.png b/app/src/main/res/drawable-hdpi/av_play.png deleted file mode 100644 index e70f0413e..000000000 Binary files a/app/src/main/res/drawable-hdpi/av_play.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/av_play_dark.png b/app/src/main/res/drawable-hdpi/av_play_dark.png deleted file mode 100755 index df8a2ca28..000000000 Binary files a/app/src/main/res/drawable-hdpi/av_play_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/av_rewind.png b/app/src/main/res/drawable-hdpi/av_rewind.png deleted file mode 100644 index e2f843ce2..000000000 Binary files a/app/src/main/res/drawable-hdpi/av_rewind.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/av_rewind_dark.png b/app/src/main/res/drawable-hdpi/av_rewind_dark.png deleted file mode 100755 index caf517498..000000000 Binary files a/app/src/main/res/drawable-hdpi/av_rewind_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/content_discard.png b/app/src/main/res/drawable-hdpi/content_discard.png deleted file mode 100644 index e9ce89e04..000000000 Binary files a/app/src/main/res/drawable-hdpi/content_discard.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/content_discard_dark.png b/app/src/main/res/drawable-hdpi/content_discard_dark.png deleted file mode 100755 index ffd19d9e8..000000000 Binary files a/app/src/main/res/drawable-hdpi/content_discard_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/content_new.png b/app/src/main/res/drawable-hdpi/content_new.png deleted file mode 100644 index 5741995cb..000000000 Binary files a/app/src/main/res/drawable-hdpi/content_new.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/content_new_dark.png b/app/src/main/res/drawable-hdpi/content_new_dark.png deleted file mode 100755 index ad8ada6bd..000000000 Binary files a/app/src/main/res/drawable-hdpi/content_new_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/default_cover.png b/app/src/main/res/drawable-hdpi/default_cover.png deleted file mode 100644 index a6e67e2ca..000000000 Binary files a/app/src/main/res/drawable-hdpi/default_cover.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/default_cover_dark.png b/app/src/main/res/drawable-hdpi/default_cover_dark.png deleted file mode 100755 index 0f650ee25..000000000 Binary files a/app/src/main/res/drawable-hdpi/default_cover_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/device_access_time.png b/app/src/main/res/drawable-hdpi/device_access_time.png deleted file mode 100644 index 001549f38..000000000 Binary files a/app/src/main/res/drawable-hdpi/device_access_time.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/device_access_time_dark.png b/app/src/main/res/drawable-hdpi/device_access_time_dark.png deleted file mode 100755 index 314ec9319..000000000 Binary files a/app/src/main/res/drawable-hdpi/device_access_time_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_action_overflow.png b/app/src/main/res/drawable-hdpi/ic_action_overflow.png deleted file mode 100644 index 002fc4bfb..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_action_overflow.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_action_overflow_dark.png b/app/src/main/res/drawable-hdpi/ic_action_overflow_dark.png deleted file mode 100644 index c8792cbe2..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_action_overflow_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_action_pause_over_video.png b/app/src/main/res/drawable-hdpi/ic_action_pause_over_video.png deleted file mode 100755 index 64b07728f..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_action_pause_over_video.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_action_play_over_video.png b/app/src/main/res/drawable-hdpi/ic_action_play_over_video.png deleted file mode 100755 index a364ca7c2..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_action_play_over_video.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_drag_handle.png b/app/src/main/res/drawable-hdpi/ic_drag_handle.png deleted file mode 100755 index 38ec201de..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_drag_handle.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_drag_handle_dark.png b/app/src/main/res/drawable-hdpi/ic_drag_handle_dark.png deleted file mode 100755 index e96d23252..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_drag_handle_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_drawer.png b/app/src/main/res/drawable-hdpi/ic_drawer.png deleted file mode 100644 index c59f601ca..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_drawer.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_drawer_dark.png b/app/src/main/res/drawable-hdpi/ic_drawer_dark.png deleted file mode 100644 index 6614ea4f4..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_drawer_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_launcher.png b/app/src/main/res/drawable-hdpi/ic_launcher.png deleted file mode 100644 index 994b763cc..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_launcher.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_new.png b/app/src/main/res/drawable-hdpi/ic_new.png deleted file mode 100755 index 8ff519052..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_new.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_new_dark.png b/app/src/main/res/drawable-hdpi/ic_new_dark.png deleted file mode 100755 index c8581e01c..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_new_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_stat_antenna.png b/app/src/main/res/drawable-hdpi/ic_stat_antenna.png deleted file mode 100644 index 36d502492..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_stat_antenna.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_stat_authentication.png b/app/src/main/res/drawable-hdpi/ic_stat_authentication.png deleted file mode 100755 index c6b5efd33..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_stat_authentication.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/location_web_site.png b/app/src/main/res/drawable-hdpi/location_web_site.png deleted file mode 100644 index 6a2bc8857..000000000 Binary files a/app/src/main/res/drawable-hdpi/location_web_site.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/location_web_site_dark.png b/app/src/main/res/drawable-hdpi/location_web_site_dark.png deleted file mode 100755 index e154afdbc..000000000 Binary files a/app/src/main/res/drawable-hdpi/location_web_site_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/navigation_accept.png b/app/src/main/res/drawable-hdpi/navigation_accept.png deleted file mode 100644 index 58bf97217..000000000 Binary files a/app/src/main/res/drawable-hdpi/navigation_accept.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/navigation_accept_dark.png b/app/src/main/res/drawable-hdpi/navigation_accept_dark.png deleted file mode 100755 index 53cf6877e..000000000 Binary files a/app/src/main/res/drawable-hdpi/navigation_accept_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/navigation_cancel.png b/app/src/main/res/drawable-hdpi/navigation_cancel.png deleted file mode 100644 index cde36e1fa..000000000 Binary files a/app/src/main/res/drawable-hdpi/navigation_cancel.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/navigation_cancel_dark.png b/app/src/main/res/drawable-hdpi/navigation_cancel_dark.png deleted file mode 100755 index 094eea589..000000000 Binary files a/app/src/main/res/drawable-hdpi/navigation_cancel_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/navigation_chapters.png b/app/src/main/res/drawable-hdpi/navigation_chapters.png deleted file mode 100755 index b034459bc..000000000 Binary files a/app/src/main/res/drawable-hdpi/navigation_chapters.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/navigation_chapters_dark.png b/app/src/main/res/drawable-hdpi/navigation_chapters_dark.png deleted file mode 100755 index 7b0d4889c..000000000 Binary files a/app/src/main/res/drawable-hdpi/navigation_chapters_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/navigation_collapse.png b/app/src/main/res/drawable-hdpi/navigation_collapse.png deleted file mode 100755 index bd405bada..000000000 Binary files a/app/src/main/res/drawable-hdpi/navigation_collapse.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/navigation_collapse_dark.png b/app/src/main/res/drawable-hdpi/navigation_collapse_dark.png deleted file mode 100755 index ca78f2ec0..000000000 Binary files a/app/src/main/res/drawable-hdpi/navigation_collapse_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/navigation_expand.png b/app/src/main/res/drawable-hdpi/navigation_expand.png deleted file mode 100644 index 8225e74b7..000000000 Binary files a/app/src/main/res/drawable-hdpi/navigation_expand.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/navigation_expand_dark.png b/app/src/main/res/drawable-hdpi/navigation_expand_dark.png deleted file mode 100755 index 1676b104b..000000000 Binary files a/app/src/main/res/drawable-hdpi/navigation_expand_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/navigation_refresh.png b/app/src/main/res/drawable-hdpi/navigation_refresh.png deleted file mode 100644 index 479aca465..000000000 Binary files a/app/src/main/res/drawable-hdpi/navigation_refresh.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/navigation_refresh_dark.png b/app/src/main/res/drawable-hdpi/navigation_refresh_dark.png deleted file mode 100755 index bb9d855f7..000000000 Binary files a/app/src/main/res/drawable-hdpi/navigation_refresh_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/navigation_shownotes.png b/app/src/main/res/drawable-hdpi/navigation_shownotes.png deleted file mode 100755 index c5f6c97b2..000000000 Binary files a/app/src/main/res/drawable-hdpi/navigation_shownotes.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/navigation_shownotes_dark.png b/app/src/main/res/drawable-hdpi/navigation_shownotes_dark.png deleted file mode 100755 index e45ea1fd9..000000000 Binary files a/app/src/main/res/drawable-hdpi/navigation_shownotes_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/navigation_up.png b/app/src/main/res/drawable-hdpi/navigation_up.png deleted file mode 100755 index a2cf2ba52..000000000 Binary files a/app/src/main/res/drawable-hdpi/navigation_up.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/navigation_up_dark.png b/app/src/main/res/drawable-hdpi/navigation_up_dark.png deleted file mode 100755 index f2374a323..000000000 Binary files a/app/src/main/res/drawable-hdpi/navigation_up_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/social_share.png b/app/src/main/res/drawable-hdpi/social_share.png deleted file mode 100644 index 47ae18674..000000000 Binary files a/app/src/main/res/drawable-hdpi/social_share.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/social_share_dark.png b/app/src/main/res/drawable-hdpi/social_share_dark.png deleted file mode 100755 index c329f58da..000000000 Binary files a/app/src/main/res/drawable-hdpi/social_share_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/spinner_button.9.png b/app/src/main/res/drawable-hdpi/spinner_button.9.png deleted file mode 100644 index fa68a137f..000000000 Binary files a/app/src/main/res/drawable-hdpi/spinner_button.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/spinner_button_dark.9.png b/app/src/main/res/drawable-hdpi/spinner_button_dark.9.png deleted file mode 100644 index 88f8765cd..000000000 Binary files a/app/src/main/res/drawable-hdpi/spinner_button_dark.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/stat_notify_sync.png b/app/src/main/res/drawable-hdpi/stat_notify_sync.png deleted file mode 100644 index bfb8110fe..000000000 Binary files a/app/src/main/res/drawable-hdpi/stat_notify_sync.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/stat_notify_sync_error.png b/app/src/main/res/drawable-hdpi/stat_notify_sync_error.png deleted file mode 100644 index b340a313e..000000000 Binary files a/app/src/main/res/drawable-hdpi/stat_notify_sync_error.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/stat_playlist.png b/app/src/main/res/drawable-hdpi/stat_playlist.png deleted file mode 100644 index 93c3f02b8..000000000 Binary files a/app/src/main/res/drawable-hdpi/stat_playlist.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/stat_playlist_dark.png b/app/src/main/res/drawable-hdpi/stat_playlist_dark.png deleted file mode 100644 index 972ce98b3..000000000 Binary files a/app/src/main/res/drawable-hdpi/stat_playlist_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/type_audio.png b/app/src/main/res/drawable-hdpi/type_audio.png deleted file mode 100644 index d43e8a33c..000000000 Binary files a/app/src/main/res/drawable-hdpi/type_audio.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/type_audio_dark.png b/app/src/main/res/drawable-hdpi/type_audio_dark.png deleted file mode 100755 index 7b69ea56b..000000000 Binary files a/app/src/main/res/drawable-hdpi/type_audio_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/type_video.png b/app/src/main/res/drawable-hdpi/type_video.png deleted file mode 100644 index f9467146c..000000000 Binary files a/app/src/main/res/drawable-hdpi/type_video.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/type_video_dark.png b/app/src/main/res/drawable-hdpi/type_video_dark.png deleted file mode 100755 index 37f3a93a2..000000000 Binary files a/app/src/main/res/drawable-hdpi/type_video_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-ldpi-v11/ic_stat_antenna.png b/app/src/main/res/drawable-ldpi-v11/ic_stat_antenna.png deleted file mode 100644 index e44f42510..000000000 Binary files a/app/src/main/res/drawable-ldpi-v11/ic_stat_antenna.png and /dev/null differ diff --git a/app/src/main/res/drawable-ldpi/action_stream.png b/app/src/main/res/drawable-ldpi/action_stream.png deleted file mode 100644 index 5ae4f3d34..000000000 Binary files a/app/src/main/res/drawable-ldpi/action_stream.png and /dev/null differ diff --git a/app/src/main/res/drawable-ldpi/action_stream_dark.png b/app/src/main/res/drawable-ldpi/action_stream_dark.png deleted file mode 100644 index f3c81fff8..000000000 Binary files a/app/src/main/res/drawable-ldpi/action_stream_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-ldpi/ic_launcher.png b/app/src/main/res/drawable-ldpi/ic_launcher.png deleted file mode 100644 index 546090dd2..000000000 Binary files a/app/src/main/res/drawable-ldpi/ic_launcher.png and /dev/null differ diff --git a/app/src/main/res/drawable-ldpi/ic_stat_antenna.png b/app/src/main/res/drawable-ldpi/ic_stat_antenna.png deleted file mode 100644 index 63d72970d..000000000 Binary files a/app/src/main/res/drawable-ldpi/ic_stat_antenna.png and /dev/null differ diff --git a/app/src/main/res/drawable-ldpi/stat_playlist.png b/app/src/main/res/drawable-ldpi/stat_playlist.png deleted file mode 100644 index 3a702ef2f..000000000 Binary files a/app/src/main/res/drawable-ldpi/stat_playlist.png and /dev/null differ diff --git a/app/src/main/res/drawable-ldpi/stat_playlist_dark.png b/app/src/main/res/drawable-ldpi/stat_playlist_dark.png deleted file mode 100644 index b82b06f67..000000000 Binary files a/app/src/main/res/drawable-ldpi/stat_playlist_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi-v11/ic_stat_antenna.png b/app/src/main/res/drawable-mdpi-v11/ic_stat_antenna.png deleted file mode 100644 index 8808dedc7..000000000 Binary files a/app/src/main/res/drawable-mdpi-v11/ic_stat_antenna.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi-v11/ic_stat_authentication.png b/app/src/main/res/drawable-mdpi-v11/ic_stat_authentication.png deleted file mode 100755 index de69b17c0..000000000 Binary files a/app/src/main/res/drawable-mdpi-v11/ic_stat_authentication.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi-v11/stat_notify_sync.png b/app/src/main/res/drawable-mdpi-v11/stat_notify_sync.png deleted file mode 100644 index 1be8677f1..000000000 Binary files a/app/src/main/res/drawable-mdpi-v11/stat_notify_sync.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi-v11/stat_notify_sync_error.png b/app/src/main/res/drawable-mdpi-v11/stat_notify_sync_error.png deleted file mode 100644 index 30658c583..000000000 Binary files a/app/src/main/res/drawable-mdpi-v11/stat_notify_sync_error.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/action_about.png b/app/src/main/res/drawable-mdpi/action_about.png deleted file mode 100644 index 7c57436fc..000000000 Binary files a/app/src/main/res/drawable-mdpi/action_about.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/action_about_dark.png b/app/src/main/res/drawable-mdpi/action_about_dark.png deleted file mode 100755 index d7b7e6986..000000000 Binary files a/app/src/main/res/drawable-mdpi/action_about_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/action_search.png b/app/src/main/res/drawable-mdpi/action_search.png deleted file mode 100644 index 3aa644048..000000000 Binary files a/app/src/main/res/drawable-mdpi/action_search.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/action_search_dark.png b/app/src/main/res/drawable-mdpi/action_search_dark.png deleted file mode 100755 index 587d9e0bf..000000000 Binary files a/app/src/main/res/drawable-mdpi/action_search_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/action_settings.png b/app/src/main/res/drawable-mdpi/action_settings.png deleted file mode 100644 index dc66d914e..000000000 Binary files a/app/src/main/res/drawable-mdpi/action_settings.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/action_settings_dark.png b/app/src/main/res/drawable-mdpi/action_settings_dark.png deleted file mode 100755 index d3e42edcb..000000000 Binary files a/app/src/main/res/drawable-mdpi/action_settings_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/action_stream.png b/app/src/main/res/drawable-mdpi/action_stream.png deleted file mode 100644 index 4bc7d8379..000000000 Binary files a/app/src/main/res/drawable-mdpi/action_stream.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/action_stream_dark.png b/app/src/main/res/drawable-mdpi/action_stream_dark.png deleted file mode 100644 index 1f4fdd186..000000000 Binary files a/app/src/main/res/drawable-mdpi/action_stream_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/av_download.png b/app/src/main/res/drawable-mdpi/av_download.png deleted file mode 100644 index 678ecfad4..000000000 Binary files a/app/src/main/res/drawable-mdpi/av_download.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/av_download_dark.png b/app/src/main/res/drawable-mdpi/av_download_dark.png deleted file mode 100755 index cc4d9576b..000000000 Binary files a/app/src/main/res/drawable-mdpi/av_download_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/av_fast_forward.png b/app/src/main/res/drawable-mdpi/av_fast_forward.png deleted file mode 100644 index 43f15a245..000000000 Binary files a/app/src/main/res/drawable-mdpi/av_fast_forward.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/av_fast_forward_dark.png b/app/src/main/res/drawable-mdpi/av_fast_forward_dark.png deleted file mode 100755 index fc8074cea..000000000 Binary files a/app/src/main/res/drawable-mdpi/av_fast_forward_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/av_pause.png b/app/src/main/res/drawable-mdpi/av_pause.png deleted file mode 100644 index 01858e34d..000000000 Binary files a/app/src/main/res/drawable-mdpi/av_pause.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/av_pause_dark.png b/app/src/main/res/drawable-mdpi/av_pause_dark.png deleted file mode 100755 index a5aee6f2c..000000000 Binary files a/app/src/main/res/drawable-mdpi/av_pause_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/av_play.png b/app/src/main/res/drawable-mdpi/av_play.png deleted file mode 100644 index 1e3bc97af..000000000 Binary files a/app/src/main/res/drawable-mdpi/av_play.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/av_play_dark.png b/app/src/main/res/drawable-mdpi/av_play_dark.png deleted file mode 100755 index 6a40cd5f7..000000000 Binary files a/app/src/main/res/drawable-mdpi/av_play_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/av_rewind.png b/app/src/main/res/drawable-mdpi/av_rewind.png deleted file mode 100644 index a2f7f5895..000000000 Binary files a/app/src/main/res/drawable-mdpi/av_rewind.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/av_rewind_dark.png b/app/src/main/res/drawable-mdpi/av_rewind_dark.png deleted file mode 100755 index e555a2046..000000000 Binary files a/app/src/main/res/drawable-mdpi/av_rewind_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/content_discard.png b/app/src/main/res/drawable-mdpi/content_discard.png deleted file mode 100644 index cedb1085b..000000000 Binary files a/app/src/main/res/drawable-mdpi/content_discard.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/content_discard_dark.png b/app/src/main/res/drawable-mdpi/content_discard_dark.png deleted file mode 100755 index a8ee5f253..000000000 Binary files a/app/src/main/res/drawable-mdpi/content_discard_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/content_new.png b/app/src/main/res/drawable-mdpi/content_new.png deleted file mode 100644 index 884c9d270..000000000 Binary files a/app/src/main/res/drawable-mdpi/content_new.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/content_new_dark.png b/app/src/main/res/drawable-mdpi/content_new_dark.png deleted file mode 100755 index 4d5d484b3..000000000 Binary files a/app/src/main/res/drawable-mdpi/content_new_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/default_cover.png b/app/src/main/res/drawable-mdpi/default_cover.png deleted file mode 100644 index 62adf32ab..000000000 Binary files a/app/src/main/res/drawable-mdpi/default_cover.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/default_cover_dark.png b/app/src/main/res/drawable-mdpi/default_cover_dark.png deleted file mode 100755 index d6235554b..000000000 Binary files a/app/src/main/res/drawable-mdpi/default_cover_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/device_access_time.png b/app/src/main/res/drawable-mdpi/device_access_time.png deleted file mode 100644 index de9b4fb2a..000000000 Binary files a/app/src/main/res/drawable-mdpi/device_access_time.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/device_access_time_dark.png b/app/src/main/res/drawable-mdpi/device_access_time_dark.png deleted file mode 100755 index a09df2b99..000000000 Binary files a/app/src/main/res/drawable-mdpi/device_access_time_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_action_overflow.png b/app/src/main/res/drawable-mdpi/ic_action_overflow.png deleted file mode 100644 index 6f0fb23f4..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_action_overflow.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_action_overflow_dark.png b/app/src/main/res/drawable-mdpi/ic_action_overflow_dark.png deleted file mode 100644 index b4a4a221f..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_action_overflow_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_action_pause_over_video.png b/app/src/main/res/drawable-mdpi/ic_action_pause_over_video.png deleted file mode 100755 index f478ac321..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_action_pause_over_video.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_action_play_over_video.png b/app/src/main/res/drawable-mdpi/ic_action_play_over_video.png deleted file mode 100755 index 835ff3636..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_action_play_over_video.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_drag_handle.png b/app/src/main/res/drawable-mdpi/ic_drag_handle.png deleted file mode 100755 index 4afbdc67d..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_drag_handle.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_drag_handle_dark.png b/app/src/main/res/drawable-mdpi/ic_drag_handle_dark.png deleted file mode 100755 index 2b25c4101..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_drag_handle_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_drawer.png b/app/src/main/res/drawable-mdpi/ic_drawer.png deleted file mode 100644 index 1ed2c56ee..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_drawer.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_drawer_dark.png b/app/src/main/res/drawable-mdpi/ic_drawer_dark.png deleted file mode 100644 index b05c026c1..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_drawer_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_launcher.png b/app/src/main/res/drawable-mdpi/ic_launcher.png deleted file mode 100644 index 403dfabc4..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_launcher.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_new.png b/app/src/main/res/drawable-mdpi/ic_new.png deleted file mode 100755 index 84994bd10..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_new.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_new_dark.png b/app/src/main/res/drawable-mdpi/ic_new_dark.png deleted file mode 100755 index b723618b4..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_new_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_stat_antenna.png b/app/src/main/res/drawable-mdpi/ic_stat_antenna.png deleted file mode 100644 index 8b1206b51..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_stat_antenna.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_stat_authentication.png b/app/src/main/res/drawable-mdpi/ic_stat_authentication.png deleted file mode 100755 index cadfb9643..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_stat_authentication.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/location_web_site.png b/app/src/main/res/drawable-mdpi/location_web_site.png deleted file mode 100644 index f146cf997..000000000 Binary files a/app/src/main/res/drawable-mdpi/location_web_site.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/location_web_site_dark.png b/app/src/main/res/drawable-mdpi/location_web_site_dark.png deleted file mode 100755 index 41b56ec92..000000000 Binary files a/app/src/main/res/drawable-mdpi/location_web_site_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/navigation_accept.png b/app/src/main/res/drawable-mdpi/navigation_accept.png deleted file mode 100644 index cf5fab3ad..000000000 Binary files a/app/src/main/res/drawable-mdpi/navigation_accept.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/navigation_accept_dark.png b/app/src/main/res/drawable-mdpi/navigation_accept_dark.png deleted file mode 100755 index 35cda8e11..000000000 Binary files a/app/src/main/res/drawable-mdpi/navigation_accept_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/navigation_cancel.png b/app/src/main/res/drawable-mdpi/navigation_cancel.png deleted file mode 100644 index 9f4c3d6a2..000000000 Binary files a/app/src/main/res/drawable-mdpi/navigation_cancel.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/navigation_cancel_dark.png b/app/src/main/res/drawable-mdpi/navigation_cancel_dark.png deleted file mode 100755 index 3336760d5..000000000 Binary files a/app/src/main/res/drawable-mdpi/navigation_cancel_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/navigation_chapters.png b/app/src/main/res/drawable-mdpi/navigation_chapters.png deleted file mode 100755 index b1884726c..000000000 Binary files a/app/src/main/res/drawable-mdpi/navigation_chapters.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/navigation_chapters_dark.png b/app/src/main/res/drawable-mdpi/navigation_chapters_dark.png deleted file mode 100755 index 1042294e4..000000000 Binary files a/app/src/main/res/drawable-mdpi/navigation_chapters_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/navigation_collapse.png b/app/src/main/res/drawable-mdpi/navigation_collapse.png deleted file mode 100755 index 6673c7aea..000000000 Binary files a/app/src/main/res/drawable-mdpi/navigation_collapse.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/navigation_collapse_dark.png b/app/src/main/res/drawable-mdpi/navigation_collapse_dark.png deleted file mode 100755 index 01d6511ee..000000000 Binary files a/app/src/main/res/drawable-mdpi/navigation_collapse_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/navigation_expand.png b/app/src/main/res/drawable-mdpi/navigation_expand.png deleted file mode 100644 index 78107862c..000000000 Binary files a/app/src/main/res/drawable-mdpi/navigation_expand.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/navigation_expand_dark.png b/app/src/main/res/drawable-mdpi/navigation_expand_dark.png deleted file mode 100755 index aa2b40ca0..000000000 Binary files a/app/src/main/res/drawable-mdpi/navigation_expand_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/navigation_refresh.png b/app/src/main/res/drawable-mdpi/navigation_refresh.png deleted file mode 100644 index 63e70e178..000000000 Binary files a/app/src/main/res/drawable-mdpi/navigation_refresh.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/navigation_refresh_dark.png b/app/src/main/res/drawable-mdpi/navigation_refresh_dark.png deleted file mode 100755 index bd611e8e2..000000000 Binary files a/app/src/main/res/drawable-mdpi/navigation_refresh_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/navigation_shownotes.png b/app/src/main/res/drawable-mdpi/navigation_shownotes.png deleted file mode 100755 index ec6a2bf8f..000000000 Binary files a/app/src/main/res/drawable-mdpi/navigation_shownotes.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/navigation_shownotes_dark.png b/app/src/main/res/drawable-mdpi/navigation_shownotes_dark.png deleted file mode 100755 index 9c748b0b5..000000000 Binary files a/app/src/main/res/drawable-mdpi/navigation_shownotes_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/navigation_up.png b/app/src/main/res/drawable-mdpi/navigation_up.png deleted file mode 100755 index 1ee248a79..000000000 Binary files a/app/src/main/res/drawable-mdpi/navigation_up.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/navigation_up_dark.png b/app/src/main/res/drawable-mdpi/navigation_up_dark.png deleted file mode 100755 index 8ef44cbac..000000000 Binary files a/app/src/main/res/drawable-mdpi/navigation_up_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/social_share.png b/app/src/main/res/drawable-mdpi/social_share.png deleted file mode 100644 index 8aa52bc7d..000000000 Binary files a/app/src/main/res/drawable-mdpi/social_share.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/social_share_dark.png b/app/src/main/res/drawable-mdpi/social_share_dark.png deleted file mode 100755 index 056deb57b..000000000 Binary files a/app/src/main/res/drawable-mdpi/social_share_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/spinner_button.9.png b/app/src/main/res/drawable-mdpi/spinner_button.9.png deleted file mode 100644 index 716560bb1..000000000 Binary files a/app/src/main/res/drawable-mdpi/spinner_button.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/spinner_button_dark.9.png b/app/src/main/res/drawable-mdpi/spinner_button_dark.9.png deleted file mode 100644 index 8d7594685..000000000 Binary files a/app/src/main/res/drawable-mdpi/spinner_button_dark.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/stat_notify_sync.png b/app/src/main/res/drawable-mdpi/stat_notify_sync.png deleted file mode 100644 index 03ce57a47..000000000 Binary files a/app/src/main/res/drawable-mdpi/stat_notify_sync.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/stat_notify_sync_error.png b/app/src/main/res/drawable-mdpi/stat_notify_sync_error.png deleted file mode 100644 index f849b5040..000000000 Binary files a/app/src/main/res/drawable-mdpi/stat_notify_sync_error.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/stat_playlist.png b/app/src/main/res/drawable-mdpi/stat_playlist.png deleted file mode 100644 index 136a7a265..000000000 Binary files a/app/src/main/res/drawable-mdpi/stat_playlist.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/stat_playlist_dark.png b/app/src/main/res/drawable-mdpi/stat_playlist_dark.png deleted file mode 100644 index 7ed94b13c..000000000 Binary files a/app/src/main/res/drawable-mdpi/stat_playlist_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/type_audio.png b/app/src/main/res/drawable-mdpi/type_audio.png deleted file mode 100644 index 4ec9efd97..000000000 Binary files a/app/src/main/res/drawable-mdpi/type_audio.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/type_audio_dark.png b/app/src/main/res/drawable-mdpi/type_audio_dark.png deleted file mode 100755 index f8dd8469c..000000000 Binary files a/app/src/main/res/drawable-mdpi/type_audio_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/type_video.png b/app/src/main/res/drawable-mdpi/type_video.png deleted file mode 100644 index a2722b812..000000000 Binary files a/app/src/main/res/drawable-mdpi/type_video.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/type_video_dark.png b/app/src/main/res/drawable-mdpi/type_video_dark.png deleted file mode 100755 index aa0c320dc..000000000 Binary files a/app/src/main/res/drawable-mdpi/type_video_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi-v11/ic_stat_antenna.png b/app/src/main/res/drawable-xhdpi-v11/ic_stat_antenna.png deleted file mode 100644 index 59de64c87..000000000 Binary files a/app/src/main/res/drawable-xhdpi-v11/ic_stat_antenna.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi-v11/ic_stat_authentication.png b/app/src/main/res/drawable-xhdpi-v11/ic_stat_authentication.png deleted file mode 100755 index f58fb21df..000000000 Binary files a/app/src/main/res/drawable-xhdpi-v11/ic_stat_authentication.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi-v11/stat_notify_sync.png b/app/src/main/res/drawable-xhdpi-v11/stat_notify_sync.png deleted file mode 100644 index b3bf21ffe..000000000 Binary files a/app/src/main/res/drawable-xhdpi-v11/stat_notify_sync.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi-v11/stat_notify_sync_error.png b/app/src/main/res/drawable-xhdpi-v11/stat_notify_sync_error.png deleted file mode 100644 index 33582ef10..000000000 Binary files a/app/src/main/res/drawable-xhdpi-v11/stat_notify_sync_error.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/action_about.png b/app/src/main/res/drawable-xhdpi/action_about.png deleted file mode 100644 index 2641f142a..000000000 Binary files a/app/src/main/res/drawable-xhdpi/action_about.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/action_about_dark.png b/app/src/main/res/drawable-xhdpi/action_about_dark.png deleted file mode 100755 index 4ee903f07..000000000 Binary files a/app/src/main/res/drawable-xhdpi/action_about_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/action_search.png b/app/src/main/res/drawable-xhdpi/action_search.png deleted file mode 100644 index 804420aee..000000000 Binary files a/app/src/main/res/drawable-xhdpi/action_search.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/action_search_dark.png b/app/src/main/res/drawable-xhdpi/action_search_dark.png deleted file mode 100755 index 3549f84dd..000000000 Binary files a/app/src/main/res/drawable-xhdpi/action_search_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/action_settings.png b/app/src/main/res/drawable-xhdpi/action_settings.png deleted file mode 100644 index 04b65dc34..000000000 Binary files a/app/src/main/res/drawable-xhdpi/action_settings.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/action_settings_dark.png b/app/src/main/res/drawable-xhdpi/action_settings_dark.png deleted file mode 100755 index 09b014834..000000000 Binary files a/app/src/main/res/drawable-xhdpi/action_settings_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/action_stream.png b/app/src/main/res/drawable-xhdpi/action_stream.png deleted file mode 100644 index f87f2da5e..000000000 Binary files a/app/src/main/res/drawable-xhdpi/action_stream.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/action_stream_dark.png b/app/src/main/res/drawable-xhdpi/action_stream_dark.png deleted file mode 100644 index d3721318c..000000000 Binary files a/app/src/main/res/drawable-xhdpi/action_stream_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/av_download.png b/app/src/main/res/drawable-xhdpi/av_download.png deleted file mode 100644 index dfe81e064..000000000 Binary files a/app/src/main/res/drawable-xhdpi/av_download.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/av_download_dark.png b/app/src/main/res/drawable-xhdpi/av_download_dark.png deleted file mode 100755 index bc0ced50f..000000000 Binary files a/app/src/main/res/drawable-xhdpi/av_download_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/av_fast_forward.png b/app/src/main/res/drawable-xhdpi/av_fast_forward.png deleted file mode 100644 index 026c3b779..000000000 Binary files a/app/src/main/res/drawable-xhdpi/av_fast_forward.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/av_fast_forward_dark.png b/app/src/main/res/drawable-xhdpi/av_fast_forward_dark.png deleted file mode 100755 index 896334d47..000000000 Binary files a/app/src/main/res/drawable-xhdpi/av_fast_forward_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/av_pause.png b/app/src/main/res/drawable-xhdpi/av_pause.png deleted file mode 100644 index 97d6f91ac..000000000 Binary files a/app/src/main/res/drawable-xhdpi/av_pause.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/av_pause_dark.png b/app/src/main/res/drawable-xhdpi/av_pause_dark.png deleted file mode 100755 index 333c1b24d..000000000 Binary files a/app/src/main/res/drawable-xhdpi/av_pause_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/av_play.png b/app/src/main/res/drawable-xhdpi/av_play.png deleted file mode 100644 index 2d67d31e7..000000000 Binary files a/app/src/main/res/drawable-xhdpi/av_play.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/av_play_dark.png b/app/src/main/res/drawable-xhdpi/av_play_dark.png deleted file mode 100755 index 51124993d..000000000 Binary files a/app/src/main/res/drawable-xhdpi/av_play_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/av_rewind.png b/app/src/main/res/drawable-xhdpi/av_rewind.png deleted file mode 100644 index 57b41744d..000000000 Binary files a/app/src/main/res/drawable-xhdpi/av_rewind.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/av_rewind_dark.png b/app/src/main/res/drawable-xhdpi/av_rewind_dark.png deleted file mode 100755 index 69dda127c..000000000 Binary files a/app/src/main/res/drawable-xhdpi/av_rewind_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/content_discard.png b/app/src/main/res/drawable-xhdpi/content_discard.png deleted file mode 100644 index 98c73da1f..000000000 Binary files a/app/src/main/res/drawable-xhdpi/content_discard.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/content_discard_dark.png b/app/src/main/res/drawable-xhdpi/content_discard_dark.png deleted file mode 100755 index 412b33354..000000000 Binary files a/app/src/main/res/drawable-xhdpi/content_discard_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/content_new.png b/app/src/main/res/drawable-xhdpi/content_new.png deleted file mode 100644 index 9b48a63da..000000000 Binary files a/app/src/main/res/drawable-xhdpi/content_new.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/content_new_dark.png b/app/src/main/res/drawable-xhdpi/content_new_dark.png deleted file mode 100755 index 23b9a1c18..000000000 Binary files a/app/src/main/res/drawable-xhdpi/content_new_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/content_remove.png b/app/src/main/res/drawable-xhdpi/content_remove.png deleted file mode 100644 index ca7d159fd..000000000 Binary files a/app/src/main/res/drawable-xhdpi/content_remove.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/content_remove_dark.png b/app/src/main/res/drawable-xhdpi/content_remove_dark.png deleted file mode 100755 index f391760ef..000000000 Binary files a/app/src/main/res/drawable-xhdpi/content_remove_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/default_cover.png b/app/src/main/res/drawable-xhdpi/default_cover.png deleted file mode 100644 index c2f4578f9..000000000 Binary files a/app/src/main/res/drawable-xhdpi/default_cover.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/default_cover_dark.png b/app/src/main/res/drawable-xhdpi/default_cover_dark.png deleted file mode 100755 index 3f93e4f65..000000000 Binary files a/app/src/main/res/drawable-xhdpi/default_cover_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/device_access_time.png b/app/src/main/res/drawable-xhdpi/device_access_time.png deleted file mode 100644 index 2beae08c3..000000000 Binary files a/app/src/main/res/drawable-xhdpi/device_access_time.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/device_access_time_dark.png b/app/src/main/res/drawable-xhdpi/device_access_time_dark.png deleted file mode 100755 index c8771db97..000000000 Binary files a/app/src/main/res/drawable-xhdpi/device_access_time_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_action_overflow.png b/app/src/main/res/drawable-xhdpi/ic_action_overflow.png deleted file mode 100644 index 7ba4e10ea..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_action_overflow.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_action_overflow_dark.png b/app/src/main/res/drawable-xhdpi/ic_action_overflow_dark.png deleted file mode 100644 index 5d8af5d63..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_action_overflow_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_action_pause_over_video.png b/app/src/main/res/drawable-xhdpi/ic_action_pause_over_video.png deleted file mode 100755 index b0777a023..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_action_pause_over_video.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_action_play_over_video.png b/app/src/main/res/drawable-xhdpi/ic_action_play_over_video.png deleted file mode 100755 index 24331a48c..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_action_play_over_video.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_drag_handle.png b/app/src/main/res/drawable-xhdpi/ic_drag_handle.png deleted file mode 100755 index 5bdcac342..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_drag_handle.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_drag_handle_dark.png b/app/src/main/res/drawable-xhdpi/ic_drag_handle_dark.png deleted file mode 100755 index d341c7c82..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_drag_handle_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_drawer.png b/app/src/main/res/drawable-xhdpi/ic_drawer.png deleted file mode 100644 index a5fa74def..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_drawer.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_drawer_dark.png b/app/src/main/res/drawable-xhdpi/ic_drawer_dark.png deleted file mode 100644 index bcf49dd73..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_drawer_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_launcher.png b/app/src/main/res/drawable-xhdpi/ic_launcher.png deleted file mode 100644 index 857a1b12e..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_launcher.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_new.png b/app/src/main/res/drawable-xhdpi/ic_new.png deleted file mode 100755 index 447a9398b..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_new.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_new_dark.png b/app/src/main/res/drawable-xhdpi/ic_new_dark.png deleted file mode 100755 index 4a23d309c..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_new_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_stat_antenna.png b/app/src/main/res/drawable-xhdpi/ic_stat_antenna.png deleted file mode 100644 index 50d73271d..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_stat_antenna.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_stat_authentication.png b/app/src/main/res/drawable-xhdpi/ic_stat_authentication.png deleted file mode 100755 index 4adfb636c..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_stat_authentication.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_undobar_undo.png b/app/src/main/res/drawable-xhdpi/ic_undobar_undo.png deleted file mode 100644 index 91c8429ad..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_undobar_undo.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/location_web_site.png b/app/src/main/res/drawable-xhdpi/location_web_site.png deleted file mode 100644 index bd6b8682a..000000000 Binary files a/app/src/main/res/drawable-xhdpi/location_web_site.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/location_web_site_dark.png b/app/src/main/res/drawable-xhdpi/location_web_site_dark.png deleted file mode 100755 index 9b77be967..000000000 Binary files a/app/src/main/res/drawable-xhdpi/location_web_site_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/navigation_accept.png b/app/src/main/res/drawable-xhdpi/navigation_accept.png deleted file mode 100644 index b8915716e..000000000 Binary files a/app/src/main/res/drawable-xhdpi/navigation_accept.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/navigation_accept_dark.png b/app/src/main/res/drawable-xhdpi/navigation_accept_dark.png deleted file mode 100755 index b52dc3701..000000000 Binary files a/app/src/main/res/drawable-xhdpi/navigation_accept_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/navigation_cancel.png b/app/src/main/res/drawable-xhdpi/navigation_cancel.png deleted file mode 100644 index ca7d159fd..000000000 Binary files a/app/src/main/res/drawable-xhdpi/navigation_cancel.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/navigation_cancel_dark.png b/app/src/main/res/drawable-xhdpi/navigation_cancel_dark.png deleted file mode 100755 index f391760ef..000000000 Binary files a/app/src/main/res/drawable-xhdpi/navigation_cancel_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/navigation_chapters.png b/app/src/main/res/drawable-xhdpi/navigation_chapters.png deleted file mode 100755 index d527454c6..000000000 Binary files a/app/src/main/res/drawable-xhdpi/navigation_chapters.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/navigation_chapters_dark.png b/app/src/main/res/drawable-xhdpi/navigation_chapters_dark.png deleted file mode 100755 index e53d5eb16..000000000 Binary files a/app/src/main/res/drawable-xhdpi/navigation_chapters_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/navigation_collapse.png b/app/src/main/res/drawable-xhdpi/navigation_collapse.png deleted file mode 100755 index be6a7688c..000000000 Binary files a/app/src/main/res/drawable-xhdpi/navigation_collapse.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/navigation_collapse_dark.png b/app/src/main/res/drawable-xhdpi/navigation_collapse_dark.png deleted file mode 100755 index 2ed325108..000000000 Binary files a/app/src/main/res/drawable-xhdpi/navigation_collapse_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/navigation_expand.png b/app/src/main/res/drawable-xhdpi/navigation_expand.png deleted file mode 100644 index 53c013b09..000000000 Binary files a/app/src/main/res/drawable-xhdpi/navigation_expand.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/navigation_expand_dark.png b/app/src/main/res/drawable-xhdpi/navigation_expand_dark.png deleted file mode 100755 index 38c7b20d7..000000000 Binary files a/app/src/main/res/drawable-xhdpi/navigation_expand_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/navigation_refresh.png b/app/src/main/res/drawable-xhdpi/navigation_refresh.png deleted file mode 100644 index e6212cf67..000000000 Binary files a/app/src/main/res/drawable-xhdpi/navigation_refresh.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/navigation_refresh_dark.png b/app/src/main/res/drawable-xhdpi/navigation_refresh_dark.png deleted file mode 100755 index a7fdc0dfc..000000000 Binary files a/app/src/main/res/drawable-xhdpi/navigation_refresh_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/navigation_shownotes.png b/app/src/main/res/drawable-xhdpi/navigation_shownotes.png deleted file mode 100755 index a0a156a94..000000000 Binary files a/app/src/main/res/drawable-xhdpi/navigation_shownotes.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/navigation_shownotes_dark.png b/app/src/main/res/drawable-xhdpi/navigation_shownotes_dark.png deleted file mode 100755 index 95708234a..000000000 Binary files a/app/src/main/res/drawable-xhdpi/navigation_shownotes_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/navigation_up.png b/app/src/main/res/drawable-xhdpi/navigation_up.png deleted file mode 100755 index f8c3e6f75..000000000 Binary files a/app/src/main/res/drawable-xhdpi/navigation_up.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/navigation_up_dark.png b/app/src/main/res/drawable-xhdpi/navigation_up_dark.png deleted file mode 100755 index 6964e069b..000000000 Binary files a/app/src/main/res/drawable-xhdpi/navigation_up_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/social_share.png b/app/src/main/res/drawable-xhdpi/social_share.png deleted file mode 100644 index cdafd8abc..000000000 Binary files a/app/src/main/res/drawable-xhdpi/social_share.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/social_share_dark.png b/app/src/main/res/drawable-xhdpi/social_share_dark.png deleted file mode 100755 index 15549b04e..000000000 Binary files a/app/src/main/res/drawable-xhdpi/social_share_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/spinner_button.9.png b/app/src/main/res/drawable-xhdpi/spinner_button.9.png deleted file mode 100644 index 3dc481e54..000000000 Binary files a/app/src/main/res/drawable-xhdpi/spinner_button.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/spinner_button_dark.9.png b/app/src/main/res/drawable-xhdpi/spinner_button_dark.9.png deleted file mode 100644 index c43293d5c..000000000 Binary files a/app/src/main/res/drawable-xhdpi/spinner_button_dark.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/stat_playlist.png b/app/src/main/res/drawable-xhdpi/stat_playlist.png deleted file mode 100644 index 7977e6f2a..000000000 Binary files a/app/src/main/res/drawable-xhdpi/stat_playlist.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/stat_playlist_dark.png b/app/src/main/res/drawable-xhdpi/stat_playlist_dark.png deleted file mode 100644 index f32dd3780..000000000 Binary files a/app/src/main/res/drawable-xhdpi/stat_playlist_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/type_audio.png b/app/src/main/res/drawable-xhdpi/type_audio.png deleted file mode 100644 index 777fab84e..000000000 Binary files a/app/src/main/res/drawable-xhdpi/type_audio.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/type_audio_dark.png b/app/src/main/res/drawable-xhdpi/type_audio_dark.png deleted file mode 100755 index dfd2b33c7..000000000 Binary files a/app/src/main/res/drawable-xhdpi/type_audio_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/type_video.png b/app/src/main/res/drawable-xhdpi/type_video.png deleted file mode 100644 index bbd1f112f..000000000 Binary files a/app/src/main/res/drawable-xhdpi/type_video.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/type_video_dark.png b/app/src/main/res/drawable-xhdpi/type_video_dark.png deleted file mode 100755 index a74947459..000000000 Binary files a/app/src/main/res/drawable-xhdpi/type_video_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/undobar.9.png b/app/src/main/res/drawable-xhdpi/undobar.9.png deleted file mode 100644 index 22fa2205b..000000000 Binary files a/app/src/main/res/drawable-xhdpi/undobar.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/undobar_button_focused.9.png b/app/src/main/res/drawable-xhdpi/undobar_button_focused.9.png deleted file mode 100644 index d284ca7cb..000000000 Binary files a/app/src/main/res/drawable-xhdpi/undobar_button_focused.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/undobar_button_pressed.9.png b/app/src/main/res/drawable-xhdpi/undobar_button_pressed.9.png deleted file mode 100644 index e990659f0..000000000 Binary files a/app/src/main/res/drawable-xhdpi/undobar_button_pressed.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/undobar_divider.9.png b/app/src/main/res/drawable-xhdpi/undobar_divider.9.png deleted file mode 100644 index 1b067d4e7..000000000 Binary files a/app/src/main/res/drawable-xhdpi/undobar_divider.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_overflow.png b/app/src/main/res/drawable-xxhdpi/ic_action_overflow.png deleted file mode 100644 index 5a603b6bc..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_action_overflow.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_overflow_dark.png b/app/src/main/res/drawable-xxhdpi/ic_action_overflow_dark.png deleted file mode 100644 index e22049b1e..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_action_overflow_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_pause_over_video.png b/app/src/main/res/drawable-xxhdpi/ic_action_pause_over_video.png deleted file mode 100755 index fa85601cf..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_action_pause_over_video.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_play_over_video.png b/app/src/main/res/drawable-xxhdpi/ic_action_play_over_video.png deleted file mode 100755 index 121be211e..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_action_play_over_video.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_drag_handle.png b/app/src/main/res/drawable-xxhdpi/ic_drag_handle.png deleted file mode 100755 index f834699c6..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_drag_handle.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_drag_handle_dark.png b/app/src/main/res/drawable-xxhdpi/ic_drag_handle_dark.png deleted file mode 100755 index a9408bc9d..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_drag_handle_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_drawer.png b/app/src/main/res/drawable-xxhdpi/ic_drawer.png deleted file mode 100644 index 9c4685d6e..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_drawer.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_drawer_dark.png b/app/src/main/res/drawable-xxhdpi/ic_drawer_dark.png deleted file mode 100644 index f7e3b3079..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_drawer_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_launcher.png b/app/src/main/res/drawable-xxhdpi/ic_launcher.png deleted file mode 100644 index 2bef52ec7..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_launcher.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_new.png b/app/src/main/res/drawable-xxhdpi/ic_new.png deleted file mode 100755 index 5e836eae4..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_new.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_new_dark.png b/app/src/main/res/drawable-xxhdpi/ic_new_dark.png deleted file mode 100755 index bca96b751..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_new_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_stat_authentication.png b/app/src/main/res/drawable-xxhdpi/ic_stat_authentication.png deleted file mode 100755 index b274bb60f..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_stat_authentication.png and /dev/null differ diff --git a/app/src/main/res/drawable/badge.xml b/app/src/main/res/drawable/badge.xml deleted file mode 100644 index f98384cb9..000000000 --- a/app/src/main/res/drawable/badge.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/borderless_button.xml b/app/src/main/res/drawable/borderless_button.xml deleted file mode 100644 index 27d723eed..000000000 --- a/app/src/main/res/drawable/borderless_button.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/borderless_button_dark.xml b/app/src/main/res/drawable/borderless_button_dark.xml deleted file mode 100644 index 6d263938d..000000000 --- a/app/src/main/res/drawable/borderless_button_dark.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/horizontal_divider.9.png b/app/src/main/res/drawable/horizontal_divider.9.png deleted file mode 100644 index 7db0549da..000000000 Binary files a/app/src/main/res/drawable/horizontal_divider.9.png and /dev/null differ diff --git a/app/src/main/res/drawable/overlay_button_circle_background.xml b/app/src/main/res/drawable/overlay_button_circle_background.xml deleted file mode 100644 index 90c51472c..000000000 --- a/app/src/main/res/drawable/overlay_button_circle_background.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/overlay_drawable.xml b/app/src/main/res/drawable/overlay_drawable.xml deleted file mode 100644 index 185ffefc1..000000000 --- a/app/src/main/res/drawable/overlay_drawable.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/overlay_drawable_dark.xml b/app/src/main/res/drawable/overlay_drawable_dark.xml deleted file mode 100644 index fb78f5633..000000000 --- a/app/src/main/res/drawable/overlay_drawable_dark.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/type_audio.png b/app/src/main/res/drawable/type_audio.png deleted file mode 100644 index 4ec9efd97..000000000 Binary files a/app/src/main/res/drawable/type_audio.png and /dev/null differ diff --git a/app/src/main/res/drawable/type_video.png b/app/src/main/res/drawable/type_video.png deleted file mode 100644 index a2722b812..000000000 Binary files a/app/src/main/res/drawable/type_video.png and /dev/null differ diff --git a/app/src/main/res/drawable/undobar_button.xml b/app/src/main/res/drawable/undobar_button.xml deleted file mode 100644 index a4de91b49..000000000 --- a/app/src/main/res/drawable/undobar_button.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/vertical_divider.9.png b/app/src/main/res/drawable/vertical_divider.9.png deleted file mode 100644 index 6a0edafb3..000000000 Binary files a/app/src/main/res/drawable/vertical_divider.9.png and /dev/null differ diff --git a/app/src/main/res/drawable/white_circle.xml b/app/src/main/res/drawable/white_circle.xml deleted file mode 100644 index 597b70a2d..000000000 --- a/app/src/main/res/drawable/white_circle.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/values-az/strings.xml b/app/src/main/res/values-az/strings.xml deleted file mode 100644 index adb983e9e..000000000 --- a/app/src/main/res/values-az/strings.xml +++ /dev/null @@ -1,217 +0,0 @@ - - - - AntennaPod - Kanallar - PODKASTLAR - EPIZODLAR - Yeni - Gözləmədə - Parametrlər - Yükləmələr - Yükləməyi ləğv et - Oynatma tarixiçəsi - gpodder.net - - - - Brauzerdə aç - URLı kopiyala - URLı paylaş - URL buferə köçürüldü - - Tarixiçəni sildir - - Oldu - Ləğv et - Müəlif - Dil - Parametrlər - Xəta - Xəta baş verdi: - Təzələ - Heç bir yaddaş cihazı tapılmadı. - Fəsillər - Təsvir - Ən yeni epizod:\u0020 - \u0020epizod - Müddət:\u0020 - Ölçü:\u0020 - Hazırlaşma - Yükləmə... - Bağla - - Kanalın URLı - - Hamısını oxunmuş kimi işarələ - Məlumatı göstər - Web-səhifəyi paylaş - Kanalı paylaş - Bütün kanallar və epizodlar silinəçək. - - Yüklə - Oynat - Pauza - İnternetən yayimla - Sil - Oxumuş kimi işarələ - Oxunmamış kimi işarələ - Növbəyə əlavə et - Növbədən sil - Web-səhifəsini aç - Flattrla - Hamsını növbəyə əlavə et - Hamısını yüklə - Epizodu burax - - Yükləmə gözlənir - Yükləmə gedir - Yaddaş cihazı tapılmadı - Yaddaş çatmır - Fayl xətası - HTTP protokolnun xətası - Naməlum xəta - Parserin xətası - Naməlum kanal növü - Əlaqə xətasi - Naməlum xost - Yükləmələrin hamısını ləğv et - Yükləmə ləğv olundu - Yükləmə başa çatdı - Yanlış URL - IO xətasi - Tələbin xətası - \u0020yükləmə galdı - Podkast məlumatların yüklənişi - %1$d yükləmə uğurludur, %2$d uğursuzdur - Naməlum başliğ - Kanal - Mediya fayl - Şəkil - Fayl yükləmə xətası:\u0020 - - Xəta! - Heç nə oynadılmır - Hazırlanır - Hazır - Axtarış - Server iştəmir - Naməlum xəta - Heç nə oynadılmır - 00:00:00 - Buferləşmə - Podkast oynadılır - - Növbəyi sil - Qaytar - Element silindi - - Flattra gir - Girmə prosesini başlamaq üçün düyməyi basın. Flattrın giriş səhifəsinə aparılacağsınız. - Gir - Baş ekrana dön - Giriş uğurludur! İndi tətbiqlədən Flattrla istifadə edə bilərsiniz. - Heç bir Flattr tokeni tapılmadı - Olsun ki sizin Flattr hesabınız AntennaPod\'a qoşulmadı. Yenə Flattra girin ya da podkastın səhifəsinə keçin. - Gir - Əməliyyat qadağan olundu - Bu əməliyyat üçün AntennaPod\'un icazəsi yoxdur. AntennaPod\'un keçid tokenin ləğv olunması bunun səbəbi ola bilər. Yenə Flattra girin ya da podkastın səhifəsinə keçin. - Keçid ləğv olundu - AntennaPod\'un keçid tokeni uğurlu ləğv olundu. - - - Plagin yüklə - Plagin yüklü deyil - - Siyahıda heç nə yoxdur - Hələ heç bir kanala yazilmadınız - - Başqa - Proqram haqqinda - Növbə - Flattr - Qulaqliqı ayiranda oynatma dayanacağ - Oynatma başa çatanda növbədə irəlidəki epizodu oynat - Oynatma - Şəbəkə - Təzələmə intervalı - Kanalın avtomatik təzələməsinin intervalını seç ya da keçir onu - Təkçə Wi-Fi vasitəsiilə yüklə - Fasiləsiz oynatma - Wi-Fi vasitəsiilə yükləmə - Qulaqliqı ayır - Mobil şəbəbkə vasitəsiilə təzələmə - Mobil şəbəbkə vasitəsiilə təzələməyə icazə vermək - Təzələmə - Flattr parametrləri - Flattra gir - Flattr\'la istifadə etmək üçün, öz Flattr hesabınıza girin - Bu proqramı flattrla - Flattr vasitəsiilə AntennaPodun inkişafını dəstək edin. Sağolun! - Keçidi ləğv ət - Flattr hesabına keçidi ləğv et - İnterfeys - Görüşü seç - AntennaPod\'un görüşünü dəyişdir - Avtomatik yükləmə - Epizodların avtomatik yüklənişinin konfiqurasiyanı dəyiş - Wi-Fi filtr - Seçilən Wi-Fi səbəkələr vasitəsiilə avtomatik yükləməyi icazə ver - Epizod keşi - - Qara - Hədsiz - saat - saat - Əl ilə - - - Kanalları və ya epizodları axtar - Təsvirlərdə tapıldı - Fəsillərdə tapıldı - Heç nə tapılmadı - Axtar - Başlığda tapıldı - - OPML faylın idxalı üçün onu aşağıdakı qovluqa yerləşdirin və idxal prosesini başlamaq üçün düyməyi basın. - İdxalı başla - OPML idxalı - XƏTA! - OPML faylın oxunması - OPML faylını oxuyanda xəta baş verdi: - İdxal qovliqu boşdur. - Hamısını seç - Seçimi ləğv et - İdxal üçün fayl seç - OPML ixraçı - İxrac... - İxracın xətası - OPML ixracı uğurlu keçdi - OPML fayl:\u0020 yazılıb - - Yuxu taymerini qoy - Yuxu taymerini keçir - Vaxtı yaz - Yuxu taymeri - Vaxt galdı:\u0020 - Yanlış yazi. Vaxt təkçə rəqmlərlə yazılır - - TOP PODKASTLAR - - Seçilən qovluq: - Qovluqu yarat - Məlumat qovluqunu seç - \"%1$s\" adlı qovluq yaradılsınmı? - Yeni qovluq yaradıldı - Bu qovluqa yazıla bilinmer - Qovluq artiq var - Qovluq yaradılmadı - Qovluq boş deyil - Seçilən qovluq boş deyil. Mediya yükləmələr və başka fayllar bu qovluqa yazılacaqlar. Necə olsa davam olsunmu? - Başlanğıc qovluqu seç - - Yükləmə... - - - - diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml deleted file mode 100644 index ae2addb05..000000000 --- a/app/src/main/res/values-ca/strings.xml +++ /dev/null @@ -1,341 +0,0 @@ - - - - AntennaPod - Canals - Afegeix podcast - PODCASTS - EPISODIS - Episodis nous - Tots els episodis - Nous - Llista d\'espera - Configuració - Afegeix podcast - Baixades - En execució - Completat - Registre - Cancel·la la baixada - Historial de reproducció - gpodder.net - Inici de sessió a gpodder.net - - Publicats recentment - Mostra només els episodis nous - - Obre menú - Tanca menú - - Obre en un navegador - Copia l\'enllaç - Comparteix l\'enllaç - S\'ha copiat l\'enllaç al porta-retalls. - Vés a aquesta posició - - Esborra l\'historial - - D\'acord - Cancel·la - Autor - Llengua - Configuració - Imatge - Error - S\'ha produït un error: - Actualitza - L\'emmagatzemament extern no està disponible. Assegureu-vos que està muntat per què l\'aplicació funcioni correctament. - Capítols - Notes del programa - Descripció - Episodi més recent: \u0020 - \u0020episodis - Durada:\u0020 - Mida:\u0020 - S\'està processant - S\'està carregant... - Desa nom d\'usuari i contrasenya - Tanca - Reintenta - Inclou a baixades automàtiques - - Enllaç del canal - URL, canal o lloc web - Afegeix podcast amb l\'URL - Cerca podcast al directori - Podeu cercar nous podcasts al directori de gpodder.net mitjançant el seu nom, categoria o popularitat. - Navega gpodder.net - - Marca-ho tot com a llegit - S\'han marcat tots els episodis com a llegits - Mostra informació - Esborra podcast - Comparteix l\'enllaç de la plana - Comparteix l\'enllaç del canal - Confirmeu que, efectivament, voleu suprimir aquest canal i tots els episodis que us n\'heu baixat. - S\'està esborrant el canal - - Baixa - Reprodueix - Pausa - Reprodueix sense baixar - Suprimeix - Esborra episodi - Marca com a llegit - Marca com a pendent - Afegeix a la cua - Suprimeix de la cua - Visita el lloc web - Comparteix amb Flattr - Posa-ho tot a la cua - Baixa-ho tot - Omet l\'episodi - - ha funcionat - ha fallat - Baixada pendent - Baixada en procés - No s\'ha trobat cap dispositiu d\'emmagatzemament - No hi ha prou espai - Error de fitxer - Error de dades HTTP - Error desconegut - Error de l\'analitzador - Tipus de canal no suportat - Error de connexió - Amfitrió desconegut - Error d\'autenticació - Cancel·la totes les baixades - S\'ha cancel·lat la baixada - Baixades completades - URL mal formatada - Error d\'E/S - Error de petició - Error d\'accés a la base de dades - \u0020Baixades pendents - S\'estan processant les baixades - S\'estan baixant les dades del podcast - %1$d baixades finalitzades, %2$d fallides - Títol desconegut - Canal - Fitxer - Imatge - S\'ha produït un error en intentar baixar el fitxer:\u0020 - Cal autenticar-se - Es necessita un usuari i una contrasenya per accedir al recurs - - Error - No s\'està reproduint res - S\'està preparant - Preparat - S\'està cercant - El servidor no està operatiu - Error desconegut - No s\'està reproduint res - 00:00:00 - S\'està carregant - Podcast en reproducció - AntennaPod - Control desconegut: %1$d - - Buida la cua - Desfés - Ítem esborrat - Mou al principi - Mou al final - - Inici de sessió a Flattr - Premeu el botó per iniciar el procés d\'autenticació. Quan s\'obri la pantalla d\'inici de sessió de Flattr al vostre navegador, introduïu les vostres credencials i concediu a AntennaPod els permisos de compartir mitjançant Flattr. En finalitzar el procés, tornareu automàticament a aquesta pantalla. - Autenticació - Torna a l\'inici - L\'autenticació ha acabat correctament. Ja podeu compartir amb Flattr des de l\'aplicació. - No s\'ha trobat cap testimoni Flattr - Sembla que el compte flattr no està vinculat amb AntennaPod. Toqueu aquí per autenticar-vos. - Sembla que el vostre compte de Flattr no està vinculat amb AntennaPod. Podeu connectar el vostre compte Flattr amb AntennaPod per a compartir continguts des de l\'aplicació, o bé accediu a la plana web de Flattr i compartiu els continguts des d\'allà. - Autentica - L\'acció no és permesa - AntennaPod no té permisos per executar aquesta acció. És possible que el testimoni d\'accés de Flattr per a AntennaPod hagi estat revocat. Podeu tornar-vos a autenticar amb el servei de Flattr, o podeu visitar el web del contingut directament. - L\'accés ha estat revocat - El testimoni d\'accés a Flattr de l\'AntennaPod s\'ha revocat correctament. Per completar el procés, heu de suprimir aquesta aplicació de la llista d\'aplicacions aprovades que trobareu a l\'apartat de configuració del compte de la plana web de Flattr. - - S\'ha compartit una cosa per Flattr! - S\'han compartit %d coses per Flattr! - Compartit per Flattr: %s. - No s\'han pogut compartir %d coses per Flattr! - No s\'ha compartit per Flattr: %s. - Es compartirà per Flattr després - %s s\'està compartint per Flattr - AntennaPod està compartint per Flattr - AntennaPod ha compartit per Flattr - AntennaPod no ha pogut compartir per Flattr - S\'estan recuperant les coses compartides per Flattr - - Baixa el connector - Connector no instal·lat - Per a què funcioni la velocitat de reproducció variable, cal instal·lar una biblioteca addicional.\n\nFeu un toc a «Baixa el connector» per baixar-vos el connector gratuït des de la Play Store.\n\nQualsevol problema que sorgeixi en utilitzar aquest connector no és culpa de l\'AntennaPod. Cal informar-ne, doncs, al propietari del connector. - Velocitats de reproducció - - No hi ha elements a la llista. - No us heu subscrit a cap canal. - - Altres - Quant a - Cua - Serveis - Flattr - Pausa la reproducció en desconnectar els auriculars. - Salta al següent element de la cua en acabar la reproducció - Reproducció - Xarxa - Interval d\'actualització - Especifiqueu l\'interval en què els canals s\'actualitzen de forma automàtica, o deshabiliteu la funcionalitat. - Només baixa fitxers a través d\'una xarxa sense fils - Reproducció continuada - Baixa a través de xarxes sense fils - Desconnexió d\'auriculars - Actualitzacions sobre xarxes mòbils - Permet actualitzacions a través de xarxes mòbils. - S\'està actualitzant - Configuració de Flattr - Inici de sessió Flattr - Inicieu sessió al vostre compte Flattr per compartir continguts directament des de l\'aplicació. - Compartiu aquesta aplicació amb Flattr - Doneu suport al desenvolupament d\'AntennaPod compartint l\'aplicació a través de Flattr. Gràcies! - Revoca l\'accés - Revoqueu el permís d\'accés d\'aquesta aplicació al vostre compte Flattr. - Flattr automàtic - Configura la compartició automàtica per Flattr - Interfície d\'usuari - Selecció de tema - Canvieu l\'aparença d\'AntennaPod. - Baixada automàtica - Configureu la baixada automàtica d\'episodis. - Activa el filtre de la xarxa sense fils - Permet les baixades automàtiques només per a les xarxes sense fils seleccionades. - Memòria d\'episodis - Clar - Fosc - Sense límits - hores - hora - Manual - Inici de sessió - Inicieu sessió a gpodder.net per tal de sincronitzar les vostres subscripcions. - Surt - Heu sortit de la sessió - Dades d\'inici de sessió - Canvia les dades d\'inici de sessió del vostre compte de gpodder.net - Velocitats de reproducció - Personalitzeu les velocitats disponibles per a una velocitat de reproducció d\'àudio variable - Salta a l\'instant - Salta aquesta quantitat de segons en rebobinar o en avançar ràpidament - Definex nom del servidor - Utilitza el servidor per defecte - - Activa la compartició automàtica per Flattr - Comparteix per Flattr l\'episodi en haver-ne reproduït el %d per cent - Comparteix per Flattr l\'episodi en haver-ne iniciat la reproducció - Comparteix per Flattr l\'episodi en acabar-se\'n la reproducció - - Cerca canals o episodis - Trobat a notes del programa - Trobat als capítols - No s\'ha trobat cap resultat - Cerca - Trobat al títol - - Els fitxers OPML us permeten moure els podcasts d\'un gestor de podcasts a un altre. - Per importar un fitxer OPML, ubiqueu-lo al següent directori i premeu el botó de sota per iniciar el procés. - Inicia la importació - Importació OPML - Error! - S\'està llegint el fitxer OPML - S\'ha produït un error en llegir el document OPML: - El directori d\'importacions és buit. - Selecciona-ho tot - Deselecciona-ho tot - Seleccioneu el fitxer a importar - Exportació OPML - S\'està exportant... - Error d\'exportació - S\'ha exportat l\'OPML correctament. - El fitxer OPML s\'ha escrit a:\u0020 - - Defineix un temporitzador - Desactiva el temporitzador - Introduïu l\'hora - Temporitzador - Temps restant:\u0020 - L\'entrada no és vàlida, ja que el temps ha de ser un nombre i no ho és - segons - minuts - hores - - CATEGORIES - TOP PODCASTS - SUGGERÈNCIES - Cerca a gpodder.net - Inici de sessió - Benvingut al procés d\'inici de sessió a gpodder.net. Primerament, introduïu la informació d\'accés: - Entra - Si encara no teniu un compte, creeu-ne un aquí:\nhttps://gpodder.net/register/ - Nom d\'usuari - Contrasenya - Selecció de dispositiu - Per a utilitzar gpodder.net, creeu un nou dispositiu o seleccioneu-ne un d\'existent: - ID de dispositiu:\u0020 - Llegenda - Crea nou dispositiu - Seleccioneu un dispositiu existent: - L\'ID de dispositiu no pot ser buit - L\'ID de dispositiu ja existeix - Selecciona - Heu iniciat la sessió! - Felicitats! El vostre compte de gpodder.net s\'ha enllaçat amb el dispositiu. D\'ara endavant, AntennaPod sincronitzarà automàticament les subscripcions del dispositiu al vostre compte. - Sincronitza ara - Vés a la pantalla principal - Error d\'autenticació a gpodder.net - Nom d\'usuari o contrasenya incorrectes - Error de sincronització a gpodder.net - S\'ha produït un error durant la sincronització:\u0020 - - Carpeta seleccionada: - Crea una carpeta - Selecció de la carpeta de dades - Voleu crear una nova carpeta amb el nom \"%1$s\"? - S\'ha creat la nova carpeta - No es pot escriure dins d\'aquesta carpeta - La carpeta ja existeix - No s\'ha pogut crear la carpeta - La carpeta no és buida - La carpeta que heu seleccionat no és buida. Les baixades i altres fitxers es copiaran directament a aquesta carpeta. Voleu continuar? - Selecciona la carpeta per defecte - Pausa la reproducció en lloc de baixar el volum quan una altra app necessiti reproduir sons - Pausa en interrompre - - Subscriu - Subscrit - S\'està baixant... - - Mostra els capítols - Mostra les notes del programa - Mosta la imatge - Rebobina - Avança ràpidament - Àudio - Vídeo - Navega cap amunt - Més accions - S\'està reproduïnt l\'episodi - S\'està baixant l\'episodi - S\'ha baixat l\'episodi - L\'element és nou - S\'ha afegit l\'episodi a la cua - Nombre d\'episodis nous - Nombre d\'episodis que heu començat a escoltar - Arrossegueu l\'element per canviar-ne la posició - - Autenticació - Canvieu el nom d\'usuari i contrasenya per a aquest podcast i els seus episodis. - - S\'estan important les subscripcions des de les apps de propòsit únic... - diff --git a/app/src/main/res/values-cs-rCZ/strings.xml b/app/src/main/res/values-cs-rCZ/strings.xml deleted file mode 100644 index 8792d1fc9..000000000 --- a/app/src/main/res/values-cs-rCZ/strings.xml +++ /dev/null @@ -1,272 +0,0 @@ - - - - AntennaPod - Zdroje - PODCASTY - EPIZODY - Nový - Seznam nepřečtených - Nastavení - Přidat podcast - Stahování - Zrušit stahování - Historie přehrávání - gpodder.net - gpodder.net uživatel - - - - Otevřít v prohlížeči - Kopírovat URL - Sdílet URL - URL zkopírováno do schránky. - - Vymazat historii - - Potvrdit - Zrušit - Autor - Jazyk - Nastavení - Chyba - Nastala chyba: - Obnovit - Není dostupné žádné externí uložiště. Pro správnou funkci aplikace se prosím ujistěte, že je připojeno externí úložiště. - Kapitoly - Poznámky - Popis - Poslední epizoda:\u0020 - \u0020epizod - Délka:\u0020 - Velikost:\u0020 - Zpracovávám - Načítám... - Uložit uživatelské jméno a heslo - Zavřít - Zkusit znovu - Zahrnout do automaticky stahovaných - - URL zdroje - Přidat podcast pomocí URL - - Označit vše jako přečtené - Informace o zdroji - Sdílet odkaz - Sdílet adresu zdroje - Prosím potvrďte, že chcete smazat tento zdroj včetně všech stažených epizod. - Odstranit feed - - Stáhnout - Přehrát - Pozastavit - Streamovat - Odstranit - Označit jako přečtené - Označit jako nepřečtené - Přidat do fronty - Odebrat z fronty - Navštívit stránku - Flattr - Vše do fronty - Stáhnout vše - Přeskočit epizodu - - Čekající na stažení - Probíhající stahování - Úložné zařízení nenalezeno - Nedostatek volného místa - Souborová chyba - HTTP chyba - Neznámá chyba - Výjimka parseru - Nepodporovaný typ zdroje - Chyba spojení - Neznámý host - Zrušit všechna stahování - Stahování zrušeno - Všechna stahování dokončena - Chybné URL - IO chyba - Chyba požadavku - Chyba přístupu do databáze - \u0020Stahování zbývá - Stahuji podcast data - %1$d úspěšných stahování, %2$d selhalo - Neznámý název - Zdroj - Soubor - Obrázek - Nastala chyba při pokusu o stažení souboru:\u0020 - - Chyba! - Žádné probíhající přehrávání - Připravuji - Připraven - Přetáčím - Server nereaguje - Neznámá chyba - Žádné probíhající přehrávání - 00:00:00 - Načítání - Přehrávaný podcast - - Vyprázdnit frontu - Zpět - Položka odebrána - Přejít na začátek - Přejít na konec - - Flattr přihlášení - Stiskněte následující tlačítko pro spuštění autentizačního procesu. Budete přesměrováni na přihlašovací obrazovku flattru a vyzváni k potvrzení udělení práv pro použití flattru aplikací AntennaPod. Po udělení práv se automaticky vrátíte na tuto obrazovku. - Přihlásit - Návrat domů - Úspěšně přihlášen. Nyní můžete využít flattru přímo v aplikaci. - Nenalezen Flattr token - Váš flattr učet není napojen do AntenaPodu. Můžete buďto napojit váš flattr účet do AntennaPodu a využít flattru přímo v aplikaci a nebo použít flattr přímo na webových stránkách zdroje v prohlížeči. - Přihlásit - Akce zakázána - AntennaPod nemá oprávnění pro tuto akci. Důvodem může být revokování přístupového tokenu AntennaPodu k vašemu účtu. Přístup můžete obnovit nebo využít prohlížeče k návštěvě stránky zdroje. - Přístup revokován - Úspěšně revokován přístup AntennPodu k vašemu účtu. Pro dokončení tohoto procesu je ještě zapotřebí na stránkách flattru odebrat z vašeho účtu AntennaPod ze seznamu povolených aplikací. - - - Stáhnout Plugin - Plugin není nainstalován - Pro nastavení rychlosti přehrávání musí být nainstalovaná knihovna třetí strany.\n\nKlikněte na \"Stáhnout Plugin\" ke stažení pluginu z Play Store.\n\nAntennaPod nenese žádnou odpovědnost, za jakékoliv problémy, způsobené tímto pluginem. - Rychlosti přehrávání - - Žádné položky v seznamu. - Zatím nebyly přidány žádné zdroje. - - Ostatní - O aplikaci - Fronta - Služby - Flattr - Při odpojení sluchátek automaticky pozastavit přehrávání. - Po přehrání položky z fronty přejít automaticky na další. - Přehrávání - Síť - Interval aktualizace zdrojů - Udává interval, ve kterém se zdroje automaticky aktualizují nebo tuto funkci deaktivuje. - Stahovat soubory pouze pomocí WiFi - Kontinuální přehrávání - WiFi stahování - Odpojení sluchátek - Mobilní aktualizace - Povolit aktualizace pomocí mobilního připojení. - Obnovuji - Nastavení Flattr - Flattr přihlášení - Přihlásit se k flattr účtu a umožnit flattrování přímo z aplikace. - Flattrovat tuto aplikaci - Podpořit vývoj AntennaPodu na flatteru. Děkujeme! - Odebrat přístup - Odebere aplikaci přístupová práva k vašemu flattr účtu. - Uživatelské rozhraní - Vybrat motiv - Změnit vzhled AntennaPod. - Automatické stahování - Nastavení automatického stahování epizod. - Zapnout Wi-Fi filtr - Povolit automatické stahování pouze pomocí vybraných Wi-Fi sítí. - Historie epizod - Světlý - Tmavý - Bez omezení - hodin - hodina - Ručně - Přihlásit - Přihlašte se pomocí vašeho gpodder.net účtu pro synchronizaci odebíraných podcastů. - Odhlásit - Úspěšně odhlášeno - Změna přihlašovacích údajů - Změní přihlašovací údaje k vašemu gpodder.net účtu. - Rychlosti přehrávání - Přizpůsobení rychlosti je dostupné pro přehrávání zvuku různými rychlostmi - Nastavit hostname - Použít přednastaveného hosta - - - Hledat zdroje a epizody - Nalezeno v poznámkách k show - Nalezeno v kapitolách - Žádné výsledky - Vyhledat - Nalezeno v názvu - - OPML soubory umožňují přenést vaše podcasty z jednoho podcast manažera do jiného. - Pro import OPML souboru je třeba ho nejdříve umístit do následujícího adresáře a poté pro zahájení procesu importu stisknout tlačítko. - Importovat - OPML import - CHYBA! - Načítání OPML souboru - Nastala chyba při čtení OPML souboru: - Adresář importu je prázdný. - Označit vše - Zrušit výběr - Vyberte soubor k importování - OPML export - Exportuji... - Chyba exportu - OPML export byl úspěšný. - OPML soubor byl zapsán do:\u0020 - - Nastavit časovač vypnutí - Deaktivovat časovač vypnutí - Zadejte čas - Časovač vypnutí - Zbývá času:\u0020 - Neplatný vstup, musí být zadáno celé číslo - - KATEGORIE - TOP PODCASTY - DOPOTUČENÉ - Vyhledat na gpodder.net - Přihlásit - Vítejte do průvodce přihlášením ke gpodder.net účtu. Zadejte vaše přihlašovací údaje: - Přihlásit - Jestliže nemáte účet, můžete si ho vytvořit zde:\nhttps://gpodder.net/register/ - Uživatleské jméno - Heslo - Výběr zařízení - Vytvořte nové nebo vyberte již existující zařízení pro použití s vašim gpodder.net účtem. - ID zařízení:\u0020 - Nadpis - Vytvořit nové zařízení - Vybrat existující zařízení: - ID zařízení nesmí být prázdné - ID zařízení je již obsazeno - Vybrat - Úspěšně přihlášeno! - Gratulujeme! Váš gpodder.net účet je nyná úspěšně propojen s vašim zařízením. AntennaPod bude automaticky synchronizovat odebírané podcasty s vaším gpodder.net účtem. - Synchronizovat nyní - Přejít na hlavní obrazovku - gpodder.net autentizace selhala - Špatné přihlašovací jméno nebo heslo - gpodder.net synchronizace selhala - V průběhu synchronizace nastala chyba:\u0020 - - Vybraný adresář: - Vytvořit adresář - Vybrat umístění dat - Vytvořit adresář \"%1$s\"? - Nový adresář vytvořen - Nelze zapisovat do adresáře - Adresář již existuje - Nelze vytvořit adresář - Adresář není prázdný - Vybraný adresář není prázdný. Stažená media a ostatní soubory budou umístěny přímo do tohoto adresáře. Přesto pokračovat? - Vybrat hlavní adresář - Místo snížení hlasitosti pozastavit přehrávání v případě, že jiná aplikace přehrává zvuk. - Automatické pozastavení přehrávání - - Odebírat - Odebíráno - Stahuji... - - - - diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml deleted file mode 100644 index 5c41b6eb0..000000000 --- a/app/src/main/res/values-da/strings.xml +++ /dev/null @@ -1,329 +0,0 @@ - - - - AntennaPod - Feeds - PODCASTS - EPISODER - Nye episoder - Alle episoder - Nye - Venteliste - Indstillinger - Tilføj podcast - Downloads - Kører - Fuldført - Log - Annuller Download - Afspilnings historik - gpodder.net - gpodder.net login - - Nyligt udgivet - Hvis kun nye episoder - - Åben menu - Luk menu - - Åben i browser - Kopier URL - Del URL - URL kopieret til udklipsholderen. - - Fjern historik - - Bekræft - Annuller - Forfatter - Sprog - Indstillinger - Billed - Fejl - En fejl er opstået: - Opdater - Ingen ekstern harddisk er tilgængelig. Vær venlig at sørge for at den eksterne hukommelse er monteret så app\'en kan fungere korrekt. - Kapitler - Afsnitsnoter - Beskrivelse - Seneste episoder:\u0020 - \u0020episoder - Længde:\u0020 - Størrelse:\u0020 - Behandler - Indlæser... - Gem brugernavn og kodeord - Luk - Prøv igen - Inkluder i automatiske downloads - - Feed URL - Tilføj Podcast med URL - Find podcast i mappen - Du kan søge efter nye podcasts efter navn, kategori eller popularitet i gpodder.net biblioteket - Gennemse gpodder.net - - Marker alle som læst - Marker alle episoder som læst - Vis information - Fjern podcast - Del webside link - Del feed link - Bekræft venligst at du vil fjerne dette feed og ALLE episoder du har downloadet fra dette feed. - Fjerner feed - - Hent - Afspil - Pause - Stream - Fjern - Fjern episode - Marker som læst - Marker som ulæst - Tilføj til kø - Fjern fra kø - Besøg webside - Flattr dette - Sæt alle i kø - Download alle - Spring episode over - - succesfuld - fejlet - Download afventer - Download kører - Kan ikke finde lager-enhed - Ikke nok plads - Fil fejl - HTTP data fejl - Ukendt fejl - Parser undtagelse - Feed type er ikke understøttet - Forbindelsesfejl - Ukendt vært - Godkendelses fejl - Annuller alle downloads - Download afbrudt - Downloads afsluttet - Misdannet URL - IO fejl - Anmode fejl - Adgangsfejl i database - \u0020Downloads tilbage - Bearbejder downloads - Downloader podcast data - %1$d downloads lykkedes, %2$d fejlet - Ukendt titel - Feed - Medie fil - Billede - En fejl opstod under download af filen:\u0020 - Godkendelses krævet - Den ressource du efterspurgte kræver et brugernavn og et password - - Fejl! - Ingen medier afspiller - Forbereder - Klar - Søger - Server døde - Ukendt fejl - Ingen medier afspiller - 00:00:00 - Buffering - Afspiller podcast - - Fjern kø - Fortryd - Emne slettet - Flyt til toppen - Flyt til bunden - - Flattr log ind - Tryk på knappen nedenfor for at starte godkendelsesprocessen. Du vil blive ført til flattr log ind siden i din browser og bedt om at give AntennaPod tilladelse til at flattr emner. Efter at du har givet tilladelsen vil du automatisk vende tilbage til denne side. - Godkender - Retuner hjem - Godkendelse lykkedes! Du kan nu flattr emner inde i app\'en. - Ingen flattr polet fundet - Din flattr konto er vidst ikke forbundet til AntennaPod. Du kan forbinde din konto til AntennaPod for at flattr emner inde i app\'en, eller besøge websiden af mediet for at flattr det der. - Godkender - Forbudt handling - AntennaPod har ikke tilladelse til denne handling. Årsagen kunne være at adgangspoletten for AntennaPod til din konto er blevet tilbagekaldt. Du kan enten godkende den igen eller besøge websiden for mediet istedet. - Adgang tilbagekaldt - Du har succesfuldt tilbagekaldt AntennaPods adgangs polet til din konto. For at fuldføre processen skal du fjerne denne app fra listen af godkendte applikationer i din kontos indstillinger på flattr\'s hjemmeside. - - Flattr\'et en ting! - Flattr\'et %d ting! - Flattr\'et: %s. - Det mislykkedes at flattr %d ting! - Ikke flattr\'et: %s. - Ting vil blive flattr\'et senere - Flattr\'er %s - AntennaPod flatttr\'er - AntennaPod har flattr\'et - AntennaPod flattr mislykkedes - Hent flatt\'rede ting - - Hent Plugin - Plugin er ikke installeret - For at få variabel afspilningshastighed til at virke skal der installeres et tredjepartsprogram.\n\nTryk \'Download Plugin\' for at downloade et gratis plugin fra Play Store\n\nAlle problemer forårsaget ved at bruge dette plugin er ikke AntennaPods ansvar og bør meldes til ejeren af plugin\'et. - Afspilningshastigheder - - Der er ingen emner i denne liste. - Du har endnu ikke abonneret til nogle feeds. - - Andre - Om - - Tjenester - Flattr - Sæt afspilning på pause når hovedtelefoner afbrydes - Hop til næste medie i køen når afspilning er færdig - Afspilning - Netværk - Opdaterings interval - Specificer et interval indenfor hvilket feeds opdaterer automatisk eller deaktiver det - Download kun medie filer over WiFi - Kontinuerlig afspilning - WiFi medie download - Hovedtelefoner afbrudt - Mobile opdateringer - Tillad opdateringer over mobil data forbindelse - Opdaterer - Flattr indstillinger - Flattr log ind - Log ind til din flattr konto for at flattr emner direkte fra app\'en - Flattr denne app - Støt udviklingen af AntennaPod ved at flattr den. Tak! - Tilbagekald adgang - Tilbagekald adgangen til din flattr konto fra denne app. - Flattr\'er automatisk - Brugerflade - Vælg tema - Skift AntennaPods udseende. - Download automatisk - Konfigurer automatisk download af episoder - Sæt Wi-Fi filter til - Tillad kun automatisk download for de valgte Wi-Fi netværk - Episode cache - Lys - Mørk - Uendelig - timer - time - Manuelt - Log ind - Log ind med din gpodder.net konto for at synkronisere dine abonnementer. - Log ud - Logget ud - Skift login information - Skift din gpodder.net kontos login information. - Afspilningshastigheder - Tilpas tilgængelige hastigheder for variabelt afspilningshastigheds plugin - Indstil værtsnavn - Brug standard vært - - - Søg efter feeds eller episoder - Funder i showets noter - Fundet i kapitler - Fandt ingen resultater - Søg - Fundet i titel - - OPML filer lader dig flytte dine podcasts fra en podcastafspiller til en anden. - For at importere en OPML fil, skal du først placere den i følgende mappe og tryk på knappen nedenfor for at starte import-processen. - Start import - OPML import - FEJL! - Indlæser OPML fil - En fejl opstod under indlæsning af opml documentet: - Import mappen er tom. - Vælg alt - Fravælg alt - Vælg fil at importere - OPML eksport - Eksporterer... - Eksport fejl - Opml eksport lykkedes. - .opml filen var skrevet til:\u0020 - - Sæt søvn timer - Fjern søvn timer - Indtast tid - Søvn timer - Tid tilbage:\u0020 - Ugyldig indtastning, tid skal være et heltal - sekunder - minutter - timer - - KATEGORIER - TOP PODCASTS - FORSLAG - Søg på gpodder.net - Log ind - Velkommen til gpodder.nets login proces. Indsæt dine login informationer: - Log ind - Hvis du ikke har en konto endnu, så kan du oprette en her:\nhttps://gpodder.net/register/ - Brugernavn - Kodeord - Enheds valg - Tilføj en ny enhed for at bruge din gpodder.net konto eller vælg en eksisterende: - Enhed ID:\u0020 - Billedtekst - Opret en ny enhed - Vælg en eksisterende enhed: - Enheds ID må ikke være tomt - Enheds ID er allerede i brug - Vælg - Login lykkedes! - Tillykke! Din gpodder.net konto er nu forbundet med din enhed. AntennaPod vil fra nu af automatisk synkronisere dine abonnementer på din enhed med din gpodder.net konto. - Start synkronisering nu - Gå til hovedskærmen - gpodder.net autentificeringfejl - Forkert brugernavn eller kodeord - gpodder.net synkroniseringsfejl - En fejl opstod under synkronisering:\u0020 - - Valgte mappe: - Opret mappe - Vælg data mappe - Opret en ny mappe med navnet \"%1$s\"? - Opret en ny mappe - Kan ikke skrive til denne mappe - Mappen eksisterer allerede - Kunne ikke oprette ny mappe - Mappen er ikke tom - Mappen du har valgt er ikke tom. Medie downloads og andre filer vil blive placeret i denne mappe. Forsæt alligevel? - Vælg standard mappe - Sæt afspilning på pause i stedet for at sænke lydniveauet når en anden app vil afspille lyde - Pause for afbrydelser - - Abonner - Abonneret - Downloader... - - Vis kapitler - Vis shownoter - Vis billede - Spol tilbage - Hurtigt fremad - Lyd - Video - Naviger opad - Flere handlinger - Episode afspilles - Episode downloades - Episode er downloadet - Nyt emne - Episode er i køen - Antal nye episoder - Antallet af episoder du er begyndt at lytte til - Træk for at skifte denne tings position - - Godkendelse - Skift dit brugernavn og kodeord for denne podcast og dets episoder. - - Importerer abonnementer fra single-purpose apps… - diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml deleted file mode 100644 index 539470a5e..000000000 --- a/app/src/main/res/values-de/strings.xml +++ /dev/null @@ -1,341 +0,0 @@ - - - - AntennaPod - Feeds - Podcast hinzufügen - PODCASTS - EPISODEN - Neue Episoden - Alle Episoden - Neu - Warteliste - Einstellungen - Podcast hinzufügen - Downloads - Aktiv - Abgeschlossen - Log - Download abbrechen - Zuletzt gespielt - gpodder.net - gpodder.net Anmeldung - - Zuletzt veröffentlicht - Nur neue Episoden anzeigen - - Menü öffnen - Menü schließen - - Im Browser öffnen - URL kopieren - URL teilen - URL wurde in die Zwischenablage kopiert. - Gehe zu dieser Position - - Chronik löschen - - Bestätigen - Abbrechen - Autor - Sprache - Einstellungen - Bild - Fehler - Ein Fehler ist aufgetreten: - Aktualisieren - Der externe Speicher ist nicht verfügbar. Bitte stelle sicher, dass das externe Speichermedium eingelegt ist, damit die Anwendung funktioniert. - Kapitel - Notizen - Beschreibung - Letzte Episode:\u0020 - \u0020Episoden - Länge:\u0020 - Größe:\u0020 - Verarbeite - Lade ... - Benutzername und Password merken - Schließen - Erneut versuchen - Automatisch herunterladen - - Feed URL - URL des Feeds oder der Webseite - Podcast per URL hinzufügen - Podcast in Verzeichnis finden - Bei gpodder.net kannst du nach neuen Podcasts nach Name, Kategorie oder Popularität suchen. - gpodder.net durchsuchen - - Markiere alle als gelesen - Alle Episoden als gelesen markieren - Informationen anzeigen - Podcast entfernen - Webseiten-Link teilen - Feed-Link teilen - Bitte bestätige, dass du diesen Feed und ALLE heruntergeladenen Episoden dieses Feeds entfernen möchtest. - Entferne Feed - - Herunterladen - Abspielen - Pausieren - Streamen - Entfernen - Episode entfernen - Als gelesen markieren - Als ungelesen markieren - Zur Abspielliste hinzufügen - Aus der Abspielliste entfernen - Webseite besuchen - Flattr this - Alle zur Abspielliste hinzufügen - Alle herunterladen - Episode überspringen - - erfolgreich - fehlgeschlagen - Download anstehend - Download läuft - Speichermedium nicht gefunden - Zu wenig Speicherplatz - Dateifehler - HTTP Datenfehler - Unbekannter Fehler - Parserfehler - Nicht unterstützter Feed-Typ - Verbindungsfehler - Unbekannter Host - Authentifizierungsfehler - Alle Downloads abbrechen - Download abgebrochen - Download abgeschlossen - Fehler in URL - IO Error - Anfragefehler - Datenbankzugriffsfehler - \u0020Downloads übrig - Verarbeite Downloads - Lade Podcast-Daten - %1$d Downloads erfolgreich, %2$d fehlgeschlagen - Unbekannter Titel - Feed - Mediendatei - Bild - Beim Herunterladen der Datei ist ein Fehler aufgetreten:\u0020 - Authentifizierung erforderlich - Die angeforderte Quelle erfordert einen Benutzernamen und ein Passwort - - Fehler! - Keine Medienwiedergabe - Bereite vor - Fertig - Spule - Server ist offline - Unbekannter Fehler - Keine Medienwiedergabe - 00:00:00 - Puffert - Spiele Podcast ab - AntennaPod - Unbekannte Medientaste: %1$d - - Abspielliste leeren - Rückgängig - Element entfernt - Zum Anfang verschieben - Zum Ende verschieben - - Flattr Anmeldung - Drücke den Button unten um den Authentifizierungsprozess zu starten. Du wirst dann zur Flattr-Anmeldeseite weitergeleitet, wo du gefragt wirst, AntennaPod die Erlaubnis zu geben, Dinge zu flattrn. Nachdem du die Erlaubnis erteilt hast, kehrst du automatisch zu diesem Bildschirm zurück. - Authentifizieren - Zur Hauptseite zurückkehren - Die Authentifizierung war erfolgreich! Du kannst nun in der Anwendung Flattr verwenden. - Kein Flattr Token gefunden - Dein Flattr Account scheint nicht mit AntennaPod verbunden zu sein. Tippe hier zum authentifizieren. - Dein Flattr Account scheint nicht mit AntennaPod verbunden zu sein. Du kannst entweder deinen Account mit AntennaPod verbinden, um direkt in der Anwendung Flattr zu verwenden, oder du kannst die Flattr-Seite der Sache im Netz besuchen. - Authentifizieren - Aktion verboten - AntennaPod besitzt keine Erlaubnis für diese Aktion. Der Grund dafür könnte sein, dass AntennaPods Zugangstoken aufgehoben worden ist. Du kannst dich entweder erneut authentifizieren oder die Flattr-Seite der Sache im Web besuchen. - Zugriff widerrufen - Du hast AntennaPod das Zugangstoken zu deinem Account entzogen. Um diesen Prozess abzuschließen, musst du diese Anwendung aus der Liste der zugelassenen Anwendungen in deinen Account Einstellungen auf der Flattr Webseite entfernen. - - Eine Sache wurde geflattrt! - %d Sachen wurden geflattrt! - Geflattrt: %s - Flattrn von %d Sachen fehlgeschlagen! - Nicht geflattrt: %s - Sache wird später gelfattrt - Flattrt: %s - AntennaPod flattrt - AntennaPod hat geflattrt - AntennaPod Flattrn fehlgeschlagen - Rufe geflatterte Sachen ab - - Plugin herunterladen - Plugin nicht installiert - Um die Wiedergabegeschwindigkeit zu verändern, muss eine Drittanbieter-Bibliothek heruntegeladen werden.\n\nDrücke auf \"Plugin herunterladen\", um ein kostenloses Plugin aus dem Play Store zu installieren.\n\nProbleme, die bei der Benutzung des Plugins auftreten, sollten dem Entwickler des Plugins gemeldet werden. - Wiedergabegeschwindigkeiten - - Es sind keine Einträge in dieser Liste. - Du hast noch keine Feeds abonniert. - - Anderes - Über - Abspielliste - Dienste - Flattr - Pausiere die Wiedergabe wenn der Kopfhörer entfernt worden ist. - Springe zur nächsten Episode wenn die vorherige Episode endet - Wiedergabe - Netzwerk - Aktualisierungsintervall - Lege ein Intervall fest, in dem Feeds automatisch aktualisiert werden oder deaktiviere es - Lade Mediendateien nur über WiFi - Durchgehendes Abspielen - WiFi Medien-Download - Kopfhörer-Trennung - Mobile Aktualisierungen - Erlaube Aktualisierungen über die mobile Datenverbindung - Aktualisiere - Flattr Einstellungen - Flattr Anmeldung - Melde dich mit deinem Flattr Account an, um direkt in der Anwendung zu flattrn. - Flattr diese Anwendung - Unterstütze die Entwicklung von AntennaPod mit Flattr. Danke! - Zugriff entziehen - Entziehe dieser Anwendung die Zugriffserlaubnis für deinen Flattr Account. - Automatisches Flattrn - Automatisches Flattrn konfigurieren - Benutzeroberfläche - Theme auswählen - Ändere das Aussehen von AntennaPod. - Automatisches Herunterladen - Konfiguriere das automatische Herunterladen von Episoden. - W-LAN-Filter aktivieren - Erlaube das automatische Herunterladen nur in ausgewählten W-LAN Netzwerken. - Episodenspeicher - Hell - Dunkel - Unbegrenzt - Stunden - Stunde - Manuell - Anmelden - Melde dich mit deinem gpodder.net profil an um deine Abonnements zu synchronisieren - Abmelden - Abmeldung war erfolgreich - Anmeldeinformationen ändern - Ändere die Anmeldeinformationen deines gpodder.net profils - Wiedergabegeschwindigkeiten - Lege die verfügbaren Werte für die Veränderung der Wiedergabeschwindigkeit fest - Spul-Zeit - Spule so viele Sekunden vor oder zurück - Hostname ändern - Standard-Host verwenden - - Automatisches Flattrn aktivieren - Flattr eine Episode sobald %d Prozent gespielt worden sind - Flattr Episode, sobald die Wiedergabe beginnt - Flattr Episode, sobald die Wiedergabe endet - - Suche nach Feeds oder Episoden - In Sendungsnotizen gefunden - In Kapiteln gefunden - Keine Ergebnisse gefunden - Suche - In Titel gefunden - - Mit OPML Dateien kannst du deine Podcasts von einem Podcatcher auf einen anderen übertragen - Um eine OPML Datei zu importieren, musst du diese im folgenden Ordner platzieren und den unteren Button antippen, um den Import Prozess zu starten. - Import starten - OPML Import - FEHLER! - Lese OPML Datei - Ein Fehler is beim Lesen des OPML Dokuments aufgetreten: - Der Import-Ordner ist leer. - Alle auswählen - Auswahl zurücksetzen - Wähle eine Datei zum Importieren aus - OPML Export - Exportiere... - Exportfehler - OPML Export erfolgreich - Die .opml Datei wurde unter dem folgenden Pfad gespeichert:\u0020 - - Schlummerfunktion - Schlummerfunktion deaktivieren - Zeit eingeben - Schlummerfunktion - Zeit übrig:\u0020 - Ungültige Eingabe, Zeit muss eine Ganzzahl sein - Sekunden - Minuten - Stunden - - KATEGORIEN - BESTE PODCASTS - VORSCHLÄGE - gpodder.net durchsuchen - Anmeldung - Willkommen beim gpodder.net Anmeldeprozess. Gib zuerst deine Anmeldeinformationen ein: - Anmelden - Falls du noch kein gpodder.net profil hast, kannst du hier eines erstellen:\nhttps://gpodder.net/register/ - Benutzername - Passwort - Geräte-Auswahl - Erstelle ein neues Gerät für dein gpodder.net profil oder wähle ein bereits vorhandenes: - Geräte-ID:\u0020 - Beschreibung - Neues Gerät erstellen - Vorhandenes Gerät auswählen - Geräte-ID darf nicht leer sein - Geräte-ID wird bereits verwendet - Auswählen - Anmeldung erfolgreich! - Glückwunsch! Dein gpodder.net profil ist jetzt mit deinem Gerät verbunden. Von nun an wird AntennaPod automatisch deine Abonnements mit deinem gpodder.net profil synchronisieren. - Jetzt synchronisieren - Zum Hauptbildschirm zurückkehren - gpodder.net Anmeldefehler - Falscher Benutzername oder falsches Passwort - gpodder.net Synchronisierungsfehler - Ein Fehler ist beim Synchronisieren aufgetreten:\u0020 - - Ausgewählter Ordner - Neuer Ordner - Datenordner auswählen - Neuen Ordner mit Namen \"%1$s\" erstellen? - Neuer Ordner angelegt - Kann in diesem Ordner nicht schreiben - Ordner existiert bereits - Konnte Datenordner nicht erstellen - Ordner ist nicht leer - Der ausgewählte Ordner ist nicht leer. Medien-Downloads und andere Daten werden direkt in diesem Ordner gespeichert. Trotzdem fortfahren? - Standardordner auswählen - Pausiere die Wiedergabe anstatt die Lautstärke zu reduzieren, wenn eine andere Anwendung Töne abspielt - Bei Unterbrechungen pausieren - - Abonnieren - Abonniert - Lade herunter... - - Kapitel anzeigen - Sendungsnotizen anzeigen - Bild anzeigen - Zurückspulen - Vorspulen - Audio - Video - Nach oben navigieren - Mehr Aktionen - Episode wird gerade abgespielt - Episode wird gerade heruntergeladen - Episode ist heruntergeladen - Eintrag ist neu - Episode befindet sich inder Abspielliste - Anzahl neuer Episoden - Anzahl der Episoden, die du angefangen hast zu hören - Ziehe, um die Position dieses Objekts zu verändern - - Authentifizierung - Ändere den Benutzernamen und das Passwort für diesen Podcast und dessen Episoden. - - Importiere Abonnements aus Single-Purpose Apps - diff --git a/app/src/main/res/values-es-rES/strings.xml b/app/src/main/res/values-es-rES/strings.xml deleted file mode 100644 index cd4949530..000000000 --- a/app/src/main/res/values-es-rES/strings.xml +++ /dev/null @@ -1,200 +0,0 @@ - - - - AntennaPod - Canales - PODCASTS - EPISODIOS - Nuevos - Lista de espera - Ajustes - Descargas - Cancelar descarga - Historial de reproducción - - - - Abrir en el navegador - Copiar URL - Compartir URL - URL copiada al portapapeles. - - Limpiar el historial - - Confirmar - Cancelar - Autor - Idioma - Error - Ha ocurrido un error: - Actualizar - No se encuentra un almacenamiento externo. Asegúrese de que su almacenamiento externo esté montado para que la aplicación funcione correctamente. - Capítulos - Notas del programa - \u0020episodios - Duración:\u0020 - Tamaño:\u0020 - Procesando - Cargando... - - URL del canal - - Marcar todo como leído - Información del programa - Compartir el enlace de la web - Compartir el enlace del canal - Confirme que quiere eliminar este canal y TODOS los episodios descargados del mismo. - - Descargar - Reproducir - Pausar - Transmitir - Quitar - Marcar como leído - Marcar como no leído - Añadir a la cola - Quitar de la cola - Visitar el sitio web - Añadir a Flattr - Ponerlos todos en cola - Descargarlos todos - Saltar episodio - - Descarga pendiente - Descarga en curso - No se ha encontrado un dispositivo de almacenamiento - Espacio insuficiente - Error de archivo - Error de datos HTTP - Error desconocido - Excepción del analizador - Tipo de canal no admitido - Error de conexión - Host desconocido - Cancelar todas las descargas - Descarga cancelada - Descargas completadas - URL malformada - Error de E/S - Error de petición - \u0020descargas restantes - Descargando datos del podcast - %1$d descargas exitosas, %2$d fallidas - Título desconocido - Canal - Archivo de medios - Imagen - Ha ocurrido un error al intentar descargar el archivo:\u0020 - - ¡Error! - No hay medios en reproducción - Preparando - Listo - Buscando - El servidor está inactivo - Error desconocido - No hay medios en reproducción - 00:00:00 - Almacenando - Reproduciendo el podcast - - Limpiar la cola - - Identificarse en Flattr - Pulse el botón inferior para comenzar la autenticación. Su navegador abrirá la pantalla de identificación de Flattr y le preguntará si quiere conceder permiso a AntennaPod para valorar cosas. Tras concederlo, volverá a esta pantalla automáticamente. - Autenticarse - Volver a la pantalla principal - Autentificación exitosa. Ya puede valorar cosas en Flattr desde la aplicación. - No se ha encontrado un token de Flattr - Su cuenta de Flattr no está conectada con AntennaPod. Puede conectarla o puede visitar la página web de cada cosa para valorarla desde allí. - Autenticarse - Acción prohibida - AntennaPod no tiene permiso para realizar esta acción. La razón puede ser que se haya revocado el token de acceso de AntennaPod para su cuenta. Puede re-autenticarse o visitar la página web de la cosa. - Acceso revocado - Ha revocado el token de acceso de AntennaPod a su cuenta. Para completar el proceso debe eliminar esta aplicación de la lista de aplicaciones aprobadas, en los ajustes de Flattr. - - - - Esta lista no tiene elementos. - No se ha suscrito a ningún canal. - - Otros - Acerca de - Cola - Pausar la reproducción al desconectar los auriculares - Saltar al siguiente elemento de la cola al acabar la reproducción - Reproducción - Red - Intervalo de actualización - Especificar el intervalo en que se actualizarán automáticamente los canales, o desactivarlo - Solo descargar los contenidos por WiFi - Reproducción continua - Descarga de contenidos por WiFi - Desconexión de los cascos - Actualizaciones por red móvil - Permitir actualizaciones por red de datos móvil - Actualizando - Ajustes de Flattr - Identificación en Flattr - Identifíquese en Flattr para valorar cosas directamente desde la aplicación - Valorar esta aplicación en Flattr - Apoye el desarrollo de AntennaPod valorándola en Flattr. ¡Gracias! - Revocar el acceso - Rescindir el permiso de acceso de esta aplicación a su cuenta de Flattr. - Interfaz de usuario - Elegir un tema - Cambiar la apariencia de AntennaPod. - Descarga automática - Configurar la descarga automática de episodios. - Activar el filtro WiFi - Permitir la descarga automática sólo para las redes WiFi marcadas. - Caché de episodios - - - Buscar canales o episodios - Encontrado en las notas del programa - Encontrado en los capítulos - No se han encontrado resultados - Buscar - Encontrado en el título - - Para importar un archivo OPML, debe copiarlo al siguiente directorio y pulsar el botón que se muestra abajo. - Comenzar la importación - Importación de OPML - ¡ERROR! - Leyendo el archivo OPML - Ha ocurrido un error al leer el archivo OPML - El directorio de importación está vacío - Seleccionar todo - Deseleccionar todo - Elegir qué archivo importar - Exportar a OPML - Exportando... - Error en la exportación - Exportación a OPML exitosa - El archivo OPML se ha escrito en:\u0020 - - Establecer un temporizador - Desactivar el temporizador - Introducir hora - Temporizador - Tiempo restante:\u0020 - Entrada no válida, el tiempo debe ser un entero - - - Carpeta seleccionada - Crear carpeta - Elegir carpeta de datos - ¿Crear carpeta con nombre «%1$s»? - Carpeta creada - No se puede escribir a esta carpeta - Ya existe la carpeta - No se ha podido crear la carpeta - La carpeta no está vacía - La carpeta elegida no está vacía. Las descargas y otros archivos se copiarán directamente en esta carpeta. ¿Continuar igualmente? - Elegir carpeta predeterminada - - - - - diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml deleted file mode 100644 index 1b87e6dbc..000000000 --- a/app/src/main/res/values-es/strings.xml +++ /dev/null @@ -1,313 +0,0 @@ - - - - AntennaPod - Canales - PODCASTS - EPISODIOS - Episodios nuevos - Todos los episodios - Nuevos - Lista de espera - Ajustes - Añadir podcast - Descargas - Cancelar descarga - Histórico de reproducción - gpodder.net - Iniciar sesión en gpodder.net - - Mostrar solo episodios nuevos - - - Abrir en el navegador - Copiar URL - Compartir URL - URL copiada al portapapeles. - - Vaciar el histórico - - Confirmar - Cancelar - Autor - Idioma - Ajustes - Imagen - Error - Ha ocurrido un error: - Actualizar - No se encuentra un almacenamiento externo. Asegúrese de que su almacenamiento externo esté montado para que la aplicación funcione correctamente. - Capítulos - Notas del programa - Descripción - Episodio más reciente:\u0020 - \u0020episodios - Duración:\u0020 - Tamaño:\u0020 - Procesando - Cargando... - Guardar usuario y contraseña - Cerrar - Reintentar - Incluir en auto descargas - - URL del canal - Añadir podcast por URL - Explorar gpodder.net - - Marcar todo como leído - Información del programa - Compartir el enlace de la web - Compartir el enlace del canal - Confirme que quiere eliminar este canal y TODOS los episodios descargados del mismo. - Eliminando el canal - - Descargar - Reproducir - Pausar - Reproducir por streaming - Quitar - Marcar como leído - Marcar como no leído - Añadir a la cola - Quitar de la cola - Visitar el sitio web - Añadir a Flattr - Ponerlos todos en cola - Descargarlos todos - Omitir episodio - - Descarga pendiente - Descarga en curso - No se ha encontrado un dispositivo de almacenamiento - Espacio insuficiente - Error de archivo - Error de datos HTTP - Error desconocido - Excepción del analizador - Tipo de canal no admitido - Error de conexión - Host desconocido - Error de autenticación - Cancelar todas las descargas - Descarga cancelada - Descargas completadas - URL con formato incorrecto - Error de E/S - Error de solicitud - Error de acceso a la base de datos - \u0020descargas restantes - Descargando datos del podcast - %1$d descargas exitosas, %2$d fallidas - Título desconocido - Canal - Archivo de medios - Imagen - Ha ocurrido un error al intentar descargar el archivo:\u0020 - Autenticación requerida - El recurso solicitado requiere usuario y contraseña - - ¡Error! - No hay medios en reproducción - Preparando - Listo - Buscando - El servidor está inactivo - Error desconocido - No hay medios en reproducción - 00:00:00 - Almacenando - Reproduciendo el podcast - - Vaciar la cola - Deshacer - Artículo eliminado - Mover al principio - Mover al final - - Identificarse en Flattr - Pulse el botón inferior para comenzar la autenticación. Su navegador abrirá la pantalla de identificación de Flattr y le preguntará si quiere conceder permiso a AntennaPod para valorar cosas. Tras concederlo, volverá a esta pantalla automáticamente. - Autenticarse - Volver a la pantalla principal - Autentificación exitosa. Ya puede valorar cosas en Flattr desde la aplicación. - No se ha encontrado un token de Flattr - Su cuenta de Flattr no está conectada con AntennaPod. Puede conectarla o puede visitar la página web de cada cosa para valorarla desde allí. - Autenticarse - Acción prohibida - AntennaPod no tiene permiso para realizar esta acción. La razón puede ser que se haya revocado el token de acceso de AntennaPod para su cuenta. Puede re-autenticarse o visitar la página web de la cosa. - Acceso revocado - Ha revocado el token de acceso de AntennaPod a su cuenta. Para completar el proceso debe eliminar esta aplicación de la lista de aplicaciones aprobadas, en los ajustes de Flattr. - - ¡Flattr una cosa! - ¡Flattr %d cosas! - Flattr: %s. - ¡Falló Flattr de %d cosas! - No se hizo Flattr: %s. - Se hará Flattr de esta cosa más tarde - Haciendo Flattr de %s - AntennaPod haciendo Flattr - AntennaPod hizo Flattr - AntennaPod Flattr falló - Obteniendo lista de Flattr - - Descargar complemento - Complemento no instalado - Para que la reproducción a velocidad variable funcione, es necesario instalar un complemento adicional.\n\nPulse «Descargar complemento» para descargar un complemento gratuito de la Play Store.\n\nSi aparece cualquier problema durante la utilización del complemento, informe de él al propietario, pues éste no es responsabilidad de AntennaPod. - Velocidades de reproducción - - Esta lista no tiene elementos. - No se ha suscrito a ningún canal. - - Otros - Acerca de - Cola - Servicios - Flattr - Pausar la reproducción al desconectar los auriculares - Saltar al siguiente elemento de la cola al acabar la reproducción - Reproducción - Red - Intervalo de actualización - Especificar el intervalo en que se actualizarán automáticamente los canales, o desactivarlo - Solo descargar los contenidos por WiFi - Reproducción continua - Descarga de contenidos por WiFi - Desconexión de los cascos - Actualizaciones por red móvil - Permitir actualizaciones por red de datos móvil - Actualizando - Ajustes de Flattr - Identificación en Flattr - Identifíquese en Flattr para valorar cosas directamente desde la aplicación - Valorar esta aplicación en Flattr - Apoye el desarrollo de AntennaPod valorándola en Flattr. ¡Gracias! - Revocar el acceso - Rescindir el permiso de acceso de esta aplicación a su cuenta de Flattr. - Uso de Flattr automático - Interfaz de usuario - Elegir un tema - Cambiar la apariencia de AntennaPod. - Descarga automática - Configurar la descarga automática de episodios. - Activar el filtro WiFi - Permitir la descarga automática sólo para las redes WiFi marcadas. - Caché de episodios - Claro - Oscuro - Ilimitado - horas - hora - Manual - Iniciar sesión - Inicie sesión con su cuenta de gpodder.net para sincronizar sus suscripciones. - Cerrar sesión - Ha cerrado la sesión correctamente. - Cambiar información de acceso - Modificar datos de inicio de sesión en gpodder.net. - Velocidades de reproducción - Personalice las velocidades disponibles para la reproducción de audio a velocidad variable - Definir nombre de equipo - Usar nombre de equipo por defecto - - - Buscar canales o episodios - Encontrado en las notas del programa - Encontrado en los capítulos - No se han encontrado resultados - Buscar - Encontrado en el título - - Los archivos OPML le permiten migrar sus podcasts de una aplicación a otra. - Para importar un archivo OPML, debe copiarlo al siguiente directorio y pulsar el botón que se muestra abajo. - Comenzar la importación - Importación de OPML - ¡ERROR! - Leyendo el archivo OPML - Ha ocurrido un error al leer el archivo OPML - El directorio de importación está vacío. - Seleccionar todo - Deseleccionar todo - Elegir qué archivo importar - Exportar a OPML - Exportando... - Error en la exportación - Exportación a OPML exitosa - El archivo OPML se ha escrito en:\u0020 - - Establecer un temporizador - Desactivar el temporizador - Introducir hora - Temporizador - Tiempo restante:\u0020 - Entrada no válida, el tiempo debe ser un entero - segundos - minutos - horas - - CATEGORÍAS - MEJORES PODCASTS - SUGERENCIAS - Buscar en gpodder.net - Iniciar sesión - Bienvenido al proceso de autenticación de gpodder.net. Primero, escriba sus datos de inicio de sesión: - Iniciar sesión - Si tiene una cuenta aún, puede crear una aquí:\nhttps://gpodder.net/register/ - Nombre de usuario - Contraseña - Selección del dispositivo - Cree un nuevo dispositivo para usar con su cuenta de gpodder.net o elija uno existente: - Id. de dispositivo:\u0020 - Descripción - Crear nuevo dispositivo - Elegir dispositivo existente: - El ID de dispositivo no puede estar vacío - El ID de dispositivo ya está en uso - Elegir - ¡Inicio de sesión correcto! - ¡Enhorabuena! Su cuenta de gpodder.net está ahora asociada con su dispositivo. A partir de ahora AntennaPod sincronizará automáticamente las suscripciones de su dispositivo con su cuenta de gpodder.net. - Comenzar sincronización ahora - Ir a la pantalla principal - Error de autenticación de gpodder.net - Usuario o contraseña incorrectos - Error de sincronización de gpodder.net - Ocurrió un error de sincronización:\u0020 - - Carpeta seleccionada - Crear carpeta - Elegir carpeta de datos - ¿Crear carpeta con nombre «%1$s»? - Carpeta creada - No se puede escribir a esta carpeta - Ya existe la carpeta - No se ha podido crear la carpeta - La carpeta no está vacía - La carpeta elegida no está vacía. Las descargas y otros archivos se copiarán directamente en esta carpeta. ¿Continuar igualmente? - Elegir carpeta predeterminada - Pausar la reproducción en lugar de bajar el volumen cuando otra aplicación reproduzca sonidos - Pausar durante las interrupciones - - Suscribirse - Suscrito - Descargando… - - Mostrar capítulos - Mostrar notas del programa - Mostrar imagen - Rebobinar - Avance rápido - Audio - Vídeo - Navegar hacia arriba - Más acciones - El episodio se está reproduciendo - El episodio se está descargando - El episodio está descargado - El elemento es nuevo - El episodio está en la cola - Cantidad de episodios nuevos - Cantidad de episodios que ha comenzado a escuchar - - Autenticación - - Importando subscripciones de aplicaciones de uso específico... - diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml deleted file mode 100644 index afc441b99..000000000 --- a/app/src/main/res/values-fr/strings.xml +++ /dev/null @@ -1,340 +0,0 @@ - - - - AntennaPod - Flux - Ajouter un podcast - PODCASTS - ÉPISODES - Nouveaux épisodes - Tous les épisodes - Nouveau - Liste d\'attente - Préférences - Ajouter un podcast - Téléchargements - En cours - Terminé - Journal d\'activités - Annuler les téléchargements - Journal des lectures - gpodder.net - identifiants gpodder.net - - Publié récemment - N\'afficher que les nouveaux épisodes - - Ouvrir le menu - Fermer le menu - - Ouvrir dans le navigateur - Copier l\'URL - Partager l\'URL - URL copiée dans le presse-papier - Aller à cette position - - Effacer le journal - - Confirmer - Annuler - Auteur - Langue - Préférences - Image - Erreur - Une erreur a eu lieu : - Rafraîchir - Aucun stockage externe n\'est disponible. Merci de connecter un volume de stockage externe pour que l\'application puisse fonctionner correctement. - Chapitres - Notes d\'épisode - Description - Épisode le plus récent :\u0020 - \u0020épisodes - Durée :\u0020 - Taille :\u0020 - Traitement en cours - En chargement... - Sauvegarder votre identifiant et votre mot de passe - Fermer - Réessayer - Télécharger automatiquement à l\'avenir - - URL du flux - URL ou flux ou site web - Ajouter un podcast par son URL - Trouver le podcast dans la bibliothèque - Vous pouvez chercher de nouveaux podcasts en filtrant par nom, catégorie ou popularité dans la bibliothèque gpodder.net - Chercher sur gpodder.net - - Tous marquer comme lus - Tous les épisodes ont été marqués comme lus - Voir les détails - Supprimer le podcast - Partager un lien vers le site - Partager le flux - Veuillez confirmer que vous voulez bien supprimer ce flux et TOUS ses épisodes que vous avez téléchargés. - Flux en cours de suppression - - Télécharger - Lire - Pause - Lire en ligne - Supprimer - Supprimer cet épisode - Marquer comme lu - Marquer comme non lu - Ajouter à la liste - Supprimer de la liste - Visiter le site - Flattr ça! - Ajouter tous à la liste - Tous télécharger - Passer cet épisode - - terminé - échoué - Téléchargement en attente - Téléchargement en cours - Volume de stockage non trouvé - Espace insuffisant - Accès au fichier impossible - Erreur de données HTTP - Erreur inconnue - Exception de l\'analyseur - Type de flux non géré - Erreur de connexion - Hôte inconnu - Erreur d\'authentification - Annuler tous les téléchargements - Téléchargement annulé - Téléchargements terminés - URL incorrecte - Erreur d\'E/S - Erreur de requête - Problème dans l\'accès à la base de données - \u0020téléchargements restants - Traitement des téléchargements - Téléchargement des données du podcast - %1$d téléchargements réussis, %2$d échoués - Titre inconnu - Flux - Fichier média - Image - Une erreur s\'est produite durant le téléchargement du fichier :\u0020 - Authentification requise - La ressource que vous avez demandé nécessite un nom d\'utilisateur et un mot de passe - - Erreur ! - Pas de lecture en cours - En préparation - Prêt - Recherche - Le serveur ne répond pas - Erreur inconnue - Aucune lecture - 00:00:00 - Mise en mémoire - Lecture de podcast en cours - AntennaPod - Touche média inconnue : %1$d - - Effacer la liste - Annuler - Élément retiré - Déplacer vers le haut de haut de la liste - Déplacer vers le bas de la liste - - Connecter à Flattr - Appuyez sur le bouton ci-dessous pour vous authentifier. Vous serez envoyés à l\'écran de connexion Flattr dans le navigateur, et il vous sera demandé de donner à AntennaPod la permission de flattr. Une fois ceci fait, vous reviendrez automatiquement à cet écran. - S\'authentifier - Revenir au départ - L\'authentification a réussi. Vous pouvez maintenant flattr depuis cette application. - Aucun jeton Flattr trouvé. - Votre compte flattr semble ne pas être connecté à AntennaPod. Touchez ici pour vous connecter. - Votre compte Flattr se semble pas être connecté à AntennaPod. Vous pouvez soit connecter votre compte Flattr à AntennaPod pour pouvoir flattr depuis l\'application, ou vous pouvez aller sur le site de ce que vous voulez flattr. - S\'authentifier - Action interdite - AntennaPod n\'a pas la permission pour cette action. Il est possible que l\'accès à votre compte depuis AntennaPod ait été révoqué. Vous pouvez vous authentifier à nouveau, ou bien visiter le site à flattr directement. - Accès révoqué - Vous avez révoqué le jeton d\'accès d\'AntennaPod à votre compte. Pour terminer cette opération, vous devez retirer AntennaPod de la liste des applications autorisées sur le site web de Flattr. - - Une chose de Flattré ! - %d choses de Flattré ! - Flattré : %s. - Impossible de Flattrer %d choses ! - Non Flattré : %s. - Cette chose sera Flattré plus tard - En train de Flattrer %s - AntennaPod est en train de Flattrer - AntennaPod a Flattré - Flattr d\'AntennaPod a échoué - Obtention de la liste des choses Flattrées - - Télécharger une extension - Extension non installée - Pour pouvoir changer la vitesse de lecture il est nécessaire d\'installer une librairie tierce.\n\nSélectionnez \"Télécharger une extension\" pour télécharger une extension gratuite depuis le Play Store\n\nLes problèmes concernant les extensions sont de la responsabilité de leur créateur et non d\'AntennaPod. Veillez à notifier le créateur de l\'extension de tout problème. - Vitesses de lecture - - Cette liste est vide. - Vous n\'êtes encore abonné à aucun flux. - - Autres - À propos - Liste - Services - Flattr - Interrompre la lecture lorsque le casque est débranché - Après la fin d\'un épisode, passer au suivant - Lecture - Réseau - Intervalle de mise à jour - Indiquer un intervalle de mise à jour automatique des flux, ou le désactiver - Ne télécharger les épisodes que par Wi-Fi - Lecture continue - Téléchargement en Wi-Fi - Déconnexion du casque - Mise à jour mobile - Autoriser les mises à jour à travers la connexion de données mobile - Mise à jour en cours - Paramètres Flattr - Connexion à Flattr - Connectez-vous à votre compte Flattr pour pouvoir flattr directement depuis l\'application. - Flattr cette application - Encouragez le développement d\'AntennaPod grâce à Flattr. Merci ! - Révoquer l\'accès - Révoquer la permission d\'accès à votre compte Flattr depuis cette application. - Flattr automatique - Configurer les paiements flattr automatiques - Interface utilisateur - Choisir un thème - Modifier l\'apparence d\'AntennaPod. - Téléchargement automatique - Configurer le téléchargement automatique des épisodes. - Activer le filtre Wi-Fi - Autoriser le téléchargement automatique uniquement sur les réseaux Wi-Fi sélectionnés. - Épisodes stockés localement - Clair - Sombre - Illimité - heures - heure - Manuel - Identifiant - Identifiez vous avec votre compte gpodder.net afin de synchroniser vos abonnements - Se déconnecter - Vous êtes maintenant déconnecté - Modifier les informations de connexion - Modifier les information de connexion pour votre compte gpodder.net - Vitesses de lecture - Modifier la liste des vitesses disponibles pour la lecture audio - Bouger d\'autant de secondes en rembobinant ou en faisant une avance rapide - Choisir un nom de domaine - Utiliser le nom de domaine par défaut - - Activer le paiement flattr automatique - Lancer un paiement flattr pour un épisode dès que %d de l\'épisode a été joué - Lancer le paiement flattr d\'un épisode dès que la lecture commence - Lancer le paiement flattr d\'un épisode à la fin de la lecture - - Chercher des flux ou épisodes - Trouvé dans les notes - Trouvé dans les titres de chapitre - Aucun résultat trouvé - Recherche - Trouvé dans le titre - - Les fichiers OPML vous permettent de bouger vos podcasts d\'un logiciel à un autre. - Pour importer un fichier OPML, copiez-le dans le répertoire suivant, et appuyez sur le bouton ci-dessous pour l\'importer. - Démarrer l\'importation - Importation OPML - ERREUR ! - Lecture du fichier OPML en cours - Une erreur s\'est produite à la lecture du document OPML : - Le répertoire d\'importation est vide. - Tout choisir - Ne rien choisir - Choisir le fichier à importer - Exportation OPML - Exportation en cours... - Erreur d\'exportation - Exportation OPML réussie. - Le fichier .opml a été écrit ici :\u0020 - - Définir le minuteur d\'arrêt automatique - Désactiver le minuteur d\'arrêt automatique - Entrer l\'heure - Arrêt automatique - Durée restante :\u0020 - Entrée invalide, la durée doit être un nombre entier - secondes - minutes - heures - - CATEGORIES - PODCASTS POPULAIRES - SUGGESTIONS - Chercher gpodder.net - Se connecter - Bienvenue dans le processus de connexion à gpodder.net. Premièrement, veuillez entrer vos informations de connexion : - Connexion - SI vous n\'avez pas encore de compte, vous pouvez en créer un⏎\nhttps://gpodder.net/register/ - Identifiant - Mot de passe - Choix de l\'appareil - Créez un nouvel appareil à utiliser pour votre compte gpodder.net ou choisissez un appareil existant : - ID de l\'appareil :\u0020 - Légende - Créer un nouvel appareil - Choisir un appareil existant : - L\'ID de l\'appareil ne peut pas être vide - L\'ID de cet appareil est déjà en cours d\'utilisation - Choisir - Connexion réussie ! - Félicitations ! Votre compte gpodder.net est maintenant lié à votre appareil. AntennaPod va désormais automatiquement synchroniser vos podcasts sur votre appareil avec votre compte gpodder. - Commencer la synchronisation - Aller à l\'écran d\'accueil - Erreur d\'identification à gpodder.net - Problème d\'identifiant et/ou de mot de passe - Problème de synchronisation avec gpodder.net - Une erreur est apparue lors de la synchronisation :\u0020 - - Répertoire choisi : - Créer répertoire - Choisir le répertoire - Créer un répertoire nommé \"%1$s\" ? - Répertoire créé - Impossible d\'écrire dans ce répertoire - Le répertoire existe déjà - Impossible de créer le répertoire - Le répertoire n\'est pas vide - Le répertoire que vous avez choisi n\'est pas vide. Les fichiers téléchargés seront ajoutés à ce répertoire. Continuer malgré tout ? - Choisir le répertoire par défaut - Mettre la lecture en pause au lieu de baisser le volume quand une autre application veut jouer un son - Mettre en pause lors des interruptions - - S\'abonner - Abonné - Téléchargement en cours - - Afficher chapitres - Afficher notes d\'épisode - Afficher image - Retour en arrière - Avance rapide - Audio - Vidéo - Naviguer vers le haut - Plus d\'actions - L\'épisode est en train d\'être joué - L\'épisode est en train d\'être téléchargé - L\'épisode a été téléchargé - L\'élément est nouveau - L\'épisode est dans la liste - Nombre de nouveaux épisodes - Nombre d\'épisodes que vous avez commencé à écouter - Faire glisser pour changer la position de cet élément - - Authentification - Modifier votre identifiant et mot de passe pour ce podcast et tous ses épisodes - - Importation des abonnements à partir d\'applications à usage unique... - diff --git a/app/src/main/res/values-hi-rIN/strings.xml b/app/src/main/res/values-hi-rIN/strings.xml deleted file mode 100644 index 43590f62a..000000000 --- a/app/src/main/res/values-hi-rIN/strings.xml +++ /dev/null @@ -1,281 +0,0 @@ - - - - \tऐन्टेनापॉड - फिड्स - पॉडकास्ट - एपिसोड - नया - वेटिंग लिस्ट - सेटिंग्स - पॉडकास्ट जोड़ें - डाउनलोड - डाउनलोड रद्द करें - प्लेबैक इतिहास - gpodder.net - gpodder.net login - - - - ब्राउज़र में खोलें - कॉपी यूआरएल - शेयर यूआरएल - यूआरएल को क्लिपबोर्ड पर कॉपी कर लिया गया है - - हिस्ट्री हटाएँ - - पुष्टि करें - रद्द करें - \tनिर्माता - भाषा - सेटिंग्स - तस्वीर - त्रुटि - एक त्रुटि हो गई: - ताज़ा करें - कोई बाहरी भंडारण उपलब्ध नहीं है.सुनिश्चित करें कि आपने बाहरी भंडारण मुहिम शुरू की है ताकि अनुप्रयोग ठीक से काम कर सकते हैं - अध्याय - नोट्स दिखाएँ - विवरण - सबसे हाल का प्रकरण:\u0020 - \u0020एपिसोड - लंबाई:\u0020 - साइज:\u0020 - प्रसंस्करण - लोड हो रहा है ... - यूज़रनेम और पासवर्ड सहेजें - बंद करें - पुन: प्रयास - ऑटो डाउनलोड में शामिल करें - - यूआरएल फ़ीड - यूआरएल द्वारा पॉडकास्ट जोड़ें - पॉडकास्ट निर्देशिका - - पढ़ने के रूप में सभी को चिह्नित करें - जानकारी दिखाएँ - पॉडकास्ट हटाएँ\n - शेयर वेबसाइट लिंक - शेयर फ़ीड लिंक - इसकी पुष्टि करें कि आप इस फ़ीड और इस फ़ीड के सभी प्रकरणों को हटाना चाहते हैं जिन्हें आपने डाउनलोड किया है. - फ़ीड निकाल रहा है - - डाउनलोड - प्ले - रोकें - स्ट्रिम - हटाएँ - पढ़ा हुआ के रूप में चिह्नित करें - ना पढ़ा हुआ के रूप में चिह्नित करें - क़तार में जोड़ें - क़तार से हटाएँ - वेबसाइट पर जाएँ - इसे Flattr करें - पंक्ति में सभी को डालें - सभी डाउनलोड - एपिसोड छोङें - - सफल\n - डाउनलोड विफल - लंबित डाउनलोड - डाउनलोड चल रहा है - स्टोरेज डिवाइस नहीं मिला - अपर्याप्त स्थान - फ़ाइल त्रुटि - एचटीटीपी डेटा त्रुटि - अज्ञात त्रुटि - पार्सर अपवाद - असमर्थित फ़ीड प्रकार - कनेक्शन त्रुटि - अज्ञात होस्ट - सभी डाउनलोड रद्द करें - डाउनलोड रद्द - डाउनलोड पूरा हो गया है - गलत URL - आईओ त्रुटि - अनुरोध त्रुटि - डेटाबेस का उपयोग त्रुटि - \u0020Downloads छोड़ा - पॉडकास्ट डेटा डाउनलोड करें - %1$d डाउनलोड सफल रहा, %2$d में विफल रहा है - अज्ञात शीर्षक - फ़ीड - मीडिया फ़ाइल - छवि - फाइल डाउनलोड करने के लिए प्रयास करते समय एक त्रुटि हुई:\u0020 - - त्रुटि! - मीडिया नहीं चल रहा - तैयार किया जा रहा है - तैयार - मांग - सर्वर निरस्त - अज्ञात त्रुटि - मीडिया नहीं चल रहा - 00:00:00 - बफरिंग - प्लेईंग पॉडकास्ट - - कतार साफ - पूर्ववत् करें - आइटम हटाया - शीर्ष पर ले जाएं - नीचे जाएं - - Flattr पंजीकरण करें - प्रमाणीकरण प्रक्रिया शुरू करने के लिए नीचे दिए गए बटन को दबाएं. आपके ब्राउज़र में flattr लॉगिन स्क्रीन को भेजा जाएगा और flattr बातें करने के लिए अनुमति AntennaPod को देने के लिए कहा जाएगा. आपकि अनुमति देने के बाद, आप स्वतः ही इस स्क्रीन में वापस आ जाएगें. - प्रामाणीकरण - होम पर लौटें - प्रमाणीकरण सफल रहा था! अब आप अनुप्रयोग के भीतर चीजों को flattr कर सकते हैं. - कोई Flattr टोकन नहीं पाया गया - आपकी flattr खाते का AntennaPod से जुड़ा होना प्रतीत नहीं होता. आप या तो AntennaPod को अपने खाते से कनेक्ट कर सकते हैं अनुप्रयोग के भीतर चीजों को flattr करने के लिए या आप इसे वहाँ flattr करने के लिए वेबसाइट पर जा सकते हैं. - प्रामाणीकरण - कार्रवाई मना - AntennaPod को इस कार्रवाई के लिए अनुमति नहीं है.इस के लिए कारण हो सकता है की आपके खाते में AntennaPod की पहुँच टोकन को निरस्त किया गया है.आप या तो फिर से प्रमाणित कर सकते हैं या बजाय किसी बात के वेबसाइट पर जा सकते हैं. - प्रवेश निरस्त किया - आपने सफलतापूर्वक अपने खाते में AntennaPod पहुँच टोकन निरस्त कर दिया है. इस प्रक्रिया को पूरा करने के लिए, आपको flattr वेबसाइट पर अपने खाते की सेटिंग्स में अनुमोदित आवेदनों की सूची से इस एप्लिकेशन को हटाना होगा. - - सफलतापूर्वक यह बात Flattr किया - सफलतापूर्वक %d बातोंको Flattr किया - Flattr गिनती: %s - ऐन्टेनापॉड Flattr - - प्लगइन डाउनलोड करें - प्लगइन स्थापित नहीं हुआ - काम करने के लिए चर गति प्लेबैक के लिए, एक तीसरी पार्टी पुस्तकालय स्थापित किया जाना चाहिए. ⏎\n⏎\nप्ले स्टोर से एक मुक्त प्लगइन डाउनलोड करने के लिए \'डाउनलोड प्लगइन\' को ठोकें⏎\n⏎इस प्लगइन का उपयोग कर पाने में कोई समस्या है तो AntennaPod जिम्मेदार नहीं है और प्लगइन मालिक को सूचित किया जाना चाहिए. - प्लेबैक गति - - इस सूची में कोई आइटम नहीं हैं. - आपने अभी तक किसी भी फ़ीड की सदस्यता नहीं ली है. - - अन्य - के बारे में - पंक्ति - सेवाएं - Flattr - प्लेबैक रोकें जब हेडफोन काट रहे हैं - प्लेबैक के पूरा होने पर अगली पंक्ति आइटम के लिए जाएँ - प्लेबैक - संजाल - अंतराल अद्यतन - फ़ीड स्वचालित रूप से ताजा कर रहे हैं जिसमें एक अंतराल निर्दिष्ट करें या उसे निष्क्रिय करें - केवल वाईफ़ाई पर मीडिया फ़ाइलें डाउनलोड करें - सतत प्लेबैक - वाईफाई मीडिया डाउनलोड करें - headphones काटना - मोबाइल अपडेट - मोबाइल डेटा कनेक्शन पर अपडेट करने की अनुमति दें - रिफ्रेशिंग - Flattr सेटिंग्स - Flattr पंजीकरण करें - App से सीधे अपनी बातें flattr करने के लिए अपने flattr खाते में प्रवेश करें. - इस app को Flattr करें - यह flattring द्वारा AntennaPod के विकास का समर्थन करें. धन्यवाद! - उपयोग रद्द - इस अनुप्रयोग के लिए अपने flattr खाते के लिए उपयोग की अनुमति रद्द करें. - यूजर इंटरफेस - थीम का चयन करें - AntennaPod का प्रकटन बदलें. - स्वचालित डाउनलोड - एपिसोड के स्वत: डाउनलोड विन्यस्त करें. - वाई-फाई फिल्टर सक्षम करें - केवल चयनित वाई-फाई नेटवर्क के लिए स्वत: डाउनलोड की अनुमति दें. - \tगुप्त एपिसोड - हलका - अंधेरा - असीमित - घंटे - घंटा - मैनुअल - लॉगिन - अपनी सदस्यता सिंक करने के क्रम में अपने gpodder.net खाते के साथ लॉगिन करें . - लॉगआउट - लॉगआउट सफल रहा था - प्रवेश जानकारी बदलें - अपने gpodder.net खाते के लिए प्रवेश जानकारी बदलें. - प्लेबैक गति - चर गति ऑडियो प्लेबैक के लिए उपलब्ध गति बनाइए - होस्टनाम सेट - डिफ़ॉल्ट होस्ट का प्रयोग करें - - - फ़ीड या एपिसोड के लिए खोज - Shownotes में मिला - अध्यायों में मिला - कोई परिणाम नहीं मिले - खोज - शीर्षक में मिला - - OPML फ़ाइलें आपको एक podcatcher से दूसरे को अपने पॉडकास्ट स्थानांतरित करने के लिए अनुमति देते हैं. - एक OPML फ़ाइल आयात करने के लिए, आपको इसे निम्नलिखित निर्देशिका में डालना है और आयात की प्रक्रिया शुरू करने के लिए नीचे दिए गए बटन को प्रेस करना है. - आयात प्रारंभ - OPML आयात - त्रुटि! - OPML फ़ाइल पढ़ना - OPML दस्तावेज़ पढ़ते समय एक त्रुटि हुई है: - आयात निर्देशिका खाली है. - सभी का चयन करें - सभी का चयन रद्द करें - आयात करने के लिए फ़ाइल चुनें - OPML निर्यात - निर्यात ... - निर्यात त्रुटि - OPML निर्यात सफल. - .ompl फ़ाइल लिखा गया था:\u0020 - - स्लीप टाइमर सेट - स्लीप टाइमर अक्षम - समय दर्ज करें - स्लीप टाइमर - समय बचा है:\u0020 - अवैध इनपुट, समय को पूर्णांक में डालें - - श्रेणियाँ - शीर्ष पॉडकास्ट - सुझाव - gpodder.net खोज - लॉगिन - Gpodder.net प्रवेश प्रक्रिया में आपका स्वागत है.पहले, अपनी प्रवेश जानकारी टाइप करें: - लॉगिन - अगर आप अभी तक कोई खाता नहीं है, तो आप एक यहाँ बना सकते हैं:⏎\nhttps://gpodder.net/register/ - प्रयोक्ता नाम - पासवर्ड - डिवाइस चयन - अपने gpodder.net खाते के उपयोग के लिए एक नई डिवाइस बनाएँ या एक मौजूदा डिवाइस का चयन करें: - डिवाइस आईडी:\u0020 - शीर्षक - नई डिवाइस बनाएँ - मौजूदा डिवाइस चुनें: - डिवाइस आईडी खाली नहीं होना चाहिए - डिवाइस आईडी पहले से ही उपयोग में - चुनें - लॉगिन सफल! - बधाई हो! आपकी gpodder.net खाता अब आपके डिवाइस के साथ जुड़ा हुआ है. AntennaPod अब से स्वचालित रूप से आपके gpodder.net खाते के साथ अपने डिवाइस पर सदस्यता सिंक जाएगा. - अब सिंक प्रारंभ करें - मुख्य स्क्रीन पर जाएं - gpodder.net प्रमाणन त्रुटि - गलत उपयोगकर्ता नाम या पासवर्ड - gpodder.net सिंक त्रुटि - एक त्रुटि सिंक्रनाइज़ के दौरान हुई:\u0020 - - चयनित फ़ोल्डर: - फ़ोल्डर बनाएँ - डेटा फ़ोल्डर चुनें - \"%1$s\" नाम के साथ नया फ़ोल्डर बनाएँ? - नया फ़ोल्डर बनाया - इस फ़ोल्डर में लिख नहीं सकते - फ़ोल्डर पहले से मौजूद है - फ़ोल्डर नहीं बना सका - फ़ोल्डर खाली नहीं है - आपके द्वारा चुने गए फ़ोल्डर खाली नहीं है. मीडिया डाउनलोड और अन्य फ़ाइलें इस फ़ोल्डर में सीधे रखा जाएगा. फिर भी जारी रखें? - डिफ़ॉल्ट फ़ोल्डर चुनें - प्लेबैक रोकें बजाय ध्वनियों को कम करने के अगर कोई अन्य अनुप्रयोग इसे बजाना चाहता है - रुकावट के लिए रोकें - - सदस्यता लें - सदस्यता ली गई - डाउनलोड कर रहा है ... - - - - diff --git a/app/src/main/res/values-it-rIT/strings.xml b/app/src/main/res/values-it-rIT/strings.xml deleted file mode 100644 index 9bc81c269..000000000 --- a/app/src/main/res/values-it-rIT/strings.xml +++ /dev/null @@ -1,289 +0,0 @@ - - - - AntennaPod - Feed - PODCAST - EPISODI - Nuovo - Lista d\'attesa - Impostazioni - Aggiungi podcast - Download - Annulla download - Storico delle riproduzioni - gpodder.net - gpodder.net login - - - - Apri nel browser - Copia URL - Condividi URL - URL copiato negli appunti - - Cancella lo storico - - Conferma - Annulla - Autore - Lingua - Impostazioni - Immagine - Errore - Un errore è stato rilevato: - Aggiorna - Non risulta disponibile lo spazio di archiviazione esterno. Assicurati che lo spazio di archiviazione sia montato per permettere all\'applicazione di funzionare correttamente. - Capitoli - Note dell\'episodio - Descrizione - Episodi Recenti:\u0020 - \u0020episodi - Durata:\u0020 - Dimensione:\u0020 - Elaborazione in corso - Caricamento... - Salva nome utente e password - Chiudi - Riprova - Includi nei download automatici - - URL del feed - Aggiungi un Podcast tramite URL - - Segna tutti come letti - Informazioni - Condividi il link al sito - Condividi il link al feed - Per favore conferma la cancellazione di questo feed e di TUTTI gli episodi collegati che sono stati precedentemente scaricati. - Rimozione feed - - Download - Riproduci - Pausa - Stream - Rimuovi - Segna come letto - Segna come non letto - Aggiungi alla coda - Rimuovi dalla coda - Visita il sito - Flattr this - Accoda tutti - Scarica tutti - Salta episodio - - Download in attesa - Download in corso - Spazio di archiviazione non trovato - Spazio insufficiente - Errore su file - HTTP Data Error - Errore sconosciuto - Parser Exception - Tipo di feed non supportato - Errore di connessione - Host sconosciuto - Annulla tutti i download - Download annullato - Download completati - URL malformato - IO Error - Request error - Errore di accesso al database - \u0020Download rimasti - Download podcast in corso - %1$d download con successo, %2$d ko - Titolo sconosciuto - Feed - Media file - Immagine - Rilevato errore durante il download del file:\u0020 - - Errore! - Nessun media in riproduzione - Preparazione - Pronto - Ricerca posizione - Server died - Errore sconosciuto - Nessun media in riproduzione - 00:00:00 - Buffering - Riproduzione podcast in corso - - Svuota la coda - Undo - Oggetto rimosso - Sposta all\'inizio - Sposta in fondo - - Flattr sign-in - Premi il tasto seguente per iniziare il processo di autenticazione. Sarai trasferito alla pagina di login di flattr sul tuo browser e ti sarà richiesto di garantire ad AntennaPod il permesso di effettuare microdonazioni. Dopo la tua autorizzazione, sarai riportato alla seguente schermata in modo automatico. - Autenticazione - Ritorna alla home - Autenticazione avvenuta con successo! Adesso puoi microdonare con flattr dall\'interno dell\'app. - Nessun token flattr trovato - Il tuo account flattr non sembra essere collegato ad AntennaPod. Potresti collegare il tuo account ad AntennaPod per utilizzare flattr dall\'app oppure puoi visitare il sito per utilizzare flattr direttamente da lì. - Autenticazione - Azione inibita - AntennaPod non ha il permesso di effettuare questa azione. La ragione potrebbe essere che il token di accesso di AntennaPod al tuo account è stato revocato. Puoi eseguire la re-autenticazione o altrimenti visitare il sito web. - Accesso revocato - Hai revocato l\'accesso di AntennaPod al tuo account. Al fine di completare il processo devi rimuovere l\'app dalla lista delle applicazioni autorizzare nelle impostazioni del tuo account sul sito di flattr. - - AntennaPod sta eseguendo Flattr - - Scarica Plugin - Plugin non installato - Per la riproduzione a velocità variabile deve essere installata una libreria di terze parti.\n\nPremi \'Scarica Plugin\' per scaricare un plugin gratuito dal Play Store.\n\nEventuali problemi riscontrati utilizzando questo plugin non sono da imputare ad AntennaPod e devono essere segnalati al proprietario plugin. - Velocità di riproduzione - - Non ci sono oggetti in questa lista. - Non sei ancora abbonato a nessun feed. - - Altro - Informazioni - Coda - Servizi - Flattr - Metti in pausa quanto le cuffie vengono disconnesse - Passa al prossimo episodio in coda quanto si completa una riproduzione - Riproduzione - Rete - Intervallo di update - Specifica un intervallo per l\'aggiornamento automatico dei feed o disabilitalo - Abilita il download dei media solo tramite WiFi - Playback continuo - Download dei media su WiFi - Disconnessione cuffie - Update su rete mobile - Permetti gli aggiornamenti tramite connessione dati mobile - Aggiornamento - Impostazioni Flattr - Flattr sign-in - Collega il tuo account flattr per utilizzare flattr direttamente dall\'app - Supporta con flattr questa app - Supporta lo sviluppo di AntennaPod tramite flattr. Grazie! - Revoca l\'accesso - Revoca il permesso, a questa applicazione, di accedere al tuo account flattr. - Interfaccia utente - Seleziona il tema - Cambia l\'aspetto di AntennaPod - Download automatico - Configura il download automatico degli episodi - Abilita il filtro Wi-Fi - Abilita il download automatico solo per alcune reti Wi-Fi selezionate. - Cache degli episodi - Light - Dark - Illimitato - ore - ora - Manuale - Login - Effettua il login con il tuo account gpodder.net per sincronizzare le tue sottoscrizioni. - Logout - Logout effettuato - Cambia le informazioni di login - Cambia le informazioni di login per il tuo account gpodder.net. - Velocità di riproduzione - Personalizza le velocità disponibili per la riproduzione audio a velocità variabile - Imposta l\'hostname - Usa l\'host di default - - - Ricerca per Feed o Episodi - Trovato nelle note dell\'episodio - Trovato nei capitoli - Nessun risultato trovato - Ricerca - Trovato nel titolo - - I file OPML ti permettono di spostare i tuoi podcast da un programma ad un altro. - Per importare un file OPML devi posizionarlo nella directory indicata e premere il tasto seguente in modo da iniziare il processo di importazione. - Avvio importazione - Importazione OPML - ERRORE! - Lettura OPML file in corso - Un errore è stato rilevato mentre era in corso la lettura del documento opml: - La directory di importazione è vuota. - Seleziona tutti - Deseleziona tutti - Scegli il file da importare - Esportazione su OPML - Esportazione in corso... - Errore di esportazione - Esportazione OPML avvenuta con successo. - Il file .opml è stato scritto su:\u0020 - - Imposta timer - Disabilita il timer di spegnimento - Tempo di spegnimento - Timer di spegnimento - Tempo residuo:\u0020 - Input non valido, il campo deve essere un numero intero. - - CATEGORIE - TOP PODCAST - SUGGERIMENTI - Cerca su gpodder.net - Login - Benvenuto sul processo di login di gpodder.net. Per prima cosa, inserisci le tue informazioni di login: - Login - Se non possiedi ancora un account, puoi crearlo uno qui:\nhttps://gpodder.net/register/ - Username - Password - Scelta del dispositivo - Crea un nuovo dispositivo per utilizzare il tuo account gpodder.net o scegline uno esistente: - ID del dispositivo:\u0020 - Caption - Crea un nuovo dispositivo - Scegli un dispositivo esistente: - L\'ID del dispositivo non può essere vuoto - ID di dispositivo già in uso - Scegli - Login effettuato! - Congraturazioni! Il tuo account gpodder.net è stato collegato con il tuo dispositivo. Da ora AntennaPod sincronizzerà automaticamente le sottoscrizioni sul tuo dispositivo con il tuo account gpodder.net. - Avvia la sincronizzazione - Schermata principale - gpodder.net errore di autenticazione - Utente o password errata - gpodder.net errore di sincronizzazione - Rilevato un errore in fase di sincronizzazione:\u0020 - - Seleziona la directory: - Crea una directory - Scegli la directory per i dati - Crea una nuova directory con nome \"%1$s\"? - Crea una nuova directory - Impossibile scrivere in questa directory - La directory esiste già - Impossibile creare la directory - La directory non è vuota - La directory che hai selezionato non è vuota. I download dei media e altri file saranno creati in questa directory. Continuare? - Scegli la directory predefinita - Sospendi la riproduzione invece di abbassare il volume quando un\'altra app emette un suono - Pausa su interruzione - - Abbonati - Abbonato - Download in corso... - - Mostra i capitoli - Mostra le note dell\'episodio - Mosta l\'immagine - Riavvolgi - Avanti veloce - Audio - Video - Naviga su - Più azioni - L\'episodio è in corso di ripoduzione - L\'episodio sta per essere scaricato - L\'episodio è stato scaricato - L\'oggetto è nuovo - L\'episodio è in coda - Numero dei nuovi episodi - - - diff --git a/app/src/main/res/values-iw-rIL/strings.xml b/app/src/main/res/values-iw-rIL/strings.xml deleted file mode 100644 index 27f4b969d..000000000 --- a/app/src/main/res/values-iw-rIL/strings.xml +++ /dev/null @@ -1,305 +0,0 @@ - - - - אנטנה-פוד - הזנות - פודקאסטים - פרקים - חדש - רשימת המתנה - הגדרות - הוסף פודקאסט - הורדות - בטל הורדה - היסטוריית ניגון - gpodder.net - התחברות אל gpodder.net - - - - פתח בדפדפן - העתק כתובת אתר - שתף כתובת אתר - כתובת אתרהועתקה ללוח. - - נקה היסטוריה - - אישור - בטל - מחבר - שפה - הגדרות - תמונה - שגיאה - אירעה שגיאה: - רענן - אין אחסון חיצוני זמין. אנא ודא כי אחסון חיצוני הוא מותקן כך שהאפליקציה תוכל לעבוד כמו שצריך. - פרקים - הערות פרק - תיאור - הפרק האחרון:\u0020 - \u0020פרקים - אורך:\u0020 - גודל:\u0020 - מעבד - טוען... - שמור שם משתמש וססמה - סגור - נסה שוב - כלול בהורדות אוטומטיות - - כתובת הזנה - הוסף פודקאסט לפי כתובת אתר - - סמן הכל כנקרא - הצג מידע - שתף קישור אתר - שתף קישור הזנה - אשר מחיקת הזנה זו ואת כל פרקי ההזנה שהורדת. - הסר הזנה - - הורד - נגן - השהה - הזרם - הסר - סמן כנקרא - סמן כלא נקרא - הוסף לתור - הסר מהתור - בקר באתר - תרום באמצעות Flattr - הכנס לתור הכל - הורד הכל - דלג על הפרק - - הורדה עתידית - הורדה מתבצעת - התקן איחסון לא נמצא - אין די שטח איחסון - שגיאת קובץ - שגיאת מידע HTTP - שגיאה לא ידועה - שגיאת תוכנית ניתוח - סוג ההזנה אינו נתמך - שגיאת חיבור - שרת לא ידוע - שגיאת אימות - בטל את כל ההורדות - הורדה בוטלה - הורדות הושלמו - כתובת אתר שגויה - שגיאת קלט פלט - שגיאת בקשה - שגיאת גישה למסד הנתונים - הורדות נותרוu0020\ - מוריד פודקאסט - %1$d הורדות הצליחו, %2$d ניכשלו - כותרת לא ידועה - הזנה - קובץ מדיה - תמונה - שגיאה אירעה בעת הניסיון הורדת הקובץ:\u0020 - נידרש אימות - המשאב אותה ביקשת דורש שם משתמש וססמה - - שגיאה! - מדיה לא מתנגנת - מתכונן - מוכן - מחפש - שרת מת - שגיאה לא ידועה - מדיה לא מתנגנת - 00:00:00 - ממלא חוצץ - מנגן פודקאסט - - נקה תור - בטל - הסר פריט - העבר למעלה - העבר למטה - - כניסה ל-Fattr - לחץ על הכפתור למטה כדי להתחיל את תהליך האימות. אתה תועבר למסך כניסת flattr בדפדפן שלך ותתבקש לתת לאנטנה-פוד רשות לתרום באמצעות flattr. לאחר שקבלת אישור, תוכל לחזור למסך זה באופן אוטומטי. - אימות - חזור למסך הבית - האימות הצליח! עכשיו אתה יכול לתרום באמצעות flattr מתוך האפליקציה. - אסימון flattr לא נמצא - חשבון ה-flattr שלך אינו מחובר לאנטנה-פוד. אתה יכול לקשראת לחשבונך לאנטנה-פוד לתרום באמצעות flattr מתוך האפליקציה או שאתה יכול לבקר באתר האינטרנט של הדבר לו תרצה לתרום. - אמת - הפעולה אסורה - לאנטנה-פוד אין הרשאה לפעולה זו. הסיבה לכך יכולה להיות שאסימון הגישה של אנטנה-פוד לחשבון שלך בוטל. אתה יכול לבצע אימות מחדש או לבקר באתר האינטרנט של הדבר במקום. - גישה בוטלה - אסימון הגישה של אנטנה-פוד לחשבונך בוטל. על מנת להשלים את התהליך, אתה צריך להסיר יישום זה מהרשימת היישומים שאושרו בהגדרות החשבונך באתר flattr. - - תרמת ב-Flattr! - תרמת ב-Flattr %d פעמים! - תרומות Flattr: %s. - כישלון לתרום ב-Flattr %d! - לא נתרם ב-Flattr: %s. - תרומות ב-Flattr מאוחר יותר - תורם ב-Flattr %s - אנטנה-פוד תורם ב-Flattr - אנטנה-פוד תרם ב-Flattr - כישלון תרומת אנטנה-פוד ב-Flattr - איחזור תרומות Flattr - - הורד תוסף - תוסף לא מותקן - לניגון במהירות משתנה תוסף מגורם שלישי צריך להיות מותקן. \n\nהקש על \'הורד תוסף\' להוריד תוסף חינמי מחנות Play\n\nבעיות בשימוש עם תוסף זה אינן באחריות אנטנה-פוד וצריך לדווחן ליוצר התוסף. - מהירויות ניגון - - אין פריטים ברשימה זו. - לא נרשמת עדיין להזנות. - - אחר - אודות - תור - שירותים - Flattr - השהה השמעה בניתוק האוזניות - עבור לפריט הבא בתור כאשר הניגון מסתיים - ניגון - רשת - זמן בין עידכונים - ציין פרק זמן שבו ההזנות עוברות רענון באופן אוטומטי או לבטל ריענון - הורד קבצי מדיה רק דרך חיבור אינטרנט אלחוטי - ניגון מתמשך - הורדת מדיה דרך אינטרנט אלחוטי - ניתוק אוזניות - עידכון דרך רשת סלולרית - אפשר עידכונים דרך רשת סלולרית - מרענן - הגדרות Flattr - כניסה ל-Fattr - היכנס לחשבון שלך לflattr לתרום ישירות מתוך האפליקציה. - תרום באמצעות Flattr לאפליקציה זו - תמוך בפיתוח אנטנה-פוד בתרומה עם Flattr. תודה! - בטל גישה - בטל הרשאת גישה לחשבון flattr ליישום זה. - תרומות Flattr אוטומטיות - ממשק משתמש - בחר ערכת נושא - שנה את מראה אנטנה-פוד - הורדה אוטומטית - הגדר הורדה אטומטית של פרקים. - אפשר סינון אינטרנט אלחוטי - אפשר הורדה אוטומטית דרך רשתות אלחוטייות נבחרות. - מטמון פרקים - בהיר - כהה - בלתי מוגבל - שעות - שעה - ידני - כניסה - כנס עם חשבון gpodder.net שלך על מנת לסנכרן את ההרשמות שלך. - התנתקות - ההתנתקות הייתה מוצלחת - שינוי פרטי התחברות - שנה פרטי התחברות של חשבון gpodder.net. - מהירויות ניגון - התאמת המהיריות הזמינות לניגון במהירות משתנה - הגדר שם שרת - השתמש בשרת ברירת מידל - - - חפש הזנות או פרקים - נמצא בהערות פרק - נמצא בפרקים - אין תוצאות - חיפוש - נמצא בכותרת - - קבצי OPML יאפשרו לכך לנייד פודקאסטים מלוכד פודקאסטים אחד למשנו. - לייבא קובץ OPML, אתה צריך למקם אותו בספרייה הבאה וללחוץ על הכפתור למטה כדי להתחיל את תהליך היבוא. - התחל יבוא - יבוא OPML - שגיאה! - קורא קובץ OPML - אירעה שגיאה בזמן קריאת קובץ OPML: - ספריית היבוא ריקה. - בחר הכל - בטל בחירות - בחר קובץ ליבוא - יצוא OPML - מייצא... - שגיאת יצוא - יצוא OPML הצליח. - קובץ OPML נכתב ל:\u0020 - - קבע טיימר שינה - בטל טיימר שינה - קבע זמן - טיימר שינה - זמן נותר:\u0020 - קלט לא חוקי, זמן חייב להיות מספר שלם - - קטגוריות - פודקאסטים בכירים - המלצות - חפש ב-gpodder.net - התחברות - ברוך הבא להתחברות ל-gpodder.net. ראשית, הקלד את פרטי הכניסה שלך: - התחברות - אם אין לך עדיין חשבון, אתה יכול ליצור אחד כאן:\nhttps://gpodder.net/register/ - שם משתמש: - ססמה: - בחירת מכשיר - צור מכשיר חדש לשימוש עבור חשבון gpodder.net או לבחר אחד קיים: - מזהה מכשיר:\u0020 - כותרת - צור מכשיר חדש - בחר מכשיר קיים: - מזהה המכשיר אינו יכול להיות ריק - מזהה המכשיר בשימוש - בחר - התחברות מוצלחת! - מזל טוב! חשבון gpodder.net שלך מקושר כעת עם המכשיר שלך. אנטנה-פוד מעתה יסנכרן באופן אוטומטי הרשמות במכשיר שלך עם חשבון gpodder.net שלך. - התחל סנכרון כעת - עבור למסך הראשי - שגיאת אימות של gpodder.net - שם משתמש או ססמה שגויים - שגיאת סנכרון של gpodder.net - שגיאה במהל סינכרון:\u0020 - - תיקיה נבחרת: - צור תיקיה - בחר תיקיית מידע - צור תיקיה חדשה בשם \"%1$s\"? - תיקיה חדשה נוצרה - לא ניתן לכתוב לתיקה זו - תיקה כבר קיימת - לא ניתן ליצור תיקיה - התיקיה אינה ריקה - התיקייה שבחרת אינה ריקה. הורדות מדיה וקבצים אחרים יהיו ממוקמות ישירות בתיקייה זו. להמשיך בכל זאת? - בחר תיקיית ברירת מחדל - השהה ניגון במקום החלשת עוצמת שמע כשאפליקציה אחרת מנגנת - השהה בזמן הפרעה - - הרשם - נרשם - מוריד... - - הצג פרקים - הצג הערות פרק - הצג תמונה - הרץ לאחור - הרץ קדימה - שמע - וידאו - נווט למעלה - עוד פעולות - הפרק מתנגן - הפרק יורד - הפרק ירד - פריט חדש - הפרק בתור - מספר הפרקים החדשים - מספר הפרקים שהתחלת להאזין להם - - - מייבא רישום מאפליקציות יעודיות... - diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml deleted file mode 100644 index 4d783b83a..000000000 --- a/app/src/main/res/values-ko/strings.xml +++ /dev/null @@ -1,305 +0,0 @@ - - - - 안테나팟 - 피드 - 팟캐스트 - 에피소드 - 신규 - 추가 대기 목록 - 설정 - 팟캐스트 추가 - 다운로드 - 다운로드 취소 - 재생 기록 - gpodder.net - gpodder.net 로그인 - - - - 브라우저에서 열기 - URL 복사 - URL 공유 - URL을 클립보드에 복사했습니다. - - 기록 지우기 - - 확인 - 취소 - 저자 - 언어 - 설정 - 그림 - 오류 - 오류가 발생했습니다: - 새로 고침 - 외부 저장 장치가 없습니다. 앱이 제대로 동작하려면 외부 저장장치를 마운트하십시오. - 챕터 - 프로그램 메모 - 설명 - 가장 최근 에피소드:\u0020 - \u0020에피소드 - 길이:\u0020 - 크기:\u0020 - 처리 중 - 읽어들이는 중... - 사용자 이름 및 암호 저장 - 닫기 - 다시 시도 - 자동 다운로드에 포함 - - 피드 URL - URL로 팟캐스트를 추가 - - 모두 읽은 것으로 표시 - 정보 표시 - 홈페이지 링크 공유 - 피드 링크 공유 - 이 피드와 이 피드에서 다운로드한 모든 에피소드를 삭제하시려면 확인을 누르십시오. - 피드 삭제하는 중 - - 다운로드 - 재생 - 일시 중지 - 스트리밍 - 제거 - 읽은 것으로 표시 - 읽지 않은 것으로 표시 - 대기열에 추가 - 대기열에서 제거 - 홈페이지 보기 - Flattr하기 - 모두 대기열에 추가 - 모두 다운로드 - 에피소드 건너뛰기 - - 다운로드 지연 중 - 다운로드 실행 중 - 저장 장치가 없습니다 - 저장 공간이 부족합니다 - 파일 오류 - HTTP 데이터 오류 - 알 수 없는 오류 - 파서 프로그램 예외 - 지원하지 않는 피드 종류 - 연결 오류 - 알 수 없는 호스트 - 인증 오류 - 모든 다운로드 취소 - 다운로드 취소됨 - 다운로드 마침 - URL 형식 틀림 - 입출력 오류 - 요청 오류 - 데이터베이스 접근 오류 - 개\u0020다운로드 남음 - 팟캐스트 데이터 다운로드 중 - 다운로드 %1$d개 성공, %2$d개 실패 - 알 수 없는 제목 - 피드 - 미디어 파일 - 그림 - 파일을 다운로드하는 중 오류가 발생했습니다:\u0020 - 인증이 필요합니다 - 요청한 자원은 사용자 이름과 암호가 필요합니다 - - 오류! - 재생 중인 미디어 없음 - 준비하는 중 - 준비 완료 - 이동 중 - 서버가 죽었습니다 - 알 수 없는 오류 - 재생 중인 미디어 없음 - 00:00:00 - 버퍼링 중 - 팟캐스트 재생 중 - - 대기열 지우기 - 실행 취소 - 항목을 제거했습니다 - 맨 위로 이동 - 맨 아래로 이동 - - Flattr 로그인 - 인증 절차를 시작하려면 아래 버튼을 누르십시오. 브라우저의 Flattr 로그인 화면으로 이동하고, 안테나팟에 Flattr를 사용을 허락 여부를 물어봅니다. 허락을 하면 자동으로 이 화면으로 돌아옵니다. - 인증 - 홈으로 돌아가기 - 인증이 성공했습니다! 이제 앱에서 Flattr 기능을 사용할 수 있습니다. - Flattr 토큰이 없습니다 - Flattr 계정이 안테나팟에 연결되지 않은 것 같습니다. 앱 안에서 안테나팟을 Flattr 계정에 연결할 수도 있고, Flattr 홈페이지에서 Flattr할 거리를 선택할 수 있습니다. - 인증 - 금지된 동작입니다 - 안테나팟에 이 동작을 할 권한이 없습니다. 가능한 원인은 안테나팟이 계정에 접근할 때 사용하는 토큰이 철회된 경우입니다. 다시 인증할 수도 있고, 직접 웹페이지를 이용할 수도 있습니다. - 접근이 철회되었습니다 - 안테나팟에서 계정에 대한 접근 토큰을 성공적으로 철회했습니다. 절차를 마치려면 Flattr 홈페이지, 계정 설정의 허용하는 응용 프로그램 목록에서 이 앱을 제거해야 합니다. - - 1개 Flattr했습니다! - %d개 Flattr했습니다! - Flattr함: %s - %d개 Flattr하는데 실패했습니다! - Flattr하지 않음: %s. - 나중에 Flattr합니다 - %s Flattr하는 중 - 안테나팟에서 Flattr하는 중 - 안테나팟에서 Flattr했음 - 안테나팟에서 Flattr 실패 - Flattr한 내용 가져오는 중 - - 다운로드 플러그인 - 플러그인을 설치하지 않았습니다 - 여러가지 속도로 재생하려면 외부 라이브러리를 설치해야 합니다.\n\n플레이 스토어에서 무료 플러그인을 설치하려면 \"플러그인 다운로드\"를 누르십시오.\n\n이 플러그인에서 발생하는 문제는 안테나팟의 책임이 아니므로 플러그인 개발자에게 문의하십시오. - 재생 속도 - - 이 목록에 항목이 없습니다. - 아직 어떤 피드도 구독하지 않았습니다. - - 기타 - 정보 - 대기열 - 서비스 - Flattr - 헤드폰의 연결이 끊어졌을 때 재생을 일시 중지 - 재생을 마쳤을 때 다음 대기열로 이동 - 재생 - 네트워크 - 업데이트 주기 - 피드를 새로 고칠 주기를 지정하거나 새로 고침을 하지 않음 - Wi-Fi를 통해서만 미디어 파일 다운로드 - 연속 재생 - Wi-Fi 미디어 다운로드 - 헤드폰 연결 끊김 - 휴대전화망 업데이트 - 휴대전화 데이터 연결을 통해 업데이트 허용 - 새로 고치는 중 - Flattr 설정 - Flattr 로그인 - Flattr 계정에 로그인하면 앱에서 직접 Flattr할 수 있습니다. - 이 앱 Flattr하기 - Flattr해서 안테나팟 개발을 지원할 수 있습니다. 고맙습니다! - 접근 철회 - 이 앱이 Flattr 계정에 접근할 권한을 철회합니다. - 자동 Flattr - 사용자 인터페이스 - 테마 선택 - 안테나팟의 겉모양을 바꿉니다. - 자동 다운로드 - 에피소드 자동 다운로드를 설정합니다. - Wi-Fi 필터 사용 - 선택한 Wi-Fi 네트워크에 대해서만 자동 다운로드를 허용합니다. - 에피소드 임시 저장 - 밝게 - 어둡게 - 무제한 - 시간 - 시간 - 수동 지정 - 로그인 - gpodder.net 계정으로 로그인해서 구독 정보를 동기화 - 로그아웃 - 로그아웃 성공 - 로그인 정보 바꾸기 - gpodder.net 계정의 로그인 정보를 바꿉니다. - 재생 속도 - 여러가지 오디오 재생 속도 직접 설정 - 호스트 이름 설정 - 기본 호스트 사용 - - - 피드나 에피소드 검색 - 프로그램 메모에서 발견 - 챕터에서 발견 - 검색 결과가 없습니다 - 검색 - 제목에서 발견 - - OPML 파일을 이용하면 팟캐스트 목록을 한 팟캐스트 프로그램에서 다른 팟캐스트 프로그램으로 옮길 수 있습니다. - OPML 파일을 가져오려면, 다음 디렉터리에 파일을 저장하고 아래 버튼을 누르면 가져오기 처리를 시작합니다. - 가져오기 시작 - OPML 가져오기 - 오류! - OPML 파일을 읽는 중 - OPML 문서를 읽는 중 오류가 발생했습니다: - 가져오기 디렉터리가 비어 있습니다. - 모두 선택 - 모두 선택 해제 - 가져올 파일을 고르십시오 - OPML 내보내기 - 내보내는 중... - 내보내기 오류 - OPML 내보내기가 성공했습니다. - OPML 파일을 다음에 저장했습니다:\u0020 - - 취침 타이머 설정 - 취침 타이머 사용 않음 - 시간 입력 - 취침 타이머 - 남은 시간:\u0020 - 입력이 잘못되었습니다. 시간으로 숫자를 입력해야 합니다. - - 분류 - 상위 팟캐스트 - 추천 - gpodder.net 검색 - 로그인 - gpodder.net 로그인입니다. 먼저 로그인 정보를 입력하십시오: - 로그인 - 아직 계정이 없으면, 다음 사이트에서 만들 수 있습니다:\nhttps://gpodder.net/register/ - 사용자 이름 - 암호 - 장치 선택 - gpodder.net 계정에서 사용할 장치를 새로 만들거나 기존 장치를 선택하십시오: - 장치 아이디:\u0020 - 설명 - 새 장치 만들기 - 기존 장치 선택: - 장치 ID는 비어 있으면 안 됩니다 - 장치 ID를 이미 사용 중입니다 - 선택 - 로그인이 성공했습니다! - 축하합니다! gpodder.net 계정이 장치와 연결되었습니다. 이제 안테나팟에서 gpodder.net 계정의 구독 정보와 자동으로 동기화합니다. - 지금 동기화 시작 - 메인 화면으로 이동 - gpodder.net 인증 오류 - 잘못된 사용자 이름 또는 암호 - gpodder.net 동기화 오류 - 동기화 중에 오류가 발생했습니다:\u0020 - - 선택한 폴더: - 폴더 만들기 - 데이터 폴더 선택 - 이름이 \"%1$s\"인 폴더를 만드시겠습니까? - 새 폴더를 만들었습니다 - 이 폴더에 쓸 수 없습니다 - 폴더가 이미 있습니다 - 폴더를 만들 수 없습니다 - 폴더가 비어 있지 않습니다 - 선택한 폴더가 비어 있지 않습니다. 다운로드한 미디어 파일 및 기타 파일이 이 폴더에 저장됩니다. 그래도 계속 하시겠습니까? - 기본 폴더 선택 - 다른 앱이 소리를 낼 때 볼륨을 줄이지 않고 재생을 일시 중지 - 끼어들면 일시 중지 - - 구독 - 구독함 - 다운로드하는 중... - - 챕터 보이기 - 프로그램 메모 표시 - 그림 보이기 - 뒤로 감기 - 앞으로 감기 - 오디오 - 비디오 - 위 단계로 이동 - 기타 동작 - 에피소드를 재생하는 중입니다 - 에피소드를 다운로드하는 중입니다 - 에피소드를 다운로드했습니다 - 새로운 항목입니다 - 에피소드가 대기열에 들어 있습니다 - 새 에피소드 개수 - 듣기를 시작한 에피소드 개수 - - - 단일 용도 앱에서 구독 정보를 가져옵니다... - diff --git a/app/src/main/res/values-land/styles.xml b/app/src/main/res/values-land/styles.xml deleted file mode 100644 index d964ef3d4..000000000 --- a/app/src/main/res/values-land/styles.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/values-large/dimens.xml b/app/src/main/res/values-large/dimens.xml deleted file mode 100644 index 27b4868c7..000000000 --- a/app/src/main/res/values-large/dimens.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - 170dp - 80dp - 80dp - @dimen/text_size_medium - \ No newline at end of file diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml deleted file mode 100644 index a0c852059..000000000 --- a/app/src/main/res/values-nl/strings.xml +++ /dev/null @@ -1,305 +0,0 @@ - - - - AntennaPod - Feeds - PODCASTS - AFLEVERINGEN - Nieuw - Wachtlijst - Instellingen - Podcast toevoegen - Downloads - Annuleer download - Afspeelgeschiedenis - gpodder.net - gpodder.net login - - - - In de browser openen - URL kopieren - URL delen - URL naar klembord gekopieerd. - - Geschiedenis wissen - - Bevestig - Annuleer - Auteur - Taal - Instellingen - Beeld - Fout - Er is een fout opgetreden: - Verversen - Geen externe opslag beschikbaar. Zorg ervoor dat de externe opslag gemonteerd is, zodat de app goed kan werken. - Hoofdstukken - Shownotes - Beschrijving - Meest recente aflevering:\u0020 - \u0020afleveringen - Lengte:\u0020 - Grootte:\u0020 - Aan het verwerken - Laden... - Gebruikersnaam en wachtwoord opslaan - Sluiten - Opnieuw proberen - Voor het automatisch downloaden beschouwen - - Feed URL - Podcast toevoegen bij URL - - Alles als gelezen markeren - Toon informatie - Website link delen - Feed link delen - Bevestig dat u deze feed en ALLE afleveringen van deze feed die u hebt gedownload wilt verwijderen. - Feed verwijderen - - Download - Spelen - Pauze - Stream - Verwijderen - Als gelezen markeren - Als ongelezen markeren - Voeg toe aan wachtrij - Verwijder van wachtrij - Website bezoeken - Flattr dit - Alle in wachtrij plaatsen - Alles downloaden - Aflevering overslaan - - Download in afwachting - Aan het downloaden - Opslagmedium niet gevonden - Onvoldoende ruimte - Bestandsfout - HTTP data fout - Onbekende fout - Parser Exception - Niet ondersteunde feed soort - Verbindingsfout - Onbekende host - Authenticatie fout - Alle downloads annuleren - Download geannuleerd - Downloads afgerond - Misvormde URL - IO fout - Fout in de aanvraag - Databasetoegangsfout - Nog \u0020 downloads - Podcast gegevens aan het downloaden - %1$d downloads geslaagd, %2$d mislukt - Onbekende titel - Feed - Mediabestand - Beeld - Er is een fout opgetreden bij het ​​downloaden van bestand:\u0020 - Authenticatie vereist - De opgevraagde bron vereist een gebruikersnaam en een wachtwoord - - Fout! - Geen media aan het afspelen - Voorbereiding - Klaar - Aan het opzoeken - Server antwoord niet - Onbekende fout - Geen media aan het afspelen - 00:00:00 - Buffering - Podcast aan het afspelen - - Wachtrij leeg maken - Ongedaan maken - Item verwijderd - Naar boven verplaatsen - Naar beneden verplaatsen - - Flattr inloggen - Druk op onderstaande knop om het verificatieproces te starten. U wordt doorgestuurd naar de Flattr inlogscherm in uw browser en wordt gevraagd om toestemming aan AntennaPod te geven om dingen te Flattr\'en. Nadat u toestemming hebt gegeven, keert u automatisch terug naar dit scherm. - Authenticeren - Terug naar de startscherm - Authenticatie is geslaagd! U kunt nu dingen vanuit de app Flattr\'en. - Geen Flattr token gevonden - Uw Flattr account lijkt niet aangesloten te zijn op AntennaPod. U kunt uw account aan AntennaPod sluiten om dingen vanuit de app te Flattr\'en, of u kunt op de website van het ding terecht om het daar te Flattr\'en. - Authenticeren - Actie verboden - AntennaPod heeft geen toestemming voor deze actie. De reden hiervoor zou kunnen zijn dat de toegang token van AntennaPod voor uw account ingetrokken is. U kunt opnieuw authenticeren, of de website van het ding bezoeken. - Toegang ingetrokken - U heeft met succes het toegangstoken van AntennaPod tot uw account ingetrokken. Om het proces te voltooien, moet u deze app uit de lijst van goedgekeurde applicaties in uw accountinstellingen op de Flattr website verwijderen. - - Een ding geflattr\'d - %d dingen geflattr\'d! - Geflattr\'d: %s. - Kon %d dingen niet flattr\'n! - Niet geflattr\'d: %s. - Ding wordt later geflattr\'d - %s aan het flattren - AntennaPod is aan het flattren - AntennaPod heeft geflattr\'d - AntennaPod flattr niet gelukt - Geflattr\'de dingen aan het ontvangen - - Plugin downloaden - Plugin niet geinstalleerd - Voor variabele afspeelsnelheid moet er een derde partij bibliotheek geïnstalleerd worden.\n\nTik op \'Plugin downloaden\' om een ​​gratis plugin te downloaden uit de Play Store.\n\nEventuele problemen gevonden door het gebruik van deze plugin zijn niet de verantwoordelijkheid van AntennaPod en moeten aan de plugin ontwikkelaar gemeld worden. - Afspeelsnelheden - - Er zijn geen items in deze lijst. - U bent nog tot geen enkele feed geabonneerd. - - Overig - Over AntennaPod - Wachtrij - Services - Flattr - Afspelen pauzeren wanneer de hoofdtelefoon wordt losgekoppeld - Volgende wachtrij item afspelen als de episode voltooid is - Afspelen - Netwerk - Update interval - Voer een tijdsinterval in waarin de feeds automatisch worden vernieuwd, of schakel het uit - Download mediabestanden alleen via WiFi - Continu afspelen - WiFi download van media - Loskoppeling van de hoofdtelefoon - Mobiele updates - Updates toestaan ​​via de mobiele dataverbinding - Aan het verversen - Flattr settings - Flattr inlog - Log in je Flattr account om dingen rechtstreeks vanuit de app te flattr\'en. - Flattr deze app - Ondersteun de ontwikkeling van AntennaPod door het te flattr\'en. Bedankt! - Toegang intrekken - Trek de toegang van deze app in tot je Flattr account. - Automatische Flattr - User Interface - Kies theme - Verander het uiterlijk van AntennaPod. - Automatisch downloaden - Configureer het automatisch downloaden van afleveringen. - Wi-Fi filter inschakelen - Automatisch downloaden alleen toestaan voor geselecteerde Wi-Fi-netwerken. - Afleveringen cache - Licht - Donker - Onbeperkt - uren - uur - Handmatig - Log in - Log met je gpodder.net account in om je abonnementen te synchroniseren. - Log uit - Uitlog was succesvol - Aanmeldingsgegevens wijzigen - Wijzig de aanmeldingsgegevens van je gpodder.net account. - Afspeelsnelheden - Pas de beschikbare snelheden aan voor de variabele audio afspeelsnelheid - Definieer hostname - Gebruik standaard host - - - Feeds of afleveringen zoeken - Gevonden in de shownotes - Gevonden in hoofdstukken - Er zijn geen resultaten gevonden - Zoeken - Gevonden in de titel - - Met OPML-bestanden kan je podcasts van de ene naar de andere podcatcher verplaatsen. - Om een OPML-bestand te importeren moet je het in de volgende map zetten en op onderstaande knop drukken. - Start importeren - OPML import - FOUT! - OPML-bestand aan het lezen - Er is een fout opgetreden bij het lezen van het OPML-bestand: - De import map is leeg. - Selecteer alles - Deselecteer alles - Kies het te importeren bestand - OPML export - Aan het exporteren... - Export fout - OPML export succesvol. - Het OPML-bestand is in \u0020 geplaatst - - Slaap timer instellen - Slaap timer uitschakelen - Voer tijd in - Slaap timer - Resterende tijd:\u0020 - Ongeldige invoer, de tijd moet een geheel getal zijn - - CATEGORIEËN - TOP PODCASTS - SUGGESTIES - Zoek gpodder.net - Log in - Welkom op de gpodder.net login proces. Eerst, typ je login gegevens: - Log in - Als je nog geen account hebt, kun je er hier een aanmaken:\n https://gpodder.net/register/ - Gebruikersnaam - Wachtwoord - Apparaatselectie - Maak een nieuw apparaat aan om voor je gpodder.net account te gebruiken of kies een bestaande: - Device ID:\u0020 - Titel - Maak een nieuw apparaat aan - Kies een bestaand apparaat: - Apparaat ID mag niet leeg zijn - Apparaat ID al in gebruik - Kies - Login succesvol - Gefeliciteerd! Jou gpodder.net account is nu verbonden met je apparaat. AntennaPod zal voortaan abonnementen op je apparaat automatisch met je gpodder.net account synchroniseren. - Synchronisatie nu starten - Terug naar hoofdscherm - gpodder.net authenticatie fout - Ongeldig gebruikersnaam of wachtwoord - gpodder.net synchronisatie fout - Er is een fout opgetreden tijdens het synchroniseren:\u0020 - - Geselecteerde map: - Map aanmaken - Kies data map - Maak een nieuwe map aan met de naam \"%1$s\"? - Nieuwe map aangemaakt - Kan in deze map niet schrijven - Map bestaat al - Kon map niet aanmaken - Map is niet leeg - De map die je hebt gekozen is niet leeg. Media downloads en andere bestanden zullen rechtstreeks in deze map geplaatst worden. Toch doorgaan? - Kies default map - Het afspelen onderbreken in plaats van het volume te verlagen wanneer er een andere app geluiden af wilt spelen - Pauze voor onderbrekingen - - Abonneren - Geabonneerd - Aan het downloaden - - Hoofdstukken tonen - Shownotes tonen - Beeld tonen - Terugspoelen - Vooruitspoelen - Audio - Video - Navigeer naar boven - Meer acties - Aflevering wordt gespeeld - Aflevering wordt gedownload - Aflevering is gedownload - Item is nieuw - Aflevering is in de queue - Aantal nieuwe afleveringen - Aantal afleveringen dat begonnen te luisteren zijn - - - Abonnementen aan het importeren vanuit single-purpose apps... - diff --git a/app/src/main/res/values-pl-rPL/strings.xml b/app/src/main/res/values-pl-rPL/strings.xml deleted file mode 100644 index fc56ab6bf..000000000 --- a/app/src/main/res/values-pl-rPL/strings.xml +++ /dev/null @@ -1,330 +0,0 @@ - - - - AntennaPod - Kanały - Dodaj podcast - PODCASTY - ODCINKI - Nowe odcinki - Wszystkie odcinki - Nowy - Lista oczekujących - Ustawienia - Dodaj podcast - Pobrane - W toku - Ukończone - Dziennik - Anuluj pobieranie - Historia odtwarzania - gpodder.net - gpodder.net login - - Ostatnio opublikowane - Pokaż tylko nowe odcinki - - Otwórz menu - Zamknij menu - - Otwórz w przeglądarce - Kopiuj adres - Udostępnij adres - Skopiowano adres do schowka. - - Wyczyść historię - - Potwierdź - Anuluj - Autor - Język - Ustawienia - Obraz - Błąd - Wystąpił błąd: - Odśwież - Brak zewnętrznej pamięci. Sprawdź czy jest ona podłączona żeby aplikacja mogła pracować poprawnie. - Rozdziały - Opis odcinka - Opis - Najnowszy odcinek:\u0020 - :\u0020odcinków - Długość:\u0020 - Rozmiar:\u0020 - Przetwarzanie - Ładowanie... - Zapisz nazwę użytkownika i hasło - Zamknij - Spróbuj ponownie - Dołącz do automatycznego pobierania - - Adres kanału - Dodaj podcast przez adres - Znajdź podcast w folderze - Możesz wyszukiwać nowe podcasty ze względu na nazwę, kategorię lub popularność na gpodder.net - Przeglądaj gpodder.net - - Oznacz wszystkie jako przeczytane - Wszystkie odcinki zaznaczone jako przeczytane - Pokaż informacje - Usuń podcast - Udostępnij stronę - Udostępnij kanał - Potwierdź chęć usunięcia tego kanału wraz ze WSZYSTKIMI odcinkami, które zostały pobrane. - Usuwanie kanału - - Pobierz - Odtwórz - Pauza - Strumień - Usuń - Usuń odcinek - Oznacz jako przeczytane - Oznacz jako nieprzeczytane - Dodaj do kolejki - Usuń z kolejki - Odwiedź stronę - Wspomóż na Flattr - Dodaj wszystko do kolejki - Pobierz wszystkie - Pomiń odcinek - - Operacja zakończona sukcesem - Operacja nie powiodła się - Pobieranie w toku - Pobieram - Nie znaleziono urządzenia docelowego - Niewystarczająca ilość pamięci - Błąd pliku - Błąd danych HTTP - Nieznany błąd - Wyjątek parsera - Nieobsługiwany typ kanału - Błąd połączenia - Nieznany host - Błąd autoryzacji - Anuluj wszystkie pobierania - Pobieranie anulowane - Pobieranie ukończone - Niepoprawny adres - Błąd wejścia/wyjścia - Błąd żądania - Błąd dostępu do bazy danych - :\u0020pobrań pozostało - Przetwarzanie pobranych - Pobieranie danych podcastu - %1$d pobierania poprawne, %2$d nieudane - Nieznany tytuł - Kanał - Plik multimedialny - Obraz - Wystąpił błąd przy próbie pobierania:\u0020 - Wymagana autoryzacja - Żądany zasób wymaga podania nazwy użytkownika oraz hasła - - Błąd! - Żadne media nie odtwarzane - Przygotowuję - Gotowe - Szukam - Serwer zdechł - Nieznany błąd - Żadne media nie odtwarzane - 00:00:00 - Buferowanie - Odtwarzenie podcastu - - Wyczyść kolejkę - Cofnij - Element usunięty - Przesuń na górę - Przesuń na dół - - Logowanie do Flattr - Naciśnij przycisk poniżej by zacząć proces autoryzacji. Zostaniesz przekierowany na stronę logowania do flattr w przeglądarce i zostaniesz poproszony o przyznanie zezwolenia AntennaPod-owi na flattr-owanie. Po daniu zezwolenia powrócisz do tej strony automatycznie. - Autoryzacja - Wróć do ekranu głównego - Autoryzacja się powiodła. Możesz teraz używać flattr w aplikacji. - Nie znaleziono tokenu Flattr - Twoje konto Flattr wydaje się nie być podłączone do AntennaPod. Możesz połączyć konto do AntennaPod by przez program flattr-ować lub możesz odwiedzić stronę wątku by zrobić to tam. - Autoryzuj - Akcja zabroniona - AntennaPod nie ma zezwolenia na tą akcję. Powodem może być fakt iż dostęp dla AntennaPod do Twojego konta został cofnięty. Możesz ponownie autoryzować aplikację lub odwiedzić stronę. - Anulowano dostęp - Odwołałeś dostęp AntennaPod do swojego konta. W celu zakończenia procesu musisz usunąć aplikację z listy aplikacji dozwolonych na koncie Flattr. - - Poprawnie z-flattr-owano - Z-flattr-owano %d elementów - Z-flattr-owano: %s - Flattr-owanie %d elementów nie powiodło się - Flattr-owanie zakończone niepowodzeniem: %s - Elementy zostaną z-flattr-owane później - Flattr-owanie %s - Flattr-uję - AntennaPod z-flattr-owała - Flattr-owanie AntennaPod nie powiodło się - Wyszukiwanie z-flattr-owanych elementów - - Pobierz wtyczkę - Wtyczka nie zainstalowana - Do odtwarzania ze zmienną prędkością jest potrzebna biblioteka innej firmy. \n\nDotknij przycisku \"Pobierz wtyczkę\", aby pobrać darmową wtyczkę ze sklepu\n\nWszelkie znalezione za pomocą tej wtyczki problemy nie są odpowiedzialnością AntennaPod i należy zgłosić się do właściciela plugin. - Prędkość odtwarzania - - Brak elementów na tej liście. - Nie subskrybowałeś jeszcze żadnego kanału. - - Inne - O... - Kolejka - Usługi - Flattr - Wstrzymaj odtwarzanie kiedy słuchawki zostaną odłączone - Przeskocz do następnego elementu kolejki po zakończeniu odtwarzania - Odtwarzanie - Sieć - Częstość aktualizacji - Określ częstotliwość automatycznego odświeżania lub je wyłącz - Pobieraj pliki tylko przez WiFi - Odtwarzanie ciągłe - WiFi media pobrane - Słuchawki odłączone - Aktualizacje mobilne - Zezwól na aktualizacje poprzez sieć komórkową - Odświeżanie - Ustawienia Flattr - Logowanie do Flattr - Zaloguj się do konta Flattr aby wspierać twórców bezpośrednio z aplikacji. - Wesprzyj aplikację na Flattr - Wesprzyj twórcę AntennaPod przez Flattr. Dzięki! - Anuluj dostęp - Anuluj dostęp tej aplikacji do konta Flattr - Automatyczne wsparcie na Flattr - Interfejs użytkownika - Wybierz motyw - Zmień wygląd AntennaPod. - Automatyczne pobieranie - Skonfiguruj automatyczne pobieranie odcinków. - Włącz filtr Wi-Fi - Zezwól na automatyczne pobieranie tylko dla określonych sieci Wi-Fi. - Pamięć podręczna odcinków - Jasny - Ciemny - Nielimitowane - godziny - godzina - Instrukcja - Zaloguj - Zaloguj się swoim kontem na gpodder.net w celu synchronizacji Twoich subskrypcji. - Wyloguj - Wylogowanie się powiodło - Zmień informacje logowania - Zmień dane logowania konta gpodder.net. - Prędkość odtwarzania - Dostosuj prędkości dostępne dla odtwarzania audio o zmiennej prędkości - Ustaw nazwę hosta - Użyj domyślnego hosta - - - Szukaj kanałów lub odcinków - Znaleziono w notatkach - Znaleziono w rozdziałach - Brak wyników - Szukaj - Znaleziono w tytułach - - Pliki OPML pozwalają na przenoszenie podcastów między aplikacjami. - W celu importu pliku OPML musisz umieścić go w poniższym folderze i nacisnąć przycisk poniżej w celu rozpoczęcia importu. - Rozpocznij import - Import OPML - BŁĄD! - Odczytuję plik OPML - Wystąpił błąd w czasie odczytu dokumentu OPML: - Katalog importowania jest pusty. - Zaznacz wszystko - Odznacz wszystko - Wybierz plik do importu - Eksport OPML - Eksportowanie... - Błąd eksportu - Eksport OPML udany. - Plik .opml został zapisany do:\u0020 - - Ustaw czas do wyłączenia - Wyłącz wyłącznik czasowy - Podaj czas - Wyłącznik czasowy - Pozostały czas:\u0020 - Błąd wpisu, czas musi być liczbą całkowitą - sekundy - minuty - godziny - - KATEGORIE - TOP PODCASTY - SUGESTIE - Szukaj na gpodder.net - Login - Witamy w procesie logowania do gpodder.net. Najpierw podaj swoje dane logowania: - Login - Jeśli nie masz jeszcze konta, możesz utworzyć je tutaj:\nhttps://gpodder.net/register/ - Nazwa użytkownika - Hasło - Wybór urządzenia - Utwórz nowe urządzenie dla swojego konta na gpodder.net lub wybierz istniejące: - Identyfikator urządzenia:\u0020 - Tytuł - Utwórz nowe urządzenie - Wybierz istniejące urządzenie: - Identyfikator urządzenia nie może być pusty - Identyfikator urządzenia w użyciu - Wybierz - Logowanie zakończone sukcesem! - Gratulacje! Twoje konto na gpodder.net jest połączone z urządzeniem. AntennaPod będzie automatycznie synchronizować subskrypcje na urządzeniu z kontem na gpodder.net. - Rozpocznij synchronizację - Idź do strony głównej - Błąd autoryzacji na gpodder.net - Niepoprawna nazwa użytkownika lub hasło - Błąd synchronizacji z gpodder.net - Wystąpił błąd podczas synchronizacji:\u0020 - - Wybrany folder: - Utwórz folder - Wybierz folder danych - Utworzyć nowy folder o nazwie \"%1$s\"? - Utworzono nowy folder - Nie można zapisać do tego folderu - Folder już istnieje - Nie można utworzyć folderu - Folder nie jest pusty - Wybrany folder nie jest pusty. Pobierane media i inne pliki będą umieszczane bezpośrednio w folderze, czy kontynuować? - Wybierz domyślny folder - Wstrzymaj odtwarzanie zamiast wyciszenia jeśli inna aplikacja chce odtworzyć dźwięk. - Wstrzymaj przy przerwaniu - - Subskrybuj - Subskrybowane - Pobieranie... - - Pokaż rozdziały - Pokaż opis odcinka - Pokaż obraz - Cofnij - Przewiń - Audio - Wideo - Przesuń w górę - Więcej akcji - Odcinek jest odtwarzany - Odcinek jest pobierany - Odcinek pobrany - Nowa pozycja - Odcinek jest w kolejce - Liczba nowych odcinków - Liczba odcinków, których zacząłeś słuchać - Przeciągnij aby zmienić pozycję elementu - - Autoryzacja - Zmień swoją nazwę użytkownika oraz hasło dla tego podcastu i jego odcinków - - Importowanie subskrybcji z jednozadaniowych aplikacji - diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml deleted file mode 100644 index 62fd9c046..000000000 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ /dev/null @@ -1,280 +0,0 @@ - - - - AntennaPod - Feeds - PODCASTS - EPISÓDIOS - Novo - Lista de espera - Configurações - Adicionar podcast - Downloads - Cancelar Download - Histórico de reprodução - gpodder.net - gpodder.net login - - - - Abrir no navegador - Copiar URL - Compartilhar URL - URL copiada para área de transferência. - - Apagar histórico - - Confirmar - Cancelar - Autor - Idioma - Configurações - Capa - Erro - Um erro ocorreu: - Atualizar - Não há dispositivos de armazenamento externo disponíveis. Por favor, certifique-se de que um dispositivo de armazenamento externo está montado para que o aplicativo possa funcionar adequadamente. - Capítulos - Notas do podcast - Descrição - Episódio mais recente:\u0020 - \u0020episódios - Duração:\u0020 - Tamanho:\u0020 - Processando - Carregando... - Salvar nome do usuário e senha - Fechar - Tentar novamente - Incluir em downloads automáticos - - URL do Feed - Adicionar podcast por URL - - Marcar todos como lido - Mostrar informação - Compartilhar link do site - Compartilhar link do feed - Por favor confirme que você deseja apagar este feed e TODOS os episódios que você fez download deste feed. - Removendo feed - - Download - Reproduzir - Pausar - Stream - Remover - Marcar como lido - Marcar como não lido - Adicionar à fila - Remover da fila - Visitar Website - Adicionar ao Flattr - Enfileirar todos - Baixar todos - Pular episódio - - Download pendente - Download em execução - Dispositivo de armazenamento não encontrado - Espaço insuficiente - Erro de arquivo - Erro de HTTP Data - Erro desconhecido - Parser Exception - Tipo de feed não suportado - Erro de conexão - Host desconhecido - Cancelar todos os downloads - Download cancelado - Downloads finalizados - URL inválida - Erro de IO - Erro de requisição - Erro no acesso ao Banco de dados - \u0020Downloads restantes - Baixando dados do podcast - %1$d downloads com sucesso, %2$d falharam - Título desconhecido - Feed - Arquivo de mídia - Imagem - Ocorreu um erro durante download do arquivo:\u0020 - - Erro! - Nenhuma mídia tocando - Preparando - Pronto - Buscando - Servidor morreu - Erro desconhecido - Nenhuma mídia tocando - 00:00:00 - Armazenando - Reproduzindo podcast - - Limpar fila - Desfazer - Item removido - Mover para o topo - Mover para o fim - - Logar no Flattr - Pressione o botão abaixo para iniciar o processo de autenticação. Você será direcionado para a tela de login do Flattr, que pedirá autorização para que o AntennaPod utilize o Flattr. Após conceder a permissão, você retornará a esta tela automaticamente. - Autenticar - Retornar ao início - Autenticado com sucesso! Agora você poderá utilizar o Flattr de dentro do AntennaPod. - Nenhum token do Flattr encontrado - Sua conta Flattr não está conectada ao AntennaPod. Você pode conectar sua conta ao AntennaPod para usar o Flattr de dentro da aplicação ou pode visitar o website do feed para usar o Flattr por lá. - Autenticar - Ação proibida - AntennaPod não tem permissão para esta ação. A permissão de acesso do AntennaPod pode ter sido revogada. Você pode re-autenticar ou visitar o website do feed. - Acesso revogado - Você revogou o token de acesso do AntennaPod com sucesso. Para finalizar o processo, você deve remover esta app da lista de aplicativos aprovados nas configurações de sua conta no website do Flattr. - - - Download Plugin - Plugin Não Instalado - Para velocidade variável de reprodução funcionar uma biblioteca de terceiros deve ser instalada.\n\nToque em \'Download Plugin\' para baixar um plugin grátis na Play Store.\n\nQuaisquer problemas encontrados usando esse plugin não é responsabilidade do AntennaPod e deve ser reportado ao proprietário do plugin. - Velocidades de Reprodução - - Não existem itens nesta lista. - Você ainda não assinou nenhum feed. - - Outros - Sobre - Fila - Serviços - Flattr - Interromper a reprodução quando o fone de ouvido for desconectado - Pular para próximo item da fila quando a reprodução terminar - Reprodução - Rede - Intervalo de atualização - Especifica o intervalo com que os feeds serão atualizados automaticamente ou desabilita esta funcionalidade - Fazer download dos arquivos apenas via rede WiFi - Reprodução contínua - Download de mídia via WiFi - Fones de ouvido desconectados - Atualizações via Rede de Dados Celular - Permite atualizações quando conectado na rede de dados celular - Atualizando - Configurações do Flattr - Logar no Flattr - Loga na sua conta Flattr para utilizá-lo diretamente da aplicação - Registra este aplicativo no Flattr - Suportar o desenvolvimento do AntennaPod usando o Flattr. Obrigado! - Revogar acesso - Cancelar permissão de acesso à sua conta Flattr - Interface com usuário - Selecionar tema - Altera a aparência do AntennaPod - Download automático - Configurar download automático de episódios. - Habilitar filtro Wi-Fi - Permitir download automático somente pelas redes Wi-Fi selecionadas. - Cache de episódios - Claro - Escuro - Ilimitado - horas - hora - Manual - Login - Faça o login na sua conta gpodder.net para sincronizar suas assinaturas. - Sair - Saiu com sucesso - Alterar informações de login - Alterar informações de login da sua conta gpodder.net - Velocidades de Reprodução - Personalize as velocidades variáveis de reprodução de áudio. - Configurar hostname - Usar host padrão - - - Procurar por Feeds ou Episódios - Encontrado nas notas do podcast - Encontrado nos capítulos - Nenhum resultado encontrado - Pesquisar - Encontrado no título - - Arquivos OPML permitem que você mova seus podcasts de um programa de podcasts para outro. - Para importar um arquivo OPML, você precisa armazená-lo neste diretório e pressionar o botão abaixo para iniciar o processo de importação. - Iniciar importação - Importação de OPML - ERRO! - Lendo arquivo OPML - Ocorreu um erro durante a leitura do documento OPML: - O diretório de importação está vazio. - Selecionar todos - Remover seleção - Escolher arquivo para importar - Exportar OPML - Exportando... - Erro na exportação - OMPL exportado com sucesso - O arquivo .opml foi gravado em:\u0020 - - Configura desligamento automático - Desabilita desligamento automático - Informe a duração - Desligamento automático - Tempo restante:\u0020 - Entrada inválida, a duração precisa ser um número inteiro - - CATEGORIAS - TOP PODCASTS - SUGESTÕES - Buscar no gpodder.net - Login - Bem-vindo ao processo de login gpodder.net. Primeiramente, digite suas informações: - Login - Se ainda não possui uma conta, você pode criar uma aqui:\nhttps://gpodder.net/register/ - Nome do usuário - Senha - Seleção de dispositivo - Crie um novo dispositivo para usar em sua conta gpodder.net ou escolha um já existente: - ID do dispositivo:\u0020 - Descrição do dispositivo - Criar novo dispositivo - Escolher dispositivo existente: - ID do dispostivo não pode estar em branco - ID do dispositivo já está em uso - Escolher - Login realizado com sucesso! - Parabéns! Sua conta gpodder.net agora está conectada ao seu dispositivo. O AntennaPod irá, daqui em diante, sincronizar automaticamente assinaturas do seu dispositivo com sua conta gpodder.net. - Iniciar sincronização agora - Ir para tela principal - gpodder.net: erro de autenticação - Nome do usuário ou senha incorreta - gpodder.net: erro de sincronização - Ocorreu um erro durante a sincronização:\u0020 - - Selecionar pasta: - Criar pasta - Escolher pasta de dados - Criar nova pasta com o nome \"%1$s\"? - Nova pasta criada - Não é possível escrever nesta pasta - Pasta já existente - Não foi possível criar pasta - A pasta não está vazia - A pasta que você selecionou não está vazia. Os downloads de mídia e outros arquivos serão colocados diretamente nesta pasta. Deseja mesmo continuar? - Escolher pasta padrão - Pause a reprodução em vez de abaixar o volume quando outro aplicativo reproduzir sons - Pausar em interrupções - - Assinar - Assinado - Baixando... - - Mostrar imagem - Mais ações - Episódio está sendo reproduzido - Episódio foi baixado - Item é novo - Episódio está na fila - Numero de novos episódios - - - diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml deleted file mode 100644 index f1e525384..000000000 --- a/app/src/main/res/values-pt/strings.xml +++ /dev/null @@ -1,341 +0,0 @@ - - - - AntennaPod - Fontes - Adicionar podcast - Podcasts - Episódios - Novos episódios - Todos os episódios - Novo - Lista de espera - Definições - Adicionar podcast - Transferências - Em curso - Terminadas - Registo - Cancelar transferência - Histórico de reprodução - gpodder.net - Acesso gpodder.net - - Publicados recentemente - Mostrar apenas novos episódios - - Abrir menu - Fechar menu - - Abrir no navegador - Copiar URL - Partilhar URL - URL copiado para a área de transferência. - Ir para esta posição - - Limpar histórico - - Confirmar - Cancelar - Autor - Idioma - Definições - Imagem - Erro - Ocorreu um erro: - Atualizar - Não existe um cartão SD. Certifique-se que inseriu o cartão corretamente. - Capítulos - Notas - Descrição - Episódio mais recente:\u0020 - \u0020episódios - Duração:\u0020 - Tamanho:\u0020 - A processar... - A carregar... - Gravar utilizador e senha - Fechar - Tentar novamente - Incluir nas transferências automáticas - - URL da fonte - URL da fonte ou sítio web - Adicionar podcast via URL - Localizar podcasts no diretório - Pode procurar os novos podcasts no gPodder.net por nome, categoria ou popularidade. - Procurar no gPodder.net - - Marcar tudo como lido - Marcar todos os episódios como lidos - Mostrar informações - Remover podcast - Partilhar ligação do sítio web - Partilhar ligação da fonte - Confirme a eliminação desta fonte e de todos os episódios a ela petencentes. - Remover fonte - - Transferir - Reproduzir - Pausa - Emitir - Remover - Remover episódio - Marcar como lido - Marcar como novo - Adicionar à fila - Remover da fila - Aceder ao sítio web - Flattr - Colocar tudo na fila - Transferir tudo - Ignorar episódio - - sucesso - falha - Transferência pendente - Transferência atual - Cartão SD não encontrado - Espaço insuficiente - Erro no ficheiro - Erro HTTP - Erro desconhecido - Exceção do processador - Fonte não suportada - Erro de ligação - Servidor desconhecido - Erro de autenticação - Cancelar transferências - Transferência cancelada - Transferências terminadas - URL inválido - Erro I/O - Erro de pedido - Erro de acesso à base de dados - \u0020Transferências em falta - Processamento de transferências - A transferir dados... - %1$d transferências efetuadas, %2$d falhadas - Título desconhecido - Fonte - Ficheiro multimédia - Imagem - Ocorreu um erro ao transferir o ficheiro:\u0020 - Requer autenticação - O recurso solicitado requer um utilizador e uma senha - - Erro! - Nada em reprodução - A preparar - Pronto - A procurar - Erro de servidor - Erro desconhecido - Nada em reprodução - 00:00:00 - A processar... - Reproduzir podcast - Tecla multimédia desconhecida: %1$d - - Limpar fila - Anular - Item removido - Mover para o topo - Mover para o fundo - - Sessão Flattr - Prima o botão abaixo para iniciar a autenticação. O seu navegador web abrirá o ecrã da sessão flattr e ser-lhe-á solicitada a permissão para o AntennaPod efetuar as alterações. Após ser dada a permissão, voltará novamente a este ecrã. - Autenticar - Voltar ao ecrã - Autenticação efetuada! Já pode fazer o flattr com a aplicação. - Token flattr não encontrado - Parece que a sua conta flattr não está integrada ao AntennaPod. Clique aqui para autenticar. - Parece que a sua conta flattr não está vinculada ao AntennaPod. Pode vincular a sua conta ao AntennaPod ou aceder ao sítio web para fazer o flattr. - Autenticar - Ação negada - O AntennaPod não possui as permissões para esta ação. É possível que o token de acesso ao flattr via AntennaPod tenha sido revogado. Pode efetuar nova autenticação ou aceder ao sítio web do item. - Acesso revogado - Você revogou o token de acesso do AntennaPod à sua conta. Para concluir o processo, tem que remover esta aplicação da lista de aplicações presentes nas definições de conta no sítio web do flattr. - - Flattr de um item! - Flattr de %d itens! - Flattr: %s - Falha ao efetuar flattr de %d itens! - Não flattr: %s. - O flattr deste item será feito mais tarde - Flattring %s - O AntennaPod está a flattring - O AntennaPod fez o flattr - O AntennaPod não fez o flattr - A obter itens com flattr - - Transferir extra - Extra não instalado - Para melhorar a reprodução, deve transferir e instalar um biblioteca de terceiros.\nClique Transferir extra para transferir o extra através da loja Google.\n\nSe encontrar problemas ao utilizar esta biblioteca, os programadores do AntennaPod não podem ser responsabilizados e deve contactar o programador do extra. - Velocidades de reprodução - - Não existem itens na lista. - Ainda não possui quaisquer fontes. - - Outras - Sobre - Fila - Serviços - Flattr - Parar reprodução ao remover os auscultadores - Ir para a faixa seguinte ao terminar a reprodução - Reprodução - Rede - Intervalo entre atualizações - Indique o intervalo de tempo entre as atualizações de fontes ou desative a opção - Apenas transferir pelas redes sem fios - Reprodução contínua - Transferência Wi-Fi - Auscultadores removidos - Atualizações móveis - Permitir atualizações através da rede de dados - A atualizar - Definições flattr - Sessão flattr - Inicie sessão na sua conta flattr para fazer o flattr no AntennaPod. - Flattr desta aplicação - Ajude no desenvolvimento do AntennaPod através do Flattr. Obrigado! - Revogar acesso - Revogar permissões de acesso da aplicação à sua conta flattr. - Flattr automático - Configurar flattr automático - Interface - Tema - Mudar o aspeto do AntennaPod. - Transferência automática - Configure a transferência automática dos episódios. - Ativar filtro Wi-Fi - Apenas permitir transferências automáticas através de redes sem fios. - Cache de episódios - Claro - Escuro - Sem limite - horas - hora - Manual - Acesso - Aceda à sua conta gpodder.net para poder sincronizar as subscrições. - Sair - Sessão terminada - Mudar informação de acesso - Mudar informação de acesso à sua conta gpodder.net. - Velocidades de reprodução - Personalize as velocidades de reprodução disponíveis. - Intervalo de procura - Ao recuar ou avançar, procurar este valor de segundos - Definir nome de servidor - Utilizar pré-definição - - Ativar flattr automático - Flattr de episódios ao atingir %d porcento de reprodução - Flattr de episodios ao iniciar a reprodução - Flattr de episódios ao terminar a reprodução - - Procurar fontes ou episódios - Encontrado nas notas - Encontrado nos capítulos - Nenhum resultado - Procura - Encontrado no título - - Os ficheiros OPML permitem-lhe mover os podcasts entre aplicações. - Para importar um ficheiro OPML, tem que o colocar neste diretório e premir o botão abaixo para iniciar o processo. - Iniciar importação - Importação OPML - Erro! - A ler ficheiro OPML - Ocorreu um erro ao ler o ficheiro OPML: - O diretório de importação está vazio. - Marcar tudo - Desmarcar tudo - Escolha o ficheiro a importar - Exportação OPML - Exportação... - Erro de exportação - Exportação efetuada. - O ficheiro .opml foi gravado em:\u0020 - - Definir temporizador - Desativar temporizador - Introduza o tempo - Temporizador - Tempo restante:\u0020 - Valor inválido. Tem que ser um inteiro. - segundos - minutos - horas - - Categorias - Melhores - Sugestões - Procurar no gpodder.net - Acesso - Bem-vindo ao processo de acesso ao gpodder.net. Introduza os dados de acesso: - Acesso - Se ainda não possui uma conta, pode criar uma em:\nhttps://gpodder.net/register/ - Utilizador - Senha - Seleção de dispositivo - Criar um novo dispositivo ou escolher um existente para aceder à sua conta gpodder.net - ID do dispositivo:\u0020 - Legenda - Criar novo dispositivo - Escolher dispositivo: - ID do dispositivo não pode estar vazia - ID de dispositivo já utilizada - Escolher - Sessão iniciada! - Parabéns! A sua conta gpodder.net está vinculada ao seu dispositivo. Agora, já pode sincronizar as subscrições no dispositivo com a sua conta gpodder.net. - Sincronizar agora - Ir para o ecrã principal - Erro de autenticação gpodder.net - Utilizador ou senha inválido - Erro de sincronização gpodder.net - Ocorreu um erro ao sincronizar:\u0020 - - Diretório escolhido: - Criar diretório - Escolha o diretório - Criar um diretório com o nome \"%1$s\"? - Novo diretório criado - Não é possível gravar neste diretório - O diretório já existe - Não é possível criar o diretório - Diretório não vazio - O diretório escolhido não está vazio. As transferências serão colocadas neste diretório. Continuar? - Escolha a pasta pré-definida - Pausa na reprodução em vez de baixar o volume se outra aplicação quiser reproduzir sons - Pausa nas interrupções - - Subscrever - Subscrito - Transferência... - - Mostrar capítulos - Mostrar notas - Mostrar imagem - Recuar - Avanço rápido - Áudio - Vídeo - Navegar para cima - Mais ações - Episódio em reprodução - Episódio a ser transferido - Episódio transferido - Novo item - Episódio está na fila - Número de novos episódios - Número de episódios que já foi iniciada a reprodução - Arraste para mudar a posição deste item - - Autenticação - Altere o seu nome de utilizador e senha para este podcast e seus episódios. - - Importar subscrições de aplicações single-purpose... - diff --git a/app/src/main/res/values-ro-rRO/strings.xml b/app/src/main/res/values-ro-rRO/strings.xml deleted file mode 100644 index a6e782f74..000000000 --- a/app/src/main/res/values-ro-rRO/strings.xml +++ /dev/null @@ -1,245 +0,0 @@ - - - - AntennaPod - Feeduri - PODCASTURI - EPISOADE - Nou - Listă de așteptare - Setări - Descărcări - Anulează descărcare - Istorie ascultare - gpodder.net - autentificare gpodder.net - - - - Deschide în browser - Copiază URL - Împarte URL - URL copiat în clipboard - - Golește istoric - - Confirmă - Anulează - Autor - Limbă - Setări - Eroare - A avut loc o eroare: - Reîncarcă - Nu exista stocare externă. Asigurați-vă că stocarea externă este conectată pentru ca aplicația să funcționeze corespunzător. - Capitole - Notițe - Descriere - Cel mai recent episod:\u0020 - \u0020episoade - Durată:\u0020 - Dimensiune:\u0020 - Procesează - Încărcare... - Salvează numele de utilizator și parola - închide - Reîncearcă - - Adresă feed - - Marchează toate ca citite - Arată informații - Împarte adresă website - Împarte adresă feed - Confirmați ștergerea feedului și a TUTUROR episoadelor pe care le-ați descărcat. - - Descarcă - Play - Pauză - Stream - Elimină - Marchează ca citit - Marchează ca necitit - Adaugă la Coadă - Șterge din Coadă - Vizitează Website - Flattr aceasta - Adaugă toate în coadă - Descarcă toate - Sari peste episod - - Descărcare în așteptare - Se descarcă - Mediu de stocare lipsă - Spațiu insuficient - Eroare fișier - Eroare Date HTTP - Eroare necunoscută - Excepție parser - Tip de feed nesuportat - Eroare de conexiune - Host necunoscut - Anulează toate descărcările - Descărcare anulată - Descărcări terminate - URL malformat - Eroare IO - Eroare cerere - \u0020descărcări rămase - Descarcă date podcast - %1$d descărcari cu succes, %2$d eșuate - Titlu necunoscut - Feed - Fișier media - Imagine - O eroare a avut loc când se descărca fișierul:\u0020 - - Eroare! - Nu se ascultă nimic - Pregătește - Pregătit - Căutare - Server mort - Eroare necuonscută - Nu se ascultă nimic - 00:00:00 - Buffering - Cântă podcast - - Golește coada - Refă - Element înlăturat - - Flattr sign-in - Apăsați butonul de mai jos pentru a începe procesul de autentificare. Veți fi îndreptat spre pagina de logare flattr în browser și veți fi rugat să acordați permisiuni AntennaPod sa flattr. După ce veți acorda permisiunile veți fi readuși la acest ecran automat. - Autentificare - Întoarcere acasă - Autentificare cu succes! Acum puteți flattr din aplicație. - Nu s-a găsit token Flattr - Contul flattr nu pare șa fie conectat la AntennaPod. Puteți fie conecta contul cu AntennaPod pentru a flattr lucruri din aplicație sau puteți vizita site-ul pentru a flattr acolo. - Autentificați-vă - Acțiune interzisă - AntennaPod nu are permisiuni pentru această acțiune. Motivul poate fi că tokenul de acces al AntennaPod pentru contul vostru a fost revocat. Vă puteți fie re-autentifica fie vizita direct site-ul. - Acces revocat - Ați revocat cu succes accesul AntennaPod la contul vostru. Pentru a completa acest proces trebuie să ștergeți aplicația din lista de aplicații aprobate din setările contului de pe site-ul flattr. - - - Descarcă plugin - Plugin neinstalat - Pentru ca viteza variabilă de ascultare să funcționeze este necesară o librărie externă.\n\nApăsați \'Descarcă Plugin\' pentru a descărca un plugin gratuit din Play Store\n\nOrice probleme găsite folosind acest plugin nu sunt responsabilitatea AntennaPod și trebuie raportate autorului pluginului. - Viteze de ascultare - - Nu sunt elemente în listă. - Nu v-ați abonat la nici un feed momentan. - - Altele - Despre - Coadă - Servicii - Flattr - Pune pauză când căștile sunt deconectate - Sari la următorul element din coadă cand se termină ascultarea - Ascultare - Rețea - Interval actualizare - Specifică un interval în care feedurile sunt actualizate automat sau oprește funcția - Descarcă fișiere media doar pe WiFi - Ascultare continuă - Descărcare media pe WiFi - Căști deconectate - Actualizări mobile - Permite actualizări pe conexiunea de date mobilă - Reîncarcare - Setări Flattr - Sign-in Flattr - Logați la contul flattr pentru a flattr lucruri direct din aplicație. - Flattr această aplicație - Ajutați dezvoltarea AntennaPod prin flattr. Mulțumesc! - Revocare acces - Revocă accesul permisiunilor pentru contul de flattr. - Interfața grafică - Alege temă - Schimbă aspectul AntennaPod. - Descărcare automată - Configurează descărcarea automată a episoadelor. - Pornește filtru Wi-Fi - Pornește descărcarea automată doar pentru rețele Wi-Fi selectate. - Cache de episoade - Deschis - Întunecat - Nelimitat - ore - oră - Manual - Autentificare - Viteze de ascutare - Modifică vitezele disponibile pentru viteza de ascultare. - - - Caută feeduri sau episoade - Găsit în notițe - Găsit în capitole - Nu s-a găsit nici un rezultat - Caută - Găsit în titlu - - Pentru a importa un fișier OPML trebuie să-l salvați în următorul director și apăsați butonul de mai jos pentru a începe procesul. - Începe importarea - OPML import - EROARE! - Citește fișierul OPML - A avut loc o eroare la citirea documentului opml: - Directorul de import este gol. - Selectează toate - Deselectează toate - Alege fișier pentru import - Exportă OPML - Exportă... - Eroare exportare - Exportare opml cu succes. - Fișierul .opml a fost scris în:\u0020 - - Setează cronometru somn - Oprește cronometru somn - Introdu timp - Cronometru somn - Timp rămas:\u0020 - Input invalid, timpul trebuie să fie un întreg - - CATEGORII - SUGESTII - Autentificare - Conectare - Utilizator - Parolă - Alegere Dispozitiv - ID dispozitiv:\u0020 - Creează dispozitiv nou - ID-ul dispozitivului nu trebuie să fie gol - ID-ul de dispozitiv este deja în uz - Alege - Începe sincronizarea acum - Mergi la ecranul principal - eroare de autentificare la gpodder.net - Nume utilizator sau parolă greșite - eroare de sincronizare gpodder.net - - Fișier selectat: - Crează fișier - Alege fișier date - Crează fișier nou cu numele \"%1$s\"? - A fost creat fișierul - Nu poate fi scris în fișier - Fișier deja existent - Nu poate creea fișier - Fișierul nu este gol - Fișierul selectat nu este gol. Descărcările media și alte fișiere vor fi plasate direct în acest director. Continuați oricum? - Alege fișier implicit - - Abonează-te - Abonat - Se descarcă... - - - - diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml deleted file mode 100644 index c5c642da0..000000000 --- a/app/src/main/res/values-ru/strings.xml +++ /dev/null @@ -1,311 +0,0 @@ - - - - AntennaPod - Каналы - Подкасты - Выпуски - Новые выпуски - Все выпуски - Новые - В ожидании - Настройки - Добавить подкаст - Загрузки - Отменить загрузку - История воспроизведения - gpodder.net - Войти на gpodder.net - - - - Открыть в браузере - Скопировать ссылку - Поделиться ссылкой - Ссылка скопирована в буфер - - Очистить историю - - Подтвердить - Отмена - Автор - Язык - Настройки - Обложка - Ошибка - Произошла ошибка: - Обновить - Внешний носитель недоступен. Убедитесь что внешний носитель установлен, иначе приложение не сможет нормально работать. - Главы - Примечания к выпуску - Описание - Последний выпуск:\u0020 - \u0020выпуск(ов) - Продолжительность:\u0020 - Размер:\u0020 - Обработка - Загрузка... - Сохранить имя пользователя и пароль - Закрыть - Повторить - Добавить в автозагрузки - - URL канала - Добавить подкаст по URL - Найти подкаст в каталоге - - Отметить все как прочитанное - Показать информацию - Удалить подкаст - Ссылка на сайт - Ссылка на канал - Подтвердите удаление канала и ВСЕХ загруженных с этого канала выпусков. - Удаление канала - - Загрузить - Воспроизвести - Пауза - Потоковое воспроизведение - Удалить - Отметить как прочитанное - Отметить как непрочитанное - Добавить в очередь - Удалить из очереди - Посетить сайт - Поддержать через Flattr - Добавить всё в очередь - Загрузить всё - Пропустить выпуск - - успешно - не удалось - Загрузка в ожидании - Загрузка в процессе - Устройство хранения не найдено - Недостаточно места - Ошибка файла - Ошибка протокола HTTP - Неизвестная ошибка - Ошибка обработки - Неподдерживаемый тип канала - Ошибка соединения - Неизвестный узел - Ошибка авторизации - Отменить все загрузки - Загрузка отменена - Загрузки завершены - Неправильный адрес - Ошибка ввода-вывода - Ошибка запроса - Ошибка доступа к базе данных - Осталось\u0020загрузок - Получение данных подкаста - %1$d загрузок завершено, %2$d не удалось - Неизвестное название - Канал - Медиафайл - Изображение - Ошибка при загрузки файла:\u0020 - Необходима авторизация - Для доступа к ресурсу необходимо ввести имя пользователя и пароль - - Ошибка - Ничего не воспроизводится - Подготовка - Готово - Перемотка - Сервер недоступен - Неизвестная ошибка - Ничего не воспроизводится - 00:00:00 - Буферизация - Воспроизведение подкаста - - Очистить очередь - Отмена - Удалено - Переместить вверх - Переместить вниз - - Авторизоваться в Flattr - Нажмите кнопку, чтобы начать процесс авторизации. Вы будете перенаправлены на сайт Flattr, где нужно будет разрешить AntennaPod использовать ваш аккаунт. После этого вы автоматически будете перенаправлены обратно. - Авторизовать - Вернуться к началу - Успешная авторизация. Теперь можно использовать Flattr прямо из приложения. - Токен Flattr не найден - Кажется, ваш аккаунт Flattr не подключен к AntennaPod. Можно подключить аккаунт к AntennaPod или посетить сайт канала, чтобы пожертвовать через Flattr прямо на сайте. - Авторизоваться - Действие запрещено - AntennaPod не имеет прав для выполнения этого действия. Возможно, доступ к вашему аккаунту был отозван. Можно авторизоваться заново или посетить сайт, которому вы пожертвовали через Flattr. - Доступ отозван - Вы успешно отключили AntennaPod от аккаунта в Flattr. Чтобы завершить этот процесс нужно удалить AntennaPod из списка приложений подключенных к аккаунту на сайте Flattr. - - Один поддержан через Flattr! - Поддержано через Flattr: %d. - Поддержано через Flattr: %s. - Не удалось поддержать через Flattr: %d! - Не поддержано через Flattr: %s. - Будет поддержано через Flattr потом - %s поддерживается через Flattr - AntennaPod поддерживает через Flattr - Вы поддержали AntennaPod через Flattr - Ошибка - Получение списка поддержаного через Flattr - - Загрузить плагин - Плагин не установлен - Для изменения скорости воспроизведения должна быть установлена сторонняя библиотека.⏎\n⏎\nНажмите «Загрузить плагин», чтобы загрузить беспалтный плагин из Play Store⏎\n⏎\nЛюбые проблемы при использовании плагина не являются ответственностью AntennaPod и о них следует сообщать владельцу плагину. - Скорость воспроизведения - - Список пуст - Вы еще не подписаны ни на один канал - - Прочее - О программе - Очередь - Сервисы - Flattr - Приостановить воспроизведение, когда наушники отсоединены - После завершения воспроизведения перейти к следующему в очереди - Воспроизведение - Сеть - Интервал обновлений - Укажите интервал через который каналы обновляются автоматически, или отключите его - Загружать файлы только через Wi-Fi - Непрерывное воспроизведение - Загрузка по Wi-Fi - Наушники отсоединены - Мобильные обновления - Позволить обновления через мобильное интернет-подключение - Обновление - Настройки Flattr - Авторизация Flattr - Авторизуйтесь во Flattr чтобы поддерживать каналы прямо из приложения - Поддержать это приложение в Flattr - Поддержите разработку AntennaPod через Flattr. Спасибо! - Отозвать доступ - Отменить доступ этого приложения к вашему аккаунту Flattr. - Автоматически поддерживать через Flattr - Интерфейс - Выбор темы - Изменить тему оформления AntennaPod - Автоматическая загрузка - Настроить автоматическую загрузку выпусков. - Включить фильтр Wi-Fi - Разрешать автоматическую загрузку только для выбранных сетей Wi-Fi. - Кэш выпусков - Светлая - Тёмная - Неограничен - ч. - ч. - Вручную - Войти - Вход в ваш аккаунт gpodder.net для синхронизации ваших подписок. - Выход из gpodder.net - Выход произведён успешно - Изменить информацию авторизации - Изменить информацию авторизации для аккаунта gpodder.net - Скорость воспроизведения - Настроить скорости воспроизведения - Задать имя узла - Использовать узел по умолчанию - - - Поиск каналов или выпусков - Найдено в описании выпуска - Найдено в главах - Ничего не найдено - Поиск - Найдено в заголовке - - OPML файлы позволяют перемещать ваши подкасты из одного менеджера подкастов в другой. - Для импорта файла OPML его нужно поместить в указанный каталог и нажать кнопку внизу для запуска импорта. - Начать импорт - Импорт OPML - Ошибка - Чтение файла OPML - Ошибка чтения файла OPML - Каталог для импорта пуст. - Отметить все - Снять все отметки - Выбрать файл для импорта - Экспорт в OPML - Экспортируется... - Ошибка экспорта - Экспорт OPML завершён. - Файл OPML был записан в:\u0020 - - Установить таймер сна - Отключить таймер сна - Введите время - Таймер сна - Осталось времени:\u0020 - Неправильный ввод, время должно быть в виде числа - - Категории - Лучшее - Рекомендации - Искать на gpodder.net - Войти - Добро пожаловать в процесс авторизации на gpodder.net. Сначала введите вашу информацию для авторизации: - Войти - Если у вас ещё нет аккаунта, то вы можете создать его здесь:⏎\nhttps://gpodder.net/register/ - Имя пользователя - Пароль - Выбор устройства - Создайте новое устройство, чтобы использовать ваш аккаунт на gpodder.net или выберите существующее: - Идентификатор устройства:\u0020 - Название устройства - Создайте новое устройство - Выберите существующее устройство: - Поле с Device ID не должно быть пустым - Device ID уже используется - Выберите - Авторизация успешна! - Поздравляем! Ваш аккаунт на gpodder.net теперь связан с вашим устройством. AntennaPod теперь сможет автоматически синхронизировать ваши подписки с аккаунтом gpodder.net - Начать синхронизацию - Перейти на главный экран - Ошибка авторизации на gpodder.net - Неправильное имя пользователя или пароль - Ошибка синхронизации с gpodder.net - Произошла ошибка во время синхронизации:\u0020 - - Выбранная папка: - Создать папку - Выбрать папку для хранения данных - Создать папку \"%1$s\"? - Новая папка создана - Запись в эту папку невозможна - Папка уже существует - Невозможно создать папку - Папка не пуста - Выбранная папка не пуста. Загрузки и прочие файлы будут сохранены в эту папку. Продолжить? - Выберите папку по умолчанию - Пауза вместо уменьшения громкости, когда другое приложение проигрывает звуки - Пауза при смене аудиофокуса - - Подписаться - Подписка оформлена - Загрузка... - - Показать разделы - Показать заметки к эпизодам - Показать изображение - Назад - Вперед - Аудио - Видео - Перейти выше - Другие действия - Эпизод воспроизводится - Эпизод загружается - Эпизод загружен - Новый - Эпизод в очереди - Количество новых эпизодов - Количество начатых эпизодов - - - Импорт подписок из одноцелевых приложений… - diff --git a/app/src/main/res/values-sv-rSE/strings.xml b/app/src/main/res/values-sv-rSE/strings.xml deleted file mode 100644 index e17f54fa5..000000000 --- a/app/src/main/res/values-sv-rSE/strings.xml +++ /dev/null @@ -1,341 +0,0 @@ - - - - AntennaPod - Flöden - Lägg till podcast - PODCASTS - AVSNITT - Nya episoder - Alla episoder - Ny - Väntelista - Inställningar - Lägg till podcast - Nedladdningar - Körs - Färdiga - Logg - Avbryt nedladdnin - Uppspelningshistorik - gpodder.net - gpodder.net login - - Nyligen publicerade - Visa bara nya episoder - - Öppna meny - Stäng meny - - Öppna i webbläsare - Kopiera URL - Dela URL - Kopierade URL till clipboard. - Gå hit - - Rensa historik - - Bekräfta - Avbryt - Skapare - Språk - Inställningar - Bild - Fel - Ett fel inträffade: - Uppdatera - Ingen extern lagring är tillgänglig. Se till att montera en extern lagringsenhet så att appen kan fungera korrekt. - Kapitel - Shownotes - Beskrivning - Senaste avsnittet:\u0020 - \u0020episoder - Längd:\u0020 - Storlek:\u0020 - Bearbetar - Laddar... - Spara användarnamn och lösenord - Stäng - Försök igen - Inkludera i automatiska nedladdningar - - Flödets URL - URL till flöde eller webbsida - Lägg till podcast via URL - Hitta podcast i mapp - Du kan söka efter podcasts baserat på namn, kategori eller populäritet på tjänsten gpodder.net - Bläddra på gpodder.net - - Markera alla som lästa - Markera alla episoder som lästa - Visa information - Ta bort podcast - Dela hemsidans länk - Dela flödeslänk - Bekräfta att du vill ta bort denna feed och ALLA avsnitt av denna feed som du har hämtat. - Tar bort flöde - - Ladda ned - Spela - Pausa - Stream - Ta bort - Ta bort episod - Markera som läst - Markera som oläst - Lägg till i kön - Ta bort från Kön - Besök websidan - Flattr det här - Lägg till alla i kön - Ladda ner alla - Hoppa över avsnitt - - lyckades - misslyckades - Avvaktar nedladdning - Nedladdning pågår - Lagringsenhet hittades inte - Otillräckligt utrymme - Filfel - HTTP data fel - Okänt fel - Parserfel - Flödestyp utan stöd - Anslutningsfel - Okänd värd - Autentiseringsproblem - Avbryt alla nedladdningar - Nedladdning avbruten - Nedladdningar färdiga - Felaktig webbadress - IO fel - Request fel - Ingen tillgång till databasen - \u0020Nedladdningar kvar - Bearbetar nedladdningar - Laddar ner podcastdata - %1$d nedladdningar lyckades, %2$d misslyckades - Okänd titel - Flöde - Mediafil - Bild - Ett fel uppstod vid försöket att ladda ner filen:\u0020 - Autentisering krävs - Resursen du begärde kräver ett användarnamn och ett lösenord - - Fel! - Inget media spelar - Förbereder - Beredd - Söker - Servern dog - Okänt fel - Inget media spelar - 00:00:00 - Buffrar - Spelar podcast - AntannaPod - Okänd mediaknapp: %1$d - - Rensa kön - Ångra - Föremålet avlägsnades - Flytta längst upp - Flytta längst ned - - Flattr inloggning - Tryck på knappen nedan för att starta autentiseringen. Du kommer att vidarebefordras till Flattrs inloggningsskärm i din webbläsare och uppmanas att ge AntennaPod tillstånd att Flattra saker. Efter att du har gett tillstånd, kommer du automatiskt tillbaka till den här skärmen. - Autentisera - Återgå till Startsidan - Autentiseringen lyckades! Du kan nu Flattra saker i appen. - Ingen Flattr token hittades - Ditt Flattr-konto verkar inte vara anslutet till AntennaPod. Tryck här för att autentisera. - Ditt Flattr konto verkar inte vara ansluten till AntennaPod. Du kan antingen ansluta ditt konto till AntennaPod att Flattr saker i app eller så kan du besöka webbplatsen för att Flattr det där. - Autentisera - Åtgärd förbjuden - AntennaPod saknar behörighet för den här åtgärden. Anledningen till detta kan vara att AntennaPods tillgång till ditt konto har återkallats. Du kan antingen åter autentisera AntennaPod eller besöka hemsidan istället. - Tillgång återkallad - Du har nu återkallat AntennaPods tillgång till ditt konto. För att slutföra processen, måste du ta bort den här appen från listan godkända appar i dina kontoinställningar på Flattrs hemsida. - - Flattrade en sak! - Flattrade %d saker! - Flattrade: %s. - Misslyckades att flattra %d saker! - Ej flattrade: %s. - Saker som kommer att flattras senare - Flattrar %s - AnntennaPod flattrar - AntennaPod har flattrat - AntennaPod misslyckades att flattra - Hämtar flattrade saker - - Ladda ner tillägg - Tillägg ej installerat - För att variabel uppspelningshastighet skall fungera måste ett tredjepartstillägg installeras.\n\nTryck på \'Ladda ner tillägg\' för att ladda ner ett gratis tillägg från Play Store.\n\nAntennaPod ansvarar inte för problem med detta tillägg och de bör rapporteras till tilläggets skapare. - Uppspelningshastigheter - - Det finns inget i denna lista. - Du har inte prenumererat på något flöde ännu. - - Annat - Om - - Tjänster - Flattr - Pausa uppspelningen när hörlurarna bortkopplas - Hoppa till nästa i kön när uppspelningen är klar - Uppspelning - Nätverk - Uppdateringsintervall - Ange ett intervall för att automatiskt uppdatera flödet eller avaktivera det - Hämta mediefiler endast över WiFi - Kontinuerlig uppspelning - WiFi nedladdning - Hörlurar bortkopplade - Mobila uppdateringar - Tillåt uppdateringar via mobil dataanslutning - Uppdatera - Flattr inställningar - Flattr inloggning - För att Flattra saker direkt från appen, logga in på ditt Flattr-konto. - Flattra den här appen - Stöd utvecklingen av AntennaPod genom att flattra den. Tack! - Återkalla åtkomst - Återkalla behörigheten till ditt Flattr-konto för denna app. - Automatisk Flattring - Konfigurerar automatisk Flattring - Användargränssnitt - Välj tema - Ändra utseendet på AntennaPod. - Automatisk nedladdning - Konfigurera automatisk nedladdning av episoder. - Aktivera WiFi filtrering - Tillåt automatisk nedladdning endast för utvalda WiFi-nätverk. - Avsnittscache - Ljust - Mörkt - Obegränsat - timmar - timme - Manuell - Logga in - Logga in med ditt gpodder.net konto för att synkronisera dina prenumerationer. - Logga ut - Utloggning lyckades - Ändra inloggningsinformation - Ändra inloggningsinformationen för ditt gpodder.net konto. - Uppspelningshastigheter - Anpassa de tillgängliga hastigheterna för variabel uppspelningshastighet. - Söktid - Sök så här många sekunder vid snabbspolning bakåt eller framåt - Sätt värdnamn - Använd standardvärden - - Aktivera automatisk Flattring - Flattra episoden så snart %d procent har spelats - Flattra episoden när den startas - Flattra episoden när den spelats klart - - Sök efter flöden eller avsnitt - Hittad i shownotes - Hittad i kapitel - Inga resultat hittades - Sök - Hittad i titeln - - OPML-filer låter dig flytta dina podcasts från en podcatcher till en annan. - Om du vill importera en OPML-fil, måste du placera den i följande katalog och tryck på knappen nedan för att starta importen. - Påbörja importering - Importera OPML-fil - FEL! - Läser OPML-fil - Ett fel har skett vid iläsning av opml dokumentet: - Katalogen är tom. - Välj alla - Avmarkera alla - Välj fil att importera - OPML export - Exporterar... - Exporteringsfel - OPML export lyckades - .opml filen skrevs till:\u0020 - - Ställ in sömntimer - Stäng av sömntimer - Ange tid - Sömntimer - Återstående tid:\u0020 - Ogiltigt tal, tiden måste vara ett heltal - sekunder - minuter - timmar - - KATEGORIER - BÄSTA PODCASTS - FÖRSLAG - Sök på gpodder.net - Inloggning - Välkommen till inloggningsprocessen för gpodder.net. Först, skriv in din inloggningsinformation: - Logga in - Om du inte har ett konto än, så kan du skapa ett här:\nhttps://gpodder.net/register/ - Användarnamn - Lösenord - Enhetsval - Skapa en ny enhet för ditt gpodder.net konto eller välj en befintlig: - Enhets ID:\u0020 - Rubrik - Skapa ny enhet - Välj befintlig enhet: - Enhets ID måste fyllas i - Enhets ID används redan - Välj - Inloggning lyckades! - Grattis! Ditt gpodder.net konto är nu länkat med din enhet. AntennaPod kommer från och med nu automatiskt synkronisera dina prenumerationer på din enhet med ditt gpodder.net konto. - Starta synkronisering nu - Gå till huvudskärmen - gpodder.net autentiseringsfel - Fel användarnamn eller lösenord - gpodder.net synkroniseringsfel - Ett fel uppstod under synkronisering:\u0020 - - Vald mapp: - Skapa mapp - Välj mapp - Skapa ny mapp med namnet \"%1$s\"? - Skapade ny mapp - Kan inte skriva till den här mappen - Mappen finns redan - Kunde inte skapa mapp - Mappen är inte tom - Den mapp du har valt är inte tom. Filer kommer att placeras direkt i denna mapp. Fortsätt ändå? - Välj standardmapp - Pausa uppspelning istället för att sänka volymen när en annan app vill spela ljud - Pausa för avbrott - - Prenumerera - Prenumererar - Laddar ner... - - Visa kapitel - Visa shownotes - Visa bild - Backa - Snabbspola - Ljud - Video - Navigera upp - Fler åtgärder - Episoden spelas - Episoden laddas ner - Episoden är nedladdad - Föremålet är nytt - Episoden är i kön - Antal nya episoder - Antal episoder du har börjat lyssna på - Dra för att ändra dess position - - Autentisering - Byt ditt användarnamn och lösenord för den här podcasten och dess episoder. - - Importerar prenumerationer från appar gjorda för ett enda syfte... - diff --git a/app/src/main/res/values-uk-rUA/strings.xml b/app/src/main/res/values-uk-rUA/strings.xml deleted file mode 100644 index 6653e6614..000000000 --- a/app/src/main/res/values-uk-rUA/strings.xml +++ /dev/null @@ -1,329 +0,0 @@ - - - - AntennaPod - Канали - Подкасти - Епізоди - Нові епізоди - Всі епізоди - Нові - Черга - Налаштування - Додати подкаст - Завантаження - В процесі - Завершено - Журнал - Скасувати завантаження - Що грало - gpodder.net - gpodder.net логін - - Щойно опубліковано - Показати тількі нові епізоди - - Показати меню - Сховати меню - - Відкрити в браузері - Копія URL - Поділитися URL - Копіювати URL в clipboard - - Забути - - Підтвердити - Скасувати - Автор - Мова - Налаштування - Зображення - Помилка - Трапилась помілка: - Оновити - Немає доступної флешки. Зовнішній носій потрібен для коректної роботи додатку - Глави - Нотатки до епізода - Опис - Найновіший епізод:\u0020 - \u0020епізодів - Довжина:\u0020 - Розмір:\u0020 - Обробка - Завантаження категорій ... - Зберегти ім\'я користувача та пароль - Закрити - Повторити знову - Включити до автозавантаження - - Посилання на канал - Додати подкаст за URL - Знайти подкаст в каталозі - В каталозі gpodder.net можливий пошук за назвою, категорією або популярністю. - Переглянути gpodder.net - - Все прочитано - Позначити всі епізоди як переглянуті - Інформація - Видалити подкаст - Поділитися URL сайту - Поділитися URL каналу - Ви впенені що хочете видаліти канал та всі завантажені епізоди - Удаляю канал - - Завантажити - Грати - Пауза - Прослухати без завантаження - Видалити - Видалити епізод - Прочитано - Непрочитано - Додати до черги - Видалити з черги - Відкрити сайт - Підтримати за допомогою Flattr - Додати до черги - Завантажити все - Пропустити епізод - - успішно - з помилками - Потрібно завантажити - Завантаження - Немає куди зберігати - Мало місця - Помилка файлу - Помилка HTTP - Щось трапилось - Помилка парсера - Непідтримую такий канал - Помилка з\'єднання - Невідомий host - Помилка автентифікації - Скасувати всі завантаження - Відмінено завантаження - Завантажили - Невірний URL - Помилка IO - Помилка запиту - Помилка бази даних - \0020 залишилось завантажити - Обробка завантаженого - Завантаження даних подкасту - Завантажилось %1$d успішно, %2$d з помилками - Невідома назва - Канал - Файл з медіа - Зображення - Помилка при завантажені файлу:\u0020 - Потрібна автентифікація - Для доступа до цього ресурса потрібні ім\'я та пароль - - Помилка! - Нічого грати - Підготовка - Готов - Шукаю - Сервер помер - Невідома помилка - Німає що грати - 00:00:00 - Буферізую - Грає подкаст - - Очистити чергу - Скасувати - Видалено - Догори - Донизу - - Увійти до Flattr - Нажміть цю кнопку для початку авторізації. Буде відкрито flattr в браузері, буде запит на дозвіл доступу Antennapod до flattr. Після надання доступу ви повернетесь до цього екрану автоматично - Ввісти ім\'я та пароль - Повернення до початку - Вийшло авторізуватись. Тепер ви можете flattr things за допомогою додатку - Немає flattr token - Здається ваш обліковий запис flattr не під\'єднано до AntennaPod. Ви можете або під\'єднати її або відкривати web сторінку в браузері - Пароль та логін - Заборонено - AntennaPod не маэ дозвілу це зробити. Можливо відкликаний доступ до AntennaPod. Або ввідіть логін пароль в налаштуваннях або зробить це на сайті - Доступ відкликано - Ви відкликали доступ AntennaPod до облікового запису. Для закінчення процессу вам потрібно видалити додаток з затвержденного списку в вашому облікову запису на сайті flattr - - Flattr\'ed one thing! - Flattr\'ed %d things! - Flattr\'ed: %s. - Failed to flattr %d things! - Not flattr\'ed: %s. - Thing will be flattr\'ed later - Flattring %s - AntennaPod is flattring - AntennaPod has flattr\'ed - AntennaPod flattr failed - Retrieving flattr\'ed things - - Завантажити Plugin - Plugin не встановлено - Для керування швидкістю програвання потрібно встановити plugin\nНатисніть \"Завантажити Plugin\" для завантаження безкоштовного plugin з Play Store\nЯкщо при використанні plugin будуть які небудь проблеми це відповідальність автору plugin, а не автору AntennaPod - Швидкість програвання - - Нічного в цьому списку - Немає підписаних каналів - - Інше - О - Черга - Сервіси - Flattr - Зупинятись коли навушники витягнуті - До наступної черги коли дограє до кінця - Грає - Мережа - Коли оновлювати - Визначати як час для автооновлювання або відключити автооновлення - Завантажувати тільки через Wifi - Грати безперервно - Завантаження через Wifi - Навушники витягнуті - Мобільне оновлення - Дозволити оновлення через оператора зв\'язку - Оновлення - Налаштування Flattr - Увійти до Flattr - Увійти в облікову flattr в flattr things напряму з додатку - Flattr цій додаток - Підтримайте розробку AntennaPod за допомогою flattr. Дякую! - Відкликати доступ - Відкликати дозвіл на доступ до вашого flattr з цього додатку - Automatic Flattr - Зовнішній вид - Обрати тему - Змінити появу AntennaPod - Автоматичне завантаження - Налаштування автоматичного завантаження епізодів - Увімкнути фільтр Wi-Fi - Дозволити автоматичне завантаження тільки в цих Wi-Fi мережах - Кеш епізодів - Світла - Темна - Без обмежень - годин - година - Інструкція - Логін - Увійти до свого облікового запису gpodder.net для сінхронізації ваших каналів - Виход - Успішно закрили доступ - Змінити інформацію для входу - Змінити вашу інформацію для вашего gpodder.net облікового запису - Швидкість програвання - Налаштування швідкості доступно для змінної швидкості програвання - Встановити ім\'я хоста - Використати хост по замовчанню - - - Пошук каналів та епізодів - Знайдено у примітках - Знайдено в главах - Жодних результатів немає - Пошук - Знайдено у назві - - OPML файли дозволяют вам перенести подскати з однієї программи до іншої - Для імпорту OPML файлу, скопіюйте його в цю папку та натіснить кнопку внизу для початку імпорту - Почати імпорт - OPML імпорт - Помилка! - Читаємо OPML файл - Трапилась помілка коли читали OPML документ: - Директорія імпорту пуста - Обрати все - Убрати виділення - Обрати файл для імпорту - OPML экспорт - Експорт ... - Помилка експорту - OPML експорт успішний - OPML файл записаний в:\u0020 - - Таймер сну - Вимкнути засинання - Встановити час - Таймер сну - Залишилось:\u0020 - Помилка вводу, час повинен бути цілим - секунд - хвилин - годин - - КАТЕГОРІЇ - ТОП ПОДКАСТІВ - РЕКОМЕНДАЦІЇ - Пошук на gpodder.net - Логін - Ласкаво просимо до gpodder.net. Зпочатку заповнить вашу інформацію для входу - Логін - Якщо ви щє не маєте логіну, ви можете отримати тут:\nhttps://gpodder.net/register - Ім\'я користувача - Пароль - Обрати пристрій - Під\'єднати новий пристрій к gpodder.net обліковому запису о обрати інсуючий - ID Пристрою:\u0020 - Заголовок - Створити новий пристрій - Вибрати існуючий пристрій - ID пристрою не можете бути пустим - Таке ID пристрою вже є - Обрати - Успішно зайшли - Поздоровляємо! Ваш обліковий запис на gpodder.net зараз пов\'язаний за вашим пристроєм - Почати синхронізацію - Перейти до основного екрана - Помилка авторізації на gpodder.net - Помилка в імені користувача або паролі - gpodder.net помилка синхронізації - Трапилась помилка при сінхронизації:\u0020 - - Обрати папку: - Нова папка - Обрати папку - Створити папку з ім\'ям \"%1$s\"? - Створена нова папка - Не можу записати в цю папку - Папка вже є - Не можу создати папку - В папці щось є - В папці щось є. Всі завантаження зберігаються в цю папку. Все рівно продовжувати? - Обрати папку по замовчанню - Призупиняти програвання замість зниження гучності коли інша програма хоче програти звук - Пауза для перевивання - - Підписатися - Підписано - Завантаження... - - Показати глави - Показати нотатки - Показати зображення - Перемотка назад - Перемотка вперед - Звук - Відео - Догори - Додаткові дії - Епізод програється - Епізод завантажується - Епізод завантажено - Нове - Епізод чекає в черзі - Кількість нових епізодів - Кількість епізодів що ви почали слухати - Перетягніть щоб змінити позицію - - Автентикація - Змінити ваші логін та пароль для подкаста та епізодів - - Імпорт подкастів з інших програм... - diff --git a/app/src/main/res/values-v11/colors.xml b/app/src/main/res/values-v11/colors.xml deleted file mode 100644 index 520efaa06..000000000 --- a/app/src/main/res/values-v11/colors.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - #286E8A - #81CFEA - \ No newline at end of file diff --git a/app/src/main/res/values-v14/dimens.xml b/app/src/main/res/values-v14/dimens.xml deleted file mode 100644 index 090a476a8..000000000 --- a/app/src/main/res/values-v14/dimens.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - 0dp - - \ No newline at end of file diff --git a/app/src/main/res/values-v14/styles.xml b/app/src/main/res/values-v14/styles.xml deleted file mode 100644 index 6a39d6175..000000000 --- a/app/src/main/res/values-v14/styles.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/values-v16/styles.xml b/app/src/main/res/values-v16/styles.xml deleted file mode 100644 index e7c56b5f5..000000000 --- a/app/src/main/res/values-v16/styles.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/values-v19/colors.xml b/app/src/main/res/values-v19/colors.xml deleted file mode 100644 index 16c065d75..000000000 --- a/app/src/main/res/values-v19/colors.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - #484B4D - #E3E3E3 - \ No newline at end of file diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml deleted file mode 100644 index 63320b851..000000000 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ /dev/null @@ -1,317 +0,0 @@ - - - - AntennaPod - 订阅 - 添加博客 - 播客 - 曲目 - 新曲目 - 所有曲目 - 最新 - 等待列表 - 设置 - 添加播客 - 下载 - 正在运行 - 已完成 - 日志 - 取消下载 - 播放历史 - gpodder.net - gpodder.net 登录 - - 最近发布 - 仅显示新曲目 - - 打开菜单 - 关闭菜单 - - 在浏览器打开 - 复制 URL - 分享 URL - 复制 URL 到剪贴板. - - 清空历史 - - 确定 - 取消 - 作者 - 语言 - 设置 - 图片 - 错误 - 出错: - 刷新 - 没有可用的外部存储. 请确保安装外部存储器, 这样本应用才可以正常工作. - 章节 - 笔记 - 描述 - 最近曲目:\u0020 - \u0020 曲 - 长度:\u0020 - 大小:\u0020 - 处理中 - 加载中... - 保存用户名密码 - 关闭 - 重试 - 包含到自动下载 - - 订阅 URL - 添加播客 URL - 您可以在 gpodder.net 通过名称、类别或热门来搜索新播客 - 浏览 gpodder.net - - 全部标识已读 - 将所有曲目标记为已读 - 查看信息 - 删除播客 - 分享网站链接 - 分享订阅链接 - 确认要删除这些订阅吗? 该订阅所有已经下载的曲目将一并删除. - 删除订阅 - - 下载 - 播放 - 暂停 - 流媒体 - 删除 - 移除曲目 - 标记已读 - 标记未读 - 添加到播放列表 - 从播放列表中删除 - 访问网站 - Flattr 他 - 全部添加到播放列表 - 全部下载 - 跳过曲目 - - 成功 - 失败 - 下载等待 - 下载中 - 没有找到存储设备 - 空间不足 - 文件错误 - HTTP 数据错误 - 未知错误 - 解析异常 - 未提供的订阅类型 - 链接错误 - 未知主机 - 认证错误 - 取消所有下载 - 已取消下载 - 下载完成 - 畸形 URL - IO 错误 - 请求出错 - 数据库访问错误 - \u0020 下载剩余 - 正在处理下载 - 下载播客数据 - %1$d 下载成功, %2$d 失败 - 未知标题 - 订阅 - 媒体文件 - 图片 - 尝试下载文件:\u0020 时出错 - 需要认证 - 您所请求的资源需要用户名和密码 - - 错误! - 没有可播放媒体 - 预备 - 准备 - 查找 - 服务器宕机 - 未知错误 - 没有可播放的媒体 - 00:00:00 - 缓冲中 - 播客播放中 - - 清空播放列表 - 撤消 - 已删除项 - 移到顶端 - 移到下部 - - Flattr 登录 - 按下面的按钮开始身份验证过程. 将在浏览器中打开 Flattr 登录界面并要求给予 AntennaPod 访问 Flattr 的权限. 权限许可后, 将自动回到这个界面. - 验证 - 返回主页 - 验证成功! 现在可以使用应用内 Flattr 相关功能了. - 没有找到 Flattr 验证令牌信息 - 您的 flattr 账户似乎并没有连接到 AntennaPod. You can either connect your account to AntennaPod to flattr things within the app or you can visit the website of the thing to flattr it there. - 验证 - 被禁止 - AntennaPod 没有权限执行本动作. 原因可能是: AntennaPod 对您账户的访问令牌被撤销. 你可以重新\"验证\"或访问该网站来授权. - 撤销访问 - 您已经成功撤销 AntennaPod 对账户令牌的访问. 为了完成这个过程, 您必须到 Flattr 网站 \"账户设置->已批准应用\" 列表内删除本应用. - - - 插件下载 - 插件没有安装 - 安装第三方库后播放速度设置起作用.\n点击 \'插件下载\' 从 \'Pay 商店\' 下载免费插件.\n使用这些插件中碰到的任何问题请报告给插件作者, 跟 AntennaPod 无关. - 播放速度 - - 列表为空. - 还没有任何订阅. - - 其他 - 关于 - 播放列表 - 服务 - Flattr - 耳机断开时暂停播放 - 播放完成跳转到播放列表下一项 - 播放 - 网络 - 更新周期 - 设置订阅自动刷新周期 - 仅在 WIFI 情况下载媒体文件 - 连续播放 - 仅在 WIFI 情况下载 - 耳机断开 - 数据网络时更新 - 允许移动数据网络情况下进行数据链接 - 刷新中 - Flattr 设置 - Flattr 登录 - 登录 Flattr 账户, 以便直接使用本应用中的相关功能. - Flattr 本应用 - 支持 AntennaPod 发展, 请 Flattring 他. 谢谢!!\n - 撤销访问 - 撤销访问本应用对您 Flattr 账户的访问权限. - 界面 - 主题选择 - 改变 AntennaPod 外观 - 自动下载 - 配置自动下载的曲目 - 打开 Wi-Fi 过滤器 - 只允许在 Wi-Fi 网络下自动下载 - 曲目缓存 - 浅色 - 暗色 - 无限 - 小时 - - 手动 - 登录 - 登录 gpodder.net 账户同步订阅 - 注销 - 注销成功 - 改变登录信息 - 改变 gpodder.net 账户登录信息. - 播放速度 - 自定义音频播放速度 - 设置主机名 - 使用默认主机 - - - 搜索订阅或者曲目 - 笔记中查找 - 章节中查找 - 没有找到任何结果 - 搜索 - 标题中查找 - - OPML 文件可以方便的从别的播客转移数据过来。 - 导入 OPML 文件, 您必须将它放在以下目录, 之后按下面的按钮开始导入处理. - 开始导入 - OPML 导入 - 错误! - OPML 文件读取中 - 读取 OPML 文件内容出错: - 导入目录为空. - 全选 - 取消所有选择 - 选择导入文件 - OPML 导出 - 导出中... - 导出出错 - OPML 导出成功. - .opml 文件已保存到:\u0020 - - 设置休眠计时器 - 禁用休眠计时器 - 输入时间 - 休眠计时器 - 计时剩余:\u0020 - 无效的输入, 时间是一个整数 - - 分钟 - 小时 - - 目录 - 头条播客 - 建议 - 搜索 gpodder.net - 登录 - 欢迎进入 gpodder.net 登录流程. 首先, 输入请你的登录信息: - 登录 - 如果还没有账户, 从这里创建:⏎\nhttps://gpodder.net/register/ - 用户名 - 密码 - 设备选择 - 为你的 gpodder.net 账户创建一个新设备或者选择一个已存在的: - 设备编号: \u0020 - 标题 - 创建新设备 - 选择已存在设备 - 设备编号必须填写 - 设备编号已被使用 - 选择 - 登录成功! - 恭喜! 你的 gpodder.net 帐户与设备已连结完成. 现在开始 AntennaPod 将自动同步你 gpodder.net 帐户内的订阅信息到设备上. - 开始同步 - 返回主屏 - gpodder.net 验证错误 - 错误的用户名或者密码 - gpodder.net 同步错误 - 同步过程中发生错误: \u0020 - - 已选文件夹: - 穿件文件夹 - 选择数据文件夹 - 确实创建 \"%1$s\" 文件夹? - 创建新文件夹 - 本文件夹不能写入 - 文件夹已存在 - 不能创建文件夹 - 文件夹不能为空 - 您所选择的文件夹不能为空. 媒体下载和其他文件将被直接放在本文件夹. 确认继续吗? - 选择默认文件夹 - 当另一个应用程序要播放声音时暂停播放, 而不是降低音量 - 中断暂停 - - 订阅 - 已订阅 - 下载中... - - 显示章节 - 显示笔记 - 显示图片 - 回放 - 快进 - 音频 - 视频 - 向上导航 - 更多动作 - 曲目正在播放 - 曲目正在下载 - 曲目已下载 - 新项目 - 曲目已经在播放列表中 - 新曲目数 - 已收听曲目数 - 拖动以变更本项目的位置 - - 验证 - 给本播客及曲目变更用户名及密码 - - 正在从选定的应用中导入订阅... - diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml deleted file mode 100644 index f09c76080..000000000 --- a/app/src/main/res/values/arrays.xml +++ /dev/null @@ -1,114 +0,0 @@ - - - - - 5 - 10 - 15 - 20 - 30 - 45 - 60 - - - - Manual - 1 hour - 2 hours - 4 hours - 8 hours - 12 hours - 24 hours - - - - 0 - 1 - 2 - 4 - 8 - 12 - 24 - - - @string/pref_episode_cache_unlimited - 10 - 20 - 40 - 60 - 80 - 100 - - - -1 - 10 - 20 - 40 - 60 - 80 - 100 - - - 0.5 - 0.6 - 0.7 - 0.8 - 0.9 - 1.0 - 1.05 - 1.10 - 1.15 - 1.20 - 1.25 - 1.30 - 1.35 - 1.40 - 1.45 - 1.50 - 1.55 - 1.60 - 1.65 - 1.70 - 1.75 - 1.80 - 1.85 - 1.90 - 1.95 - 2.00 - 2.10 - 2.20 - 2.30 - 2.40 - 2.50 - 2.60 - 2.70 - 2.80 - 2.90 - 3.00 - 3.10 - 3.20 - 3.30 - 3.40 - 3.50 - 3.60 - 3.70 - 3.80 - 3.90 - 4.00 - - - - N/A - - - 0 - - - @string/pref_theme_title_light - @string/pref_theme_title_dark - - - 0 - 1 - - \ No newline at end of file diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml deleted file mode 100644 index 08a8063c1..000000000 --- a/app/src/main/res/values/attrs.xml +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml deleted file mode 100644 index 6b535079d..000000000 --- a/app/src/main/res/values/colors.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - #FFFFFF - #808080 - #000000 - #33B5E5 - #858585 - #DDDDDD - #669900 - #CC0000 - #E033B5E5 - #E0EE5F52 - #262C31 - #DDDDDD - #EDEDED - #060708 - #669900 - - - #FEBB20 - #FEBB20 - - \ No newline at end of file diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml deleted file mode 100644 index 1ebcdb76d..000000000 --- a/app/src/main/res/values/dimens.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - 8dp - 70dp - 70dp - 20dp - 12sp - 14sp - 16sp - 18sp - 22sp - 32dp - 85dp - 70dp - 70dp - 110dp - 42dp - 48dp - 280dp - @dimen/text_size_small - - \ No newline at end of file diff --git a/app/src/main/res/values/ids.xml b/app/src/main/res/values/ids.xml deleted file mode 100644 index 90e405fde..000000000 --- a/app/src/main/res/values/ids.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/values/integers.xml b/app/src/main/res/values/integers.xml deleted file mode 100644 index 33501d9fb..000000000 --- a/app/src/main/res/values/integers.xml +++ /dev/null @@ -1,4 +0,0 @@ - - 5000 - -1 - diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml deleted file mode 100644 index b5cc4ee86..000000000 --- a/app/src/main/res/values/strings.xml +++ /dev/null @@ -1,374 +0,0 @@ - - - - - AntennaPod - Feeds - Add podcast - PODCASTS - EPISODES - New episodes - All episodes - New - Waiting list - Settings - Add podcast - Downloads - Running - Completed - Log - Cancel Download - Playback history - gpodder.net - gpodder.net login - - - Recently published - Show only new episodes - - - Open menu - Close menu - - - Open in browser - Copy URL - Share URL - Copied URL to clipboard. - Go to this position - - - Clear history - - - Confirm - Cancel - Author - Language - Settings - Picture - Error - An error occurred: - Refresh - No external storage is available. Please make sure that external storage is mounted so that the app can work properly. - Chapters - Shownotes - Description - Most Recent Episode:\u0020 - \u0020episodes - Length:\u0020 - Size:\u0020 - Processing - Loading... - Save username and password - Close - Retry - Include in auto downloads - - - Feed URL - URL of feed or website - Add Podcast by URL - Find podcast in directory - You can search for new podcasts by name, category or popularity in the gpodder.net directory. - Browse gpodder.net - - - Mark all as read - Marked all episodes as read - Show information - Remove podcast - Share website link - Share feed link - Please confirm that you want to delete this feed and ALL episodes of this feed that you have downloaded. - Removing feed - - - Download - Play - Pause - Stream - Remove - Remove episode - Mark as read - Mark as unread - Add to Queue - Remove from Queue - Visit Website - Flattr this - Enqueue all - Download all - Skip episode - - - successful - failed - Download pending - Download running - Storage device not found - Insufficient space - File error - HTTP Data Error - Unknown Error - Parser Exception - Unsupported Feed type - Connection error - Unknown host - Authentication error - Cancel all downloads - Download cancelled - Downloads completed - Malformed URL - IO Error - Request error - Database access error - \u0020Downloads left - Processing downloads - Downloading podcast data - %1$d downloads succeeded, %2$d failed - Unknown title - Feed - Media file - Image - An error occurred when trying to download the file:\u0020 - Authentication required - The resource you requested requires a username and a password - - - Error! - No media playing - Preparing - Ready - Seeking - Server died - Unknown Error - No media playing - 00:00:00 - Buffering - Playing podcast - AntennaPod - Unknown media key: %1$d - - - Clear queue - Undo - Item removed - Move to top - Move to bottom - - - Flattr sign-in - Press the button below to start the authentication process. You will be forwarded to the flattr login screen in your browser and be asked to give AntennaPod the permission to flattr things. After you have given permission, you will return to this screen automatically. - Authenticate - Return to home - Authentication was successful! You can now flattr things within the app. - No Flattr token found - Your flattr account does not seem to be connected to AntennaPod. Tap here to authenticate. - Your flattr account does not seem to be connected to AntennaPod. You can either connect your account to AntennaPod to flattr things within the app or you can visit the website of the thing to flattr it there. - Authenticate - Action forbidden - AntennaPod has no permission for this action. The reason for this could be that the access token of AntennaPod to your account has been revoked. You can either re-reauthenticate or visit the website of the thing instead. - Access revoked - You have successfully revoked AntennaPod\'s access token to your account. In order to complete the process, you have to remove this app from the list of approved applications in your account settings on the flattr website. - - - Flattr\'ed one thing! - Flattr\'ed %d things! - Flattr\'ed: %s. - Failed to flattr %d things! - Not flattr\'ed: %s. - Thing will be flattr\'ed later - Flattring %s - AntennaPod is flattring - AntennaPod has flattr\'ed - AntennaPod flattr failed - Retrieving flattr\'ed things - - - Download Plugin - Plugin Not Installed - For variable speed playback to work, a third party library must be installed.\n\nTap \'Download Plugin\' to download a free plugin from the Play Store\n\nAny problems found using this plugin are not the responsibility of AntennaPod and should be reported to the plugin owner. - Playback Speeds - - - There are no items in this list. - You haven\'t subscribed to any feeds yet. - - - Other - About - Queue - Services - Flattr - Pause playback when the headphones are disconnected - Jump to next queue item when playback completes - Playback - Network - Update interval - Specify an interval in which the feeds are refreshed automatically or disable it - Download media files only over WiFi - Continuous playback - WiFi media download - Headphones disconnect - Mobile updates - Allow updates over the mobile data connection - Refreshing - Flattr settings - Flattr sign-in - Sign in to your flattr account to flattr things directly from the app. - Flattr this app - Support the development of AntennaPod by flattring it. Thanks! - Revoke access - Revoke the access permission to your flattr account for this app. - Automatic Flattr - Configure automatic flattring - User Interface - Select theme - Change the appearance of AntennaPod. - Automatic download - Configure the automatic download of episodes. - Enable Wi-Fi filter - Allow automatic download only for selected Wi-Fi networks. - Episode cache - Light - Dark - Unlimited - hours - hour - Manual - Login - Login with your gpodder.net account in order to sync your subscriptions. - Logout - Logout was successful - Change login information - Change the login information for your gpodder.net account. - Playback Speeds - Customize the speeds available for variable speed audio playback - Seek time - Seek this many seconds when rewinding or fast-forwarding - Set hostname - Use default host - - - Enable automatic flattring - Flattr episode as soon as %d percent have been played - Flattr episode when playback starts - Flattr episode when playback ends - - - Search for Feeds or Episodes - Found in shownotes - Found in chapters - No results were found - Search - Found in title - - - OPML files allow you to move your podcasts from one podcatcher to another. - To import an OPML file, you have to place it in the following directory and press the button below to start the import process. - Start import - OPML import - ERROR! - Reading OPML file - An error has occurred while reading the opml document: - The import directory is empty. - Select all - Deselect all - Choose file to import - OPML export - Exporting... - Export error - Opml export successful. - The .opml file was written to:\u0020 - - - Set sleep timer - Disable sleep timer - Enter time - Sleep timer - Time left:\u0020 - Invalid input, time has to be an integer - seconds - minutes - hours - - - CATEGORIES - TOP PODCASTS - SUGGESTIONS - Search gpodder.net - Login - Welcome to the gpodder.net login process. First, type in your login information: - Login - If you do not have an account yet, you can create one here:\nhttps://gpodder.net/register/ - Username - Password - Device Selection - Create a new device to use for your gpodder.net account or choose an existing one: - Device ID:\u0020 - Caption - Create new device - Choose existing device: - Device ID must not be empty - Device ID already in use - - Choose - Login successful! - Congratulations! Your gpodder.net account is now linked with your device. AntennaPod will from now on automatically sync subscriptions on your device with your gpodder.net account. - Start sync now - Go to main screen - - gpodder.net authentication error - Wrong username or password - gpodder.net sync error - An error occurred during syncing:\u0020 - - - Selected folder: - Create folder - Choose data folder - Create new folder with name "%1$s"? - Created new folder - Cannot write to this folder - Folder already exists - Could not create folder - Folder is not empty - The folder you have selected is not empty. Media downloads and other files will be placed directly in this folder. Continue anyway? - Choose default folder - Pause playback instead of lowering volume when another app wants to play sounds - Pause for interruptions - - - Subscribe - Subscribed - Downloading... - - - Show chapters - Show shownotes - Show picture - Rewind - Fast forward - Audio - Video - Navigate upwards - More actions - Episode is being played - Episode is being downloaded - Episode is downloaded - Item is new - Episode is in the queue - Number of new episodes - Number of episodes you have started listening to - Drag to change the position of this item - - - Authentication - Change your username and password for this podcast and its episodes. - - - - Importing subscriptions from single-purpose apps… - diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml deleted file mode 100644 index e42072afa..000000000 --- a/app/src/main/res/values/styles.xml +++ /dev/null @@ -1,174 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/core/.gitignore b/core/.gitignore new file mode 100644 index 000000000..796b96d1c --- /dev/null +++ b/core/.gitignore @@ -0,0 +1 @@ +/build diff --git a/core/build.gradle b/core/build.gradle new file mode 100644 index 000000000..132d68084 --- /dev/null +++ b/core/build.gradle @@ -0,0 +1,43 @@ +apply plugin: 'com.android.library' + +android { + compileSdkVersion 20 + buildToolsVersion "20.0.0" + + defaultConfig { + applicationId "de.danoeh.antennapod.core" + minSdkVersion 10 + targetSdkVersion 20 + versionCode 1 + versionName "1.0" + } + buildTypes { + release { + runProguard false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + compile fileTree(dir: 'libs', include: ['*.jar']) + compile 'com.android.support:appcompat-v7:20.0.0' + compile 'com.android.support:support-v4:20.0.0' + compile 'org.apache.commons:commons-lang3:3.3.2' + compile ('org.shredzone.flattr4j:flattr4j-core:2.10') { + exclude group: 'org.apache.httpcomponents', module: 'httpcore' + exclude group: 'org.apache.httpcomponents', module: 'httpclient' + exclude group: 'org.json', module: 'json' + } + compile 'commons-io:commons-io:2.4' + compile 'com.nineoldandroids:library:2.4.0' + compile 'com.jayway.android.robotium:robotium-solo:5.2.1' + compile ("com.doomonafireball.betterpickers:library:1.5.2") { + exclude group: 'com.android.support', module: 'support-v4' + } + compile 'org.jsoup:jsoup:1.7.3' + compile 'com.squareup.picasso:picasso:2.3.4' + compile 'com.squareup.okhttp:okhttp:2.0.0' + compile 'com.squareup.okhttp:okhttp-urlconnection:2.0.0' + compile 'com.squareup.okio:okio:1.0.0' +} diff --git a/core/proguard-rules.pro b/core/proguard-rules.pro new file mode 100644 index 000000000..41a9efda7 --- /dev/null +++ b/core/proguard-rules.pro @@ -0,0 +1,17 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /Users/daniel/bin/android-sdk/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/core/src/androidTest/java/de/danoeh/antennapod/core/ApplicationTest.java b/core/src/androidTest/java/de/danoeh/antennapod/core/ApplicationTest.java new file mode 100644 index 000000000..894bcfa63 --- /dev/null +++ b/core/src/androidTest/java/de/danoeh/antennapod/core/ApplicationTest.java @@ -0,0 +1,13 @@ +package de.danoeh.antennapod.core; + +import android.app.Application; +import android.test.ApplicationTestCase; + +/** + * Testing Fundamentals + */ +public class ApplicationTest extends ApplicationTestCase { + public ApplicationTest() { + super(Application.class); + } +} \ No newline at end of file diff --git a/core/src/main/AndroidManifest.xml b/core/src/main/AndroidManifest.xml new file mode 100644 index 000000000..db67b8003 --- /dev/null +++ b/core/src/main/AndroidManifest.xml @@ -0,0 +1,11 @@ + + + + + + + diff --git a/core/src/main/aidl/com/aocate/presto/service/IDeathCallback_0_8.aidl b/core/src/main/aidl/com/aocate/presto/service/IDeathCallback_0_8.aidl new file mode 100644 index 000000000..6bdc76801 --- /dev/null +++ b/core/src/main/aidl/com/aocate/presto/service/IDeathCallback_0_8.aidl @@ -0,0 +1,18 @@ +// Copyright 2011, Aocate, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.aocate.presto.service; + +oneway interface IDeathCallback_0_8 { +} \ No newline at end of file diff --git a/core/src/main/aidl/com/aocate/presto/service/IOnBufferingUpdateListenerCallback_0_8.aidl b/core/src/main/aidl/com/aocate/presto/service/IOnBufferingUpdateListenerCallback_0_8.aidl new file mode 100644 index 000000000..7357e402e --- /dev/null +++ b/core/src/main/aidl/com/aocate/presto/service/IOnBufferingUpdateListenerCallback_0_8.aidl @@ -0,0 +1,19 @@ +// Copyright 2011, Aocate, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.aocate.presto.service; + +interface IOnBufferingUpdateListenerCallback_0_8 { + void onBufferingUpdate(int percent); +} \ No newline at end of file diff --git a/core/src/main/aidl/com/aocate/presto/service/IOnCompletionListenerCallback_0_8.aidl b/core/src/main/aidl/com/aocate/presto/service/IOnCompletionListenerCallback_0_8.aidl new file mode 100644 index 000000000..d5edea729 --- /dev/null +++ b/core/src/main/aidl/com/aocate/presto/service/IOnCompletionListenerCallback_0_8.aidl @@ -0,0 +1,19 @@ +// Copyright 2011, Aocate, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.aocate.presto.service; + +interface IOnCompletionListenerCallback_0_8 { + void onCompletion(); +} \ No newline at end of file diff --git a/core/src/main/aidl/com/aocate/presto/service/IOnErrorListenerCallback_0_8.aidl b/core/src/main/aidl/com/aocate/presto/service/IOnErrorListenerCallback_0_8.aidl new file mode 100644 index 000000000..2c4f2df3e --- /dev/null +++ b/core/src/main/aidl/com/aocate/presto/service/IOnErrorListenerCallback_0_8.aidl @@ -0,0 +1,19 @@ +// Copyright 2011, Aocate, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.aocate.presto.service; + +interface IOnErrorListenerCallback_0_8 { + boolean onError(int what, int extra); +} diff --git a/core/src/main/aidl/com/aocate/presto/service/IOnInfoListenerCallback_0_8.aidl b/core/src/main/aidl/com/aocate/presto/service/IOnInfoListenerCallback_0_8.aidl new file mode 100644 index 000000000..9dbd1d260 --- /dev/null +++ b/core/src/main/aidl/com/aocate/presto/service/IOnInfoListenerCallback_0_8.aidl @@ -0,0 +1,19 @@ +// Copyright 2011, Aocate, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.aocate.presto.service; + +interface IOnInfoListenerCallback_0_8 { + boolean onInfo(int what, int extra); +} \ No newline at end of file diff --git a/core/src/main/aidl/com/aocate/presto/service/IOnPitchAdjustmentAvailableChangedListenerCallback_0_8.aidl b/core/src/main/aidl/com/aocate/presto/service/IOnPitchAdjustmentAvailableChangedListenerCallback_0_8.aidl new file mode 100644 index 000000000..41223a97b --- /dev/null +++ b/core/src/main/aidl/com/aocate/presto/service/IOnPitchAdjustmentAvailableChangedListenerCallback_0_8.aidl @@ -0,0 +1,19 @@ +// Copyright 2011, Aocate, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.aocate.presto.service; + +interface IOnPitchAdjustmentAvailableChangedListenerCallback_0_8 { + void onPitchAdjustmentAvailableChanged(boolean pitchAdjustmentAvailable); +} \ No newline at end of file diff --git a/core/src/main/aidl/com/aocate/presto/service/IOnPreparedListenerCallback_0_8.aidl b/core/src/main/aidl/com/aocate/presto/service/IOnPreparedListenerCallback_0_8.aidl new file mode 100644 index 000000000..7be8f1237 --- /dev/null +++ b/core/src/main/aidl/com/aocate/presto/service/IOnPreparedListenerCallback_0_8.aidl @@ -0,0 +1,19 @@ +// Copyright 2011, Aocate, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.aocate.presto.service; + +interface IOnPreparedListenerCallback_0_8 { + void onPrepared(); +} \ No newline at end of file diff --git a/core/src/main/aidl/com/aocate/presto/service/IOnSeekCompleteListenerCallback_0_8.aidl b/core/src/main/aidl/com/aocate/presto/service/IOnSeekCompleteListenerCallback_0_8.aidl new file mode 100644 index 000000000..5bdda98b6 --- /dev/null +++ b/core/src/main/aidl/com/aocate/presto/service/IOnSeekCompleteListenerCallback_0_8.aidl @@ -0,0 +1,19 @@ +// Copyright 2011, Aocate, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.aocate.presto.service; + +interface IOnSeekCompleteListenerCallback_0_8 { + void onSeekComplete(); +} \ No newline at end of file diff --git a/core/src/main/aidl/com/aocate/presto/service/IOnSpeedAdjustmentAvailableChangedListenerCallback_0_8.aidl b/core/src/main/aidl/com/aocate/presto/service/IOnSpeedAdjustmentAvailableChangedListenerCallback_0_8.aidl new file mode 100644 index 000000000..a69c1cf34 --- /dev/null +++ b/core/src/main/aidl/com/aocate/presto/service/IOnSpeedAdjustmentAvailableChangedListenerCallback_0_8.aidl @@ -0,0 +1,19 @@ +// Copyright 2011, Aocate, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.aocate.presto.service; + +interface IOnSpeedAdjustmentAvailableChangedListenerCallback_0_8 { + void onSpeedAdjustmentAvailableChanged(boolean speedAdjustmentAvailable); +} \ No newline at end of file diff --git a/core/src/main/aidl/com/aocate/presto/service/IPlayMedia_0_8.aidl b/core/src/main/aidl/com/aocate/presto/service/IPlayMedia_0_8.aidl new file mode 100644 index 000000000..12a6047de --- /dev/null +++ b/core/src/main/aidl/com/aocate/presto/service/IPlayMedia_0_8.aidl @@ -0,0 +1,75 @@ +// Copyright 2011, Aocate, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.aocate.presto.service; + +import com.aocate.presto.service.IDeathCallback_0_8; +import com.aocate.presto.service.IOnBufferingUpdateListenerCallback_0_8; +import com.aocate.presto.service.IOnCompletionListenerCallback_0_8; +import com.aocate.presto.service.IOnErrorListenerCallback_0_8; +import com.aocate.presto.service.IOnPitchAdjustmentAvailableChangedListenerCallback_0_8; +import com.aocate.presto.service.IOnPreparedListenerCallback_0_8; +import com.aocate.presto.service.IOnSeekCompleteListenerCallback_0_8; +import com.aocate.presto.service.IOnSpeedAdjustmentAvailableChangedListenerCallback_0_8; +import com.aocate.presto.service.IOnInfoListenerCallback_0_8; + +interface IPlayMedia_0_8 { + boolean canSetPitch(long sessionId); + boolean canSetSpeed(long sessionId); + float getCurrentPitchStepsAdjustment(long sessionId); + int getCurrentPosition(long sessionId); + float getCurrentSpeedMultiplier(long sessionId); + int getDuration(long sessionId); + float getMaxSpeedMultiplier(long sessionId); + float getMinSpeedMultiplier(long sessionId); + int getVersionCode(); + String getVersionName(); + boolean isLooping(long sessionId); + boolean isPlaying(long sessionId); + void pause(long sessionId); + void prepare(long sessionId); + void prepareAsync(long sessionId); + void registerOnBufferingUpdateCallback(long sessionId, IOnBufferingUpdateListenerCallback_0_8 cb); + void registerOnCompletionCallback(long sessionId, IOnCompletionListenerCallback_0_8 cb); + void registerOnErrorCallback(long sessionId, IOnErrorListenerCallback_0_8 cb); + void registerOnInfoCallback(long sessionId, IOnInfoListenerCallback_0_8 cb); + void registerOnPitchAdjustmentAvailableChangedCallback(long sessionId, IOnPitchAdjustmentAvailableChangedListenerCallback_0_8 cb); + void registerOnPreparedCallback(long sessionId, IOnPreparedListenerCallback_0_8 cb); + void registerOnSeekCompleteCallback(long sessionId, IOnSeekCompleteListenerCallback_0_8 cb); + void registerOnSpeedAdjustmentAvailableChangedCallback(long sessionId, IOnSpeedAdjustmentAvailableChangedListenerCallback_0_8 cb); + void release(long sessionId); + void reset(long sessionId); + void seekTo(long sessionId, int msec); + void setAudioStreamType(long sessionId, int streamtype); + void setDataSourceString(long sessionId, String path); + void setDataSourceUri(long sessionId, in Uri uri); + void setEnableSpeedAdjustment(long sessionId, boolean enableSpeedAdjustment); + void setLooping(long sessionId, boolean looping); + void setPitchStepsAdjustment(long sessionId, float pitchSteps); + void setPlaybackPitch(long sessionId, float f); + void setPlaybackSpeed(long sessionId, float f); + void setSpeedAdjustmentAlgorithm(long sessionId, int algorithm); + void setVolume(long sessionId, float left, float right); + void start(long sessionId); + long startSession(IDeathCallback_0_8 cb); + void stop(long sessionId); + void unregisterOnBufferingUpdateCallback(long sessionId, IOnBufferingUpdateListenerCallback_0_8 cb); + void unregisterOnCompletionCallback(long sessionId, IOnCompletionListenerCallback_0_8 cb); + void unregisterOnErrorCallback(long sessionId, IOnErrorListenerCallback_0_8 cb); + void unregisterOnInfoCallback(long sessionId, IOnInfoListenerCallback_0_8 cb); + void unregisterOnPitchAdjustmentAvailableChangedCallback(long sessionId, IOnPitchAdjustmentAvailableChangedListenerCallback_0_8 cb); + void unregisterOnPreparedCallback(long sessionId, IOnPreparedListenerCallback_0_8 cb); + void unregisterOnSeekCompleteCallback(long sessionId, IOnSeekCompleteListenerCallback_0_8 cb); + void unregisterOnSpeedAdjustmentAvailableChangedCallback(long sessionId, IOnSpeedAdjustmentAvailableChangedListenerCallback_0_8 cb); +} \ No newline at end of file diff --git a/core/src/main/java/com/aocate/media/AndroidMediaPlayer.java b/core/src/main/java/com/aocate/media/AndroidMediaPlayer.java new file mode 100644 index 000000000..17ee74a13 --- /dev/null +++ b/core/src/main/java/com/aocate/media/AndroidMediaPlayer.java @@ -0,0 +1,470 @@ +// Copyright 2011, Aocate, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.aocate.media; + +import java.io.IOException; + +import android.content.Context; +import android.media.MediaPlayer; +import android.net.Uri; +import android.util.Log; + +public class AndroidMediaPlayer extends MediaPlayerImpl { + private final static String AMP_TAG = "AocateAndroidMediaPlayer"; + + // private static final long TIMEOUT_DURATION_MS = 500; + + android.media.MediaPlayer mp = null; + + private android.media.MediaPlayer.OnBufferingUpdateListener onBufferingUpdateListener = new android.media.MediaPlayer.OnBufferingUpdateListener() { + public void onBufferingUpdate(android.media.MediaPlayer mp, int percent) { + if (owningMediaPlayer != null) { + owningMediaPlayer.lock.lock(); + try { + if ((owningMediaPlayer.onBufferingUpdateListener != null) + && (owningMediaPlayer.mpi == AndroidMediaPlayer.this)) { + owningMediaPlayer.onBufferingUpdateListener.onBufferingUpdate(owningMediaPlayer, percent); + } + } + finally { + owningMediaPlayer.lock.unlock(); + } + } + + } + }; + + private android.media.MediaPlayer.OnCompletionListener onCompletionListener = new android.media.MediaPlayer.OnCompletionListener() { + public void onCompletion(android.media.MediaPlayer mp) { + Log.d(AMP_TAG, "onCompletionListener being called"); + if (owningMediaPlayer != null) { + owningMediaPlayer.lock.lock(); + try { + if (owningMediaPlayer.onCompletionListener != null) { + owningMediaPlayer.onCompletionListener.onCompletion(owningMediaPlayer); + } + } + finally { + owningMediaPlayer.lock.unlock(); + } + } + } + }; + + private android.media.MediaPlayer.OnErrorListener onErrorListener = new android.media.MediaPlayer.OnErrorListener() { + public boolean onError(android.media.MediaPlayer mp, int what, int extra) { + // Once we're in errored state, any received messages are going to be junked + if (owningMediaPlayer != null) { + owningMediaPlayer.lock.lock(); + try { + if (owningMediaPlayer.onErrorListener != null) { + return owningMediaPlayer.onErrorListener.onError(owningMediaPlayer, what, extra); + } + } + finally { + owningMediaPlayer.lock.unlock(); + } + } + return false; + } + }; + + private android.media.MediaPlayer.OnInfoListener onInfoListener = new android.media.MediaPlayer.OnInfoListener() { + public boolean onInfo(android.media.MediaPlayer mp, int what, int extra) { + if (owningMediaPlayer != null) { + owningMediaPlayer.lock.lock(); + try { + if ((owningMediaPlayer.onInfoListener != null) + && (owningMediaPlayer.mpi == AndroidMediaPlayer.this)) { + return owningMediaPlayer.onInfoListener.onInfo(owningMediaPlayer, what, extra); + } + } + finally { + owningMediaPlayer.lock.unlock(); + } + } + return false; + } + }; + + // We have to assign this.onPreparedListener because the + // onPreparedListener in owningMediaPlayer sets the state + // to PREPARED. Due to prepareAsync, that's the only + // reasonable place to do it + // The others it just didn't make sense to have a setOnXListener that didn't use the parameter + private android.media.MediaPlayer.OnPreparedListener onPreparedListener = new android.media.MediaPlayer.OnPreparedListener() { + public void onPrepared(android.media.MediaPlayer mp) { + Log.d(AMP_TAG, "Calling onPreparedListener.onPrepared()"); + if (AndroidMediaPlayer.this.owningMediaPlayer != null) { + AndroidMediaPlayer.this.lockMuteOnPreparedCount.lock(); + try { + if (AndroidMediaPlayer.this.muteOnPreparedCount > 0) { + AndroidMediaPlayer.this.muteOnPreparedCount--; + } + else { + AndroidMediaPlayer.this.muteOnPreparedCount = 0; + if (AndroidMediaPlayer.this.owningMediaPlayer.onPreparedListener != null) { + Log.d(AMP_TAG, "Invoking AndroidMediaPlayer.this.owningMediaPlayer.onPreparedListener.onPrepared"); + AndroidMediaPlayer.this.owningMediaPlayer.onPreparedListener.onPrepared(AndroidMediaPlayer.this.owningMediaPlayer); + } + } + } + finally { + AndroidMediaPlayer.this.lockMuteOnPreparedCount.unlock(); + } + if (owningMediaPlayer.mpi != AndroidMediaPlayer.this) { + Log.d(AMP_TAG, "owningMediaPlayer has changed implementation"); + } + } + } + }; + + private android.media.MediaPlayer.OnSeekCompleteListener onSeekCompleteListener = new android.media.MediaPlayer.OnSeekCompleteListener() { + public void onSeekComplete(android.media.MediaPlayer mp) { + if (owningMediaPlayer != null) { + owningMediaPlayer.lock.lock(); + try { + lockMuteOnSeekCount.lock(); + try { + if (AndroidMediaPlayer.this.muteOnSeekCount > 0) { + AndroidMediaPlayer.this.muteOnSeekCount--; + } + else { + AndroidMediaPlayer.this.muteOnSeekCount = 0; + if (AndroidMediaPlayer.this.owningMediaPlayer.onSeekCompleteListener != null) { + owningMediaPlayer.onSeekCompleteListener.onSeekComplete(owningMediaPlayer); + } + } + } + finally { + lockMuteOnSeekCount.unlock(); + } + } + finally { + owningMediaPlayer.lock.unlock(); + } + } + } + }; + + public AndroidMediaPlayer(com.aocate.media.MediaPlayer owningMediaPlayer, Context context) { + super(owningMediaPlayer, context); + + mp = new MediaPlayer(); + +// final ReentrantLock lock = new ReentrantLock(); +// Handler handler = new Handler(Looper.getMainLooper()) { +// @Override +// public void handleMessage(Message msg) { +// Log.d(AMP_TAG, "Instantiating new AndroidMediaPlayer from Handler"); +// lock.lock(); +// if (mp == null) { +// mp = new MediaPlayer(); +// } +// lock.unlock(); +// } +// }; +// +// long endTime = System.currentTimeMillis() + TIMEOUT_DURATION_MS; +// +// while (true) { +// // Retry messages until mp isn't null or it's time to give up +// handler.sendMessage(handler.obtainMessage()); +// if ((mp != null) +// || (endTime < System.currentTimeMillis())) { +// break; +// } +// try { +// Thread.sleep(50); +// } catch (InterruptedException e) { +// // TODO Auto-generated catch block +// e.printStackTrace(); +// } +// } + + if (mp == null) { + throw new IllegalStateException("Did not instantiate android.media.MediaPlayer successfully"); + } + + mp.setOnBufferingUpdateListener(this.onBufferingUpdateListener); + mp.setOnCompletionListener(this.onCompletionListener); + mp.setOnErrorListener(this.onErrorListener); + mp.setOnInfoListener(this.onInfoListener); + Log.d(AMP_TAG, " ++++++++++++++++++++++++++++++++ Setting prepared listener to this.onPreparedListener"); + mp.setOnPreparedListener(this.onPreparedListener); + mp.setOnSeekCompleteListener(this.onSeekCompleteListener); + } + + @Override + public boolean canSetPitch() { + return false; + } + + @Override + public boolean canSetSpeed() { + return false; + } + + @Override + public float getCurrentPitchStepsAdjustment() { + return 0; + } + + @Override + public int getCurrentPosition() { + owningMediaPlayer.lock.lock(); + try { + return mp.getCurrentPosition(); + } + finally { + owningMediaPlayer.lock.unlock(); + } + } + + @Override + public float getCurrentSpeedMultiplier() { + return 1f; + } + + @Override + public int getDuration() { + owningMediaPlayer.lock.lock(); + try { + return mp.getDuration(); + } + finally { + owningMediaPlayer.lock.unlock(); + } + } + + @Override + public float getMaxSpeedMultiplier() { + return 1f; + } + + @Override + public float getMinSpeedMultiplier() { + return 1f; + } + + @Override + public boolean isLooping() { + owningMediaPlayer.lock.lock(); + try { + return mp.isLooping(); + } + finally { + owningMediaPlayer.lock.unlock(); + } + } + + @Override + public boolean isPlaying() { + owningMediaPlayer.lock.lock(); + try { + return mp.isPlaying(); + } + finally { + owningMediaPlayer.lock.unlock(); + } + } + + @Override + public void pause() { + owningMediaPlayer.lock.lock(); + try { + mp.pause(); + } + finally { + owningMediaPlayer.lock.unlock(); + } + } + + @Override + public void prepare() throws IllegalStateException, IOException { + owningMediaPlayer.lock.lock(); + Log.d(AMP_TAG, "prepare()"); + try { + mp.prepare(); + Log.d(AMP_TAG, "Finish prepare()"); + } + finally { + owningMediaPlayer.lock.unlock(); + } + } + + @Override + public void prepareAsync() { + mp.prepareAsync(); + } + + @Override + public void release() { + owningMediaPlayer.lock.lock(); + try { + if (mp != null) { + Log.d(AMP_TAG, "mp.release()"); + mp.release(); + } + } + finally { + owningMediaPlayer.lock.unlock(); + } + } + + @Override + public void reset() { + owningMediaPlayer.lock.lock(); + try { + mp.reset(); + } + finally { + owningMediaPlayer.lock.unlock(); + } + } + + @Override + public void seekTo(int msec) throws IllegalStateException { + owningMediaPlayer.lock.lock(); + try { + mp.setOnSeekCompleteListener(this.onSeekCompleteListener); + mp.seekTo(msec); + } + finally { + owningMediaPlayer.lock.unlock(); + } + } + + @Override + public void setAudioStreamType(int streamtype) { + owningMediaPlayer.lock.lock(); + try { + mp.setAudioStreamType(streamtype); + } + finally { + owningMediaPlayer.lock.unlock(); + } + } + + @Override + public void setDataSource(Context context, Uri uri) + throws IllegalArgumentException, IllegalStateException, IOException { + owningMediaPlayer.lock.lock(); + try { + Log.d(AMP_TAG, "setDataSource(context, " + uri.toString() + ")"); + mp.setDataSource(context, uri); + } + finally { + owningMediaPlayer.lock.unlock(); + } + } + + @Override + public void setDataSource(String path) throws IllegalArgumentException, + IllegalStateException, IOException { + owningMediaPlayer.lock.lock(); + try { + Log.d(AMP_TAG, "setDataSource(" + path + ")"); + mp.setDataSource(path); + } + finally { + owningMediaPlayer.lock.unlock(); + } + } + + @Override + public void setEnableSpeedAdjustment(boolean enableSpeedAdjustment) { + // Can't! + } + + @Override + public void setLooping(boolean loop) { + owningMediaPlayer.lock.lock(); + try { + mp.setLooping(loop); + } + finally { + owningMediaPlayer.lock.unlock(); + } + } + + @Override + public void setPitchStepsAdjustment(float pitchSteps) { + // Can't! + } + + @Override + public void setPlaybackPitch(float f) { + // Can't! + } + + @Override + public void setPlaybackSpeed(float f) { + // Can't! + Log.d(AMP_TAG, "setPlaybackSpeed(" + f + ")"); + } + + @Override + public void setSpeedAdjustmentAlgorithm(int algorithm) { + // Can't! + Log.d(AMP_TAG, "setSpeedAdjustmentAlgorithm(" + algorithm + ")"); + } + + @Override + public void setVolume(float leftVolume, float rightVolume) { + owningMediaPlayer.lock.lock(); + try { + mp.setVolume(leftVolume, rightVolume); + } + finally { + owningMediaPlayer.lock.unlock(); + } + } + + @Override + public void setWakeMode(Context context, int mode) { + owningMediaPlayer.lock.lock(); + try { + if (mode != 0) { + mp.setWakeMode(context, mode); + } + } + finally { + owningMediaPlayer.lock.unlock(); + } + } + + @Override + public void start() { + owningMediaPlayer.lock.lock(); + try { + mp.start(); + } + finally { + owningMediaPlayer.lock.unlock(); + } + } + + @Override + public void stop() { + owningMediaPlayer.lock.lock(); + try { + mp.stop(); + } + finally { + owningMediaPlayer.lock.unlock(); + } + } +} diff --git a/core/src/main/java/com/aocate/media/MediaPlayer.java b/core/src/main/java/com/aocate/media/MediaPlayer.java new file mode 100644 index 000000000..c73c5219e --- /dev/null +++ b/core/src/main/java/com/aocate/media/MediaPlayer.java @@ -0,0 +1,1278 @@ +// Copyright 2011, Aocate, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.aocate.media; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.media.AudioManager; +import android.net.Uri; +import android.os.Handler; +import android.os.Handler.Callback; +import android.os.IBinder; +import android.os.Message; +import android.util.Log; + +import java.io.IOException; +import java.util.List; +import java.util.concurrent.locks.ReentrantLock; + +import de.danoeh.antennapod.core.BuildConfig; + +public class MediaPlayer { + public interface OnBufferingUpdateListener { + public abstract void onBufferingUpdate(MediaPlayer arg0, int percent); + } + + public interface OnCompletionListener { + public abstract void onCompletion(MediaPlayer arg0); + } + + public interface OnErrorListener { + public abstract boolean onError(MediaPlayer arg0, int what, int extra); + } + + public interface OnInfoListener { + public abstract boolean onInfo(MediaPlayer arg0, int what, int extra); + } + + public interface OnPitchAdjustmentAvailableChangedListener { + /** + * @param arg0 The owning media player + * @param pitchAdjustmentAvailable True if pitch adjustment is available, false if not + */ + public abstract void onPitchAdjustmentAvailableChanged( + MediaPlayer arg0, boolean pitchAdjustmentAvailable); + } + + public interface OnPreparedListener { + public abstract void onPrepared(MediaPlayer arg0); + } + + public interface OnSeekCompleteListener { + public abstract void onSeekComplete(MediaPlayer arg0); + } + + public interface OnSpeedAdjustmentAvailableChangedListener { + /** + * @param arg0 The owning media player + * @param speedAdjustmentAvailable True if speed adjustment is available, false if not + */ + public abstract void onSpeedAdjustmentAvailableChanged( + MediaPlayer arg0, boolean speedAdjustmentAvailable); + } + + public enum State { + IDLE, INITIALIZED, PREPARED, STARTED, PAUSED, STOPPED, PREPARING, PLAYBACK_COMPLETED, END, ERROR + } + + private static Uri SPEED_ADJUSTMENT_MARKET_URI = Uri + .parse("market://details?id=com.aocate.presto"); + + private static Intent prestoMarketIntent = null; + + public static final int MEDIA_ERROR_SERVER_DIED = android.media.MediaPlayer.MEDIA_ERROR_SERVER_DIED; + public static final int MEDIA_ERROR_UNKNOWN = android.media.MediaPlayer.MEDIA_ERROR_UNKNOWN; + public static final int MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK = android.media.MediaPlayer.MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK; + + /** + * Indicates whether the specified action can be used as an intent. This + * method queries the package manager for installed packages that can + * respond to an intent with the specified action. If no suitable package is + * found, this method returns false. + * + * @param context The application's environment. + * @param action The Intent action to check for availability. + * @return True if an Intent with the specified action can be sent and + * responded to, false otherwise. + */ + public static boolean isIntentAvailable(Context context, String action) { + final PackageManager packageManager = context.getPackageManager(); + final Intent intent = new Intent(action); + List list = packageManager.queryIntentServices(intent, + PackageManager.MATCH_DEFAULT_ONLY); + return list.size() > 0; + } + + /** + * Indicates whether the Presto library is installed + * + * @param context The context to use to query the package manager. + * @return True if the Presto library is installed, false if not. + */ + public static boolean isPrestoLibraryInstalled(Context context) { + return isIntentAvailable(context, ServiceBackedMediaPlayer.INTENT_NAME); + } + + /** + * Return an Intent that opens the Android Market page for the speed + * alteration library + * + * @return The Intent for the Presto library on the Android Market + */ + public static Intent getPrestoMarketIntent() { + if (prestoMarketIntent == null) { + prestoMarketIntent = new Intent(Intent.ACTION_VIEW, + SPEED_ADJUSTMENT_MARKET_URI); + } + return prestoMarketIntent; + } + + /** + * Open the Android Market page for the Presto library + * + * @param context The context from which to open the Android Market page + */ + public static void openPrestoMarketIntent(Context context) { + context.startActivity(getPrestoMarketIntent()); + } + + private static final String MP_TAG = "AocateReplacementMediaPlayer"; + + private static final double PITCH_STEP_CONSTANT = 1.0594630943593; + + private AndroidMediaPlayer amp = null; + // This is whether speed adjustment should be enabled (by the Service) + // To avoid the Service entirely, set useService to false + protected boolean enableSpeedAdjustment = true; + private int lastKnownPosition = 0; + // In some cases, we're going to have to replace the + // android.media.MediaPlayer on the fly, and we don't want to touch the + // wrong media player, so lock it way too much. + ReentrantLock lock = new ReentrantLock(); + private int mAudioStreamType = AudioManager.STREAM_MUSIC; + private Context mContext; + private boolean mIsLooping = false; + private float mLeftVolume = 1f; + private float mPitchStepsAdjustment = 0f; + private float mRightVolume = 1f; + private float mSpeedMultiplier = 1f; + private int mWakeMode = 0; + MediaPlayerImpl mpi = null; + protected boolean pitchAdjustmentAvailable = false; + private ServiceBackedMediaPlayer sbmp = null; + protected boolean speedAdjustmentAvailable = false; + + private Handler mServiceDisconnectedHandler = null; + + // Some parts of state cannot be found by calling MediaPlayerImpl functions, + // so store our own state. This also helps copy state when changing + // implementations + State state = State.INITIALIZED; + String stringDataSource = null; + Uri uriDataSource = null; + private boolean useService = false; + + // Naming Convention for Listeners + // Most listeners can both be set by clients and called by MediaPlayImpls + // There are a few that have to do things in this class as well as calling + // the function. In all cases, onX is what is called by MediaPlayerImpl + // If there is work to be done in this class, then the listener that is + // set by setX is X (with the first letter lowercase). + OnBufferingUpdateListener onBufferingUpdateListener = null; + OnCompletionListener onCompletionListener = null; + OnErrorListener onErrorListener = null; + OnInfoListener onInfoListener = null; + + // Special case. Pitch adjustment ceases to be available when we switch + // to the android.media.MediaPlayer (though it is not guaranteed to be + // available when using the ServiceBackedMediaPlayer) + OnPitchAdjustmentAvailableChangedListener onPitchAdjustmentAvailableChangedListener = new OnPitchAdjustmentAvailableChangedListener() { + public void onPitchAdjustmentAvailableChanged(MediaPlayer arg0, + boolean pitchAdjustmentAvailable) { + lock.lock(); + try { + Log + .d( + MP_TAG, + "onPitchAdjustmentAvailableChangedListener.onPitchAdjustmentAvailableChanged being called"); + if (MediaPlayer.this.pitchAdjustmentAvailable != pitchAdjustmentAvailable) { + Log.d(MP_TAG, "Pitch adjustment state has changed from " + + MediaPlayer.this.pitchAdjustmentAvailable + + " to " + pitchAdjustmentAvailable); + MediaPlayer.this.pitchAdjustmentAvailable = pitchAdjustmentAvailable; + if (MediaPlayer.this.pitchAdjustmentAvailableChangedListener != null) { + MediaPlayer.this.pitchAdjustmentAvailableChangedListener + .onPitchAdjustmentAvailableChanged(arg0, + pitchAdjustmentAvailable); + } + } + } finally { + lock.unlock(); + } + } + }; + OnPitchAdjustmentAvailableChangedListener pitchAdjustmentAvailableChangedListener = null; + + MediaPlayer.OnPreparedListener onPreparedListener = new MediaPlayer.OnPreparedListener() { + public void onPrepared(MediaPlayer arg0) { + Log.d(MP_TAG, "onPreparedListener 242 setting state to PREPARED"); + MediaPlayer.this.state = State.PREPARED; + if (MediaPlayer.this.preparedListener != null) { + Log.d(MP_TAG, "Calling preparedListener"); + MediaPlayer.this.preparedListener.onPrepared(arg0); + } + Log.d(MP_TAG, "Wrap up onPreparedListener"); + } + }; + + OnPreparedListener preparedListener = null; + OnSeekCompleteListener onSeekCompleteListener = null; + + // Special case. Speed adjustment ceases to be available when we switch + // to the android.media.MediaPlayer (though it is not guaranteed to be + // available when using the ServiceBackedMediaPlayer) + OnSpeedAdjustmentAvailableChangedListener onSpeedAdjustmentAvailableChangedListener = new OnSpeedAdjustmentAvailableChangedListener() { + public void onSpeedAdjustmentAvailableChanged(MediaPlayer arg0, + boolean speedAdjustmentAvailable) { + lock.lock(); + try { + Log + .d( + MP_TAG, + "onSpeedAdjustmentAvailableChangedListener.onSpeedAdjustmentAvailableChanged being called"); + if (MediaPlayer.this.speedAdjustmentAvailable != speedAdjustmentAvailable) { + Log.d(MP_TAG, "Speed adjustment state has changed from " + + MediaPlayer.this.speedAdjustmentAvailable + + " to " + speedAdjustmentAvailable); + MediaPlayer.this.speedAdjustmentAvailable = speedAdjustmentAvailable; + if (MediaPlayer.this.speedAdjustmentAvailableChangedListener != null) { + MediaPlayer.this.speedAdjustmentAvailableChangedListener + .onSpeedAdjustmentAvailableChanged(arg0, + speedAdjustmentAvailable); + } + } + } finally { + lock.unlock(); + } + } + }; + OnSpeedAdjustmentAvailableChangedListener speedAdjustmentAvailableChangedListener = null; + + private int speedAdjustmentAlgorithm = SpeedAdjustmentAlgorithm.SONIC; + + public MediaPlayer(final Context context) { + this(context, true); + } + + public MediaPlayer(final Context context, boolean useService) { + this.mContext = context; + this.useService = useService; + + // So here's the major problem + // Sometimes the service won't exist or won't be connected, + // so start with an android.media.MediaPlayer, and when + // the service is connected, use that from then on + this.mpi = this.amp = new AndroidMediaPlayer(this, context); + + // setupMpi will go get the Service, if it can, then bring that + // implementation into sync + Log.d(MP_TAG, "setupMpi"); + setupMpi(context); + } + + private boolean invalidServiceConnectionConfiguration() { + if (!(this.mpi instanceof ServiceBackedMediaPlayer)) { + if (this.useService && isPrestoLibraryInstalled()) { + // In this case, the Presto library has been installed + // or something while playing sound + // We could be using the service, but we're not + Log.d(MP_TAG, "We could be using the service, but we're not 316"); + return true; + } + // If useService is false, then we shouldn't be using the SBMP + // If the Presto library isn't installed, ditto + Log.d(MP_TAG, "this.mpi is not a ServiceBackedMediaPlayer, but we couldn't use it anyway 321"); + return false; + } else { + if (BuildConfig.DEBUG && !(this.mpi instanceof ServiceBackedMediaPlayer)) + throw new AssertionError(); + if (this.useService && isPrestoLibraryInstalled()) { + // We should be using the service, and we are. Great! + Log.d(MP_TAG, "We could be using a ServiceBackedMediaPlayer and we are 327"); + return false; + } + // We're trying to use the service when we shouldn't, + // that's an invalid configuration + Log.d(MP_TAG, "We're trying to use a ServiceBackedMediaPlayer but we shouldn't be 332"); + return true; + } + } + + private void setupMpi(final Context context) { + lock.lock(); + try { + Log.d(MP_TAG, "setupMpi 336"); + // Check if the client wants to use the service at all, + // then if we're already using the right kind of media player + if (this.useService && isPrestoLibraryInstalled()) { + if ((this.mpi != null) + && (this.mpi instanceof ServiceBackedMediaPlayer)) { + Log.d(MP_TAG, "Already using ServiceBackedMediaPlayer"); + return; + } + if (this.sbmp == null) { + Log.d(MP_TAG, "Instantiating new ServiceBackedMediaPlayer 346"); + this.sbmp = new ServiceBackedMediaPlayer(this, context, + new ServiceConnection() { + public void onServiceConnected( + ComponentName className, + final IBinder service) { + Thread t = new Thread(new Runnable() { + public void run() { + // This lock probably isn't granular + // enough + MediaPlayer.this.lock.lock(); + Log.d(MP_TAG, + "onServiceConnected 257"); + try { + MediaPlayer.this + .switchMediaPlayerImpl( + MediaPlayer.this.amp, + MediaPlayer.this.sbmp); + Log.d(MP_TAG, "End onServiceConnected 362"); + } finally { + MediaPlayer.this.lock.unlock(); + } + } + }); + t.start(); + } + + public void onServiceDisconnected( + ComponentName className) { + MediaPlayer.this.lock.lock(); + try { + // Can't get any more useful information + // out of sbmp + if (MediaPlayer.this.sbmp != null) { + MediaPlayer.this.sbmp.release(); + } + // Unlike most other cases, sbmp gets set + // to null since there's nothing useful + // backing it now + MediaPlayer.this.sbmp = null; + + if (mServiceDisconnectedHandler == null) { + mServiceDisconnectedHandler = new Handler(new Callback() { + public boolean handleMessage(Message msg) { + // switchMediaPlayerImpl won't try to + // clone anything from null + lock.lock(); + try { + if (MediaPlayer.this.amp == null) { + // This should never be in this state + MediaPlayer.this.amp = new AndroidMediaPlayer( + MediaPlayer.this, + MediaPlayer.this.mContext); + } + // Use sbmp instead of null in case by some miracle it's + // been restored in the meantime + MediaPlayer.this.switchMediaPlayerImpl( + MediaPlayer.this.sbmp, + MediaPlayer.this.amp); + return true; + } finally { + lock.unlock(); + } + } + }); + } + + // This code needs to execute on the + // original thread to instantiate + // the new object in the right place + mServiceDisconnectedHandler + .sendMessage( + mServiceDisconnectedHandler + .obtainMessage()); + // Note that we do NOT want to set + // useService. useService is about + // what the user wants, not what they + // get + } finally { + MediaPlayer.this.lock.unlock(); + } + } + } + ); + } + switchMediaPlayerImpl(this.amp, this.sbmp); + } else { + if ((this.mpi != null) + && (this.mpi instanceof AndroidMediaPlayer)) { + Log.d(MP_TAG, "Already using AndroidMediaPlayer"); + return; + } + if (this.amp == null) { + Log.d(MP_TAG, "Instantiating new AndroidMediaPlayer (this should be impossible)"); + this.amp = new AndroidMediaPlayer(this, context); + } + switchMediaPlayerImpl(this.sbmp, this.amp); + } + } finally { + lock.unlock(); + } + } + + private void switchMediaPlayerImpl(MediaPlayerImpl from, MediaPlayerImpl to) { + lock.lock(); + try { + Log.d(MP_TAG, "switchMediaPlayerImpl"); + if ((from == to) + // Same object, nothing to synchronize + || (to == null) + // Nothing to copy to (maybe this should throw an error?) + || ((to instanceof ServiceBackedMediaPlayer) && !((ServiceBackedMediaPlayer) to).isConnected()) + // ServiceBackedMediaPlayer hasn't yet connected, onServiceConnected will take care of the transition + || (MediaPlayer.this.state == State.END)) { + // State.END is after a release(), no further functions should + // be called on this class and from is likely to have problems + // retrieving state that won't be used anyway + return; + } + // Extract all that we can from the existing implementation + // and copy it to the new implementation + + Log.d(MP_TAG, "switchMediaPlayerImpl(), current state is " + + this.state.toString()); + + to.reset(); + + // Do this first so we don't have to prepare the same + // data file twice + to.setEnableSpeedAdjustment(MediaPlayer.this.enableSpeedAdjustment); + + // This is a reasonable place to set all of these, + // none of them require prepare() or the like first + to.setAudioStreamType(this.mAudioStreamType); + to.setSpeedAdjustmentAlgorithm(this.speedAdjustmentAlgorithm); + to.setLooping(this.mIsLooping); + to.setPitchStepsAdjustment(this.mPitchStepsAdjustment); + Log.d(MP_TAG, "Setting playback speed to " + this.mSpeedMultiplier); + to.setPlaybackSpeed(this.mSpeedMultiplier); + to.setVolume(MediaPlayer.this.mLeftVolume, + MediaPlayer.this.mRightVolume); + to.setWakeMode(this.mContext, this.mWakeMode); + + Log.d(MP_TAG, "asserting at least one data source is null"); + assert ((MediaPlayer.this.stringDataSource == null) || (MediaPlayer.this.uriDataSource == null)); + + if (uriDataSource != null) { + Log.d(MP_TAG, "switchMediaPlayerImpl(): uriDataSource != null"); + try { + to.setDataSource(this.mContext, uriDataSource); + } catch (IllegalArgumentException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (IllegalStateException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + if (stringDataSource != null) { + Log.d(MP_TAG, + "switchMediaPlayerImpl(): stringDataSource != null"); + try { + to.setDataSource(stringDataSource); + } catch (IllegalArgumentException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (IllegalStateException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + if ((this.state == State.PREPARED) + || (this.state == State.PREPARING) + || (this.state == State.PAUSED) + || (this.state == State.STOPPED) + || (this.state == State.STARTED) + || (this.state == State.PLAYBACK_COMPLETED)) { + Log.d(MP_TAG, "switchMediaPlayerImpl(): prepare and seek"); + // Use prepare here instead of prepareAsync so that + // we wait for it to be ready before we try to use it + try { + to.muteNextOnPrepare(); + to.prepare(); + } catch (IllegalStateException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + int seekPos = 0; + if (from != null) { + seekPos = from.getCurrentPosition(); + } else if (this.lastKnownPosition < to.getDuration()) { + // This can happen if the Service unexpectedly + // disconnected. Because it would result in too much + // information being passed around, we don't constantly + // poll for the lastKnownPosition, but we'll save it + // when getCurrentPosition is called + seekPos = this.lastKnownPosition; + } + to.muteNextSeek(); + to.seekTo(seekPos); + } + if ((from != null) + && from.isPlaying()) { + from.pause(); + } + if ((this.state == State.STARTED) + || (this.state == State.PAUSED) + || (this.state == State.STOPPED)) { + Log.d(MP_TAG, "switchMediaPlayerImpl(): start"); + if (to != null) { + to.start(); + } + } + + if (this.state == State.PAUSED) { + Log.d(MP_TAG, "switchMediaPlayerImpl(): paused"); + if (to != null) { + to.pause(); + } + } else if (this.state == State.STOPPED) { + Log.d(MP_TAG, "switchMediaPlayerImpl(): stopped"); + if (to != null) { + to.stop(); + } + } + + this.mpi = to; + + // Cheating here by relying on the side effect in + // on(Pitch|Speed)AdjustmentAvailableChanged + if ((to.canSetPitch() != this.pitchAdjustmentAvailable) + && (this.onPitchAdjustmentAvailableChangedListener != null)) { + this.onPitchAdjustmentAvailableChangedListener + .onPitchAdjustmentAvailableChanged(this, to + .canSetPitch()); + } + if ((to.canSetSpeed() != this.speedAdjustmentAvailable) + && (this.onSpeedAdjustmentAvailableChangedListener != null)) { + this.onSpeedAdjustmentAvailableChangedListener + .onSpeedAdjustmentAvailableChanged(this, to + .canSetSpeed()); + } + Log.d(MP_TAG, "switchMediaPlayerImpl() 625 " + this.state.toString()); + } finally { + lock.unlock(); + } + } + + /** + * Returns true if pitch can be changed at this moment + * + * @return True if pitch can be changed + */ + public boolean canSetPitch() { + lock.lock(); + try { + return this.mpi.canSetPitch(); + } finally { + lock.unlock(); + } + } + + /** + * Returns true if speed can be changed at this moment + * + * @return True if speed can be changed + */ + public boolean canSetSpeed() { + lock.lock(); + try { + return this.mpi.canSetSpeed(); + } finally { + lock.unlock(); + } + } + + protected void finalize() throws Throwable { + lock.lock(); + try { + Log.d(MP_TAG, "finalize() 626"); + this.release(); + } finally { + lock.unlock(); + } + } + + /** + * Returns the number of steps (in a musical scale) by which playback is + * currently shifted. When greater than zero, pitch is shifted up. When less + * than zero, pitch is shifted down. + * + * @return The number of steps pitch is currently shifted by + */ + public float getCurrentPitchStepsAdjustment() { + lock.lock(); + try { + return this.mpi.getCurrentPitchStepsAdjustment(); + } finally { + lock.unlock(); + } + } + + /** + * Functions identically to android.media.MediaPlayer.getCurrentPosition() + * Accurate only to frame size of encoded data (26 ms for MP3s) + * + * @return Current position (in milliseconds) + */ + public int getCurrentPosition() { + lock.lock(); + try { + return (this.lastKnownPosition = this.mpi.getCurrentPosition()); + } finally { + lock.unlock(); + } + } + + /** + * Returns the current speed multiplier. Defaults to 1.0 (normal speed) + * + * @return The current speed multiplier + */ + public float getCurrentSpeedMultiplier() { + lock.lock(); + try { + return this.mpi.getCurrentSpeedMultiplier(); + } finally { + lock.unlock(); + } + } + + /** + * Functions identically to android.media.MediaPlayer.getDuration() + * + * @return Length of the track (in milliseconds) + */ + public int getDuration() { + lock.lock(); + try { + return this.mpi.getDuration(); + } finally { + lock.unlock(); + } + } + + /** + * Get the maximum value that can be passed to setPlaybackSpeed + * + * @return The maximum speed multiplier + */ + public float getMaxSpeedMultiplier() { + lock.lock(); + try { + return this.mpi.getMaxSpeedMultiplier(); + } finally { + lock.unlock(); + } + } + + /** + * Get the minimum value that can be passed to setPlaybackSpeed + * + * @return The minimum speed multiplier + */ + public float getMinSpeedMultiplier() { + lock.lock(); + try { + return this.mpi.getMinSpeedMultiplier(); + } finally { + lock.unlock(); + } + } + + /** + * Gets the version code of the backing service + * + * @return -1 if ServiceBackedMediaPlayer is not used, 0 if the service is not + * connected, otherwise the version code retrieved from the service + */ + public int getServiceVersionCode() { + lock.lock(); + try { + if (this.mpi instanceof ServiceBackedMediaPlayer) { + return ((ServiceBackedMediaPlayer) this.mpi).getServiceVersionCode(); + } else { + return -1; + } + } finally { + lock.unlock(); + } + } + + /** + * Gets the version name of the backing service + * + * @return null if ServiceBackedMediaPlayer is not used, empty string if + * the service is not connected, otherwise the version name retrieved from + * the service + */ + public String getServiceVersionName() { + lock.lock(); + try { + if (this.mpi instanceof ServiceBackedMediaPlayer) { + return ((ServiceBackedMediaPlayer) this.mpi).getServiceVersionName(); + } else { + return null; + } + } finally { + lock.unlock(); + } + } + + /** + * Functions identically to android.media.MediaPlayer.isLooping() + * + * @return True if the track is looping + */ + public boolean isLooping() { + lock.lock(); + try { + return this.mpi.isLooping(); + } finally { + lock.unlock(); + } + } + + /** + * Functions identically to android.media.MediaPlayer.isPlaying() + * + * @return True if the track is playing + */ + public boolean isPlaying() { + lock.lock(); + try { + return this.mpi.isPlaying(); + } finally { + lock.unlock(); + } + } + + /** + * Returns true if this MediaPlayer has access to the Presto + * library + * + * @return True if the Presto library is installed + */ + public boolean isPrestoLibraryInstalled() { + if ((this.mpi == null) || (this.mpi.mContext == null)) { + return false; + } + return isPrestoLibraryInstalled(this.mpi.mContext); + } + + /** + * Open the Android Market page in the same context as this MediaPlayer + */ + public void openPrestoMarketIntent() { + if ((this.mpi != null) && (this.mpi.mContext != null)) { + openPrestoMarketIntent(this.mpi.mContext); + } + } + + /** + * Functions identically to android.media.MediaPlayer.pause() Pauses the + * track + */ + public void pause() { + lock.lock(); + try { + if (invalidServiceConnectionConfiguration()) { + setupMpi(this.mpi.mContext); + } + this.state = State.PAUSED; + this.mpi.pause(); + } finally { + lock.unlock(); + } + } + + /** + * Functions identically to android.media.MediaPlayer.prepare() Prepares the + * track. This or prepareAsync must be called before start() + */ + public void prepare() throws IllegalStateException, IOException { + lock.lock(); + try { + Log.d(MP_TAG, "prepare() 746 using " + ((this.mpi == null) ? "null (this shouldn't happen)" : this.mpi.getClass().toString()) + " state " + this.state.toString()); + Log.d(MP_TAG, "onPreparedListener is: " + ((this.onPreparedListener == null) ? "null" : "non-null")); + Log.d(MP_TAG, "preparedListener is: " + ((this.preparedListener == null) ? "null" : "non-null")); + if (invalidServiceConnectionConfiguration()) { + setupMpi(this.mpi.mContext); + } + this.mpi.prepare(); + this.state = State.PREPARED; + Log.d(MP_TAG, "prepare() finished 778"); + } finally { + lock.unlock(); + } + } + + /** + * Functions identically to android.media.MediaPlayer.prepareAsync() + * Prepares the track. This or prepare must be called before start() + */ + public void prepareAsync() { + lock.lock(); + try { + Log.d(MP_TAG, "prepareAsync() 779"); + if (invalidServiceConnectionConfiguration()) { + setupMpi(this.mpi.mContext); + } + this.state = State.PREPARING; + this.mpi.prepareAsync(); + } finally { + lock.unlock(); + } + } + + /** + * Functions identically to android.media.MediaPlayer.release() Releases the + * underlying resources used by the media player. + */ + public void release() { + lock.lock(); + try { + Log.d(MP_TAG, "Releasing MediaPlayer 791"); + + this.state = State.END; + if (this.amp != null) { + this.amp.release(); + } + if (this.sbmp != null) { + this.sbmp.release(); + } + + this.onBufferingUpdateListener = null; + this.onCompletionListener = null; + this.onErrorListener = null; + this.onInfoListener = null; + this.preparedListener = null; + this.onPitchAdjustmentAvailableChangedListener = null; + this.pitchAdjustmentAvailableChangedListener = null; + Log.d(MP_TAG, "Setting onSeekCompleteListener to null 871"); + this.onSeekCompleteListener = null; + this.onSpeedAdjustmentAvailableChangedListener = null; + this.speedAdjustmentAvailableChangedListener = null; + } finally { + lock.unlock(); + } + } + + /** + * Functions identically to android.media.MediaPlayer.reset() Resets the + * track to idle state + */ + public void reset() { + lock.lock(); + try { + this.state = State.IDLE; + this.stringDataSource = null; + this.uriDataSource = null; + this.mpi.reset(); + } finally { + lock.unlock(); + } + } + + /** + * Functions identically to android.media.MediaPlayer.seekTo(int msec) Seeks + * to msec in the track + */ + public void seekTo(int msec) throws IllegalStateException { + lock.lock(); + try { + this.mpi.seekTo(msec); + } finally { + lock.unlock(); + } + } + + /** + * Functions identically to android.media.MediaPlayer.setAudioStreamType(int + * streamtype) Sets the audio stream type. + */ + public void setAudioStreamType(int streamtype) { + lock.lock(); + try { + this.mAudioStreamType = streamtype; + this.mpi.setAudioStreamType(streamtype); + } finally { + lock.unlock(); + } + } + + /** + * Functions identically to android.media.MediaPlayer.setDataSource(Context + * context, Uri uri) Sets uri as data source in the context given + */ + public void setDataSource(Context context, Uri uri) + throws IllegalArgumentException, IllegalStateException, IOException { + lock.lock(); + try { + Log.d(MP_TAG, "In setDataSource(context, " + uri.toString() + "), using " + this.mpi.getClass().toString()); + if (invalidServiceConnectionConfiguration()) { + setupMpi(this.mpi.mContext); + } + this.state = State.INITIALIZED; + this.stringDataSource = null; + this.uriDataSource = uri; + this.mpi.setDataSource(context, uri); + } finally { + lock.unlock(); + } + } + + /** + * Functions identically to android.media.MediaPlayer.setDataSource(String + * path) Sets the data source of the track to a file given. + */ + public void setDataSource(String path) throws IllegalArgumentException, + IllegalStateException, IOException { + lock.lock(); + try { + Log.d(MP_TAG, "In setDataSource(context, " + path + ")"); + if (invalidServiceConnectionConfiguration()) { + setupMpi(this.mpi.mContext); + } + this.state = State.INITIALIZED; + this.stringDataSource = path; + this.uriDataSource = null; + this.mpi.setDataSource(path); + } finally { + lock.unlock(); + } + } + + /** + * Sets whether to use speed adjustment or not. Speed adjustment on is more + * computation-intensive than with it off. + * + * @param enableSpeedAdjustment Whether speed adjustment should be supported. + */ + public void setEnableSpeedAdjustment(boolean enableSpeedAdjustment) { + lock.lock(); + try { + this.enableSpeedAdjustment = enableSpeedAdjustment; + this.mpi.setEnableSpeedAdjustment(enableSpeedAdjustment); + } finally { + lock.unlock(); + } + } + + /** + * Functions identically to android.media.MediaPlayer.setLooping(boolean + * loop) Sets the track to loop infinitely if loop is true, play once if + * loop is false + */ + public void setLooping(boolean loop) { + lock.lock(); + try { + this.mIsLooping = loop; + this.mpi.setLooping(loop); + } finally { + lock.unlock(); + } + } + + /** + * Sets the number of steps (in a musical scale) by which playback is + * currently shifted. When greater than zero, pitch is shifted up. When less + * than zero, pitch is shifted down. + * + * @param pitchSteps The number of steps by which to shift playback + */ + public void setPitchStepsAdjustment(float pitchSteps) { + lock.lock(); + try { + this.mPitchStepsAdjustment = pitchSteps; + this.mpi.setPitchStepsAdjustment(pitchSteps); + } finally { + lock.unlock(); + } + } + + /** + * Set the algorithm to use for changing the speed and pitch of audio + * See SpeedAdjustmentAlgorithm constants for more details + * + * @param algorithm The algorithm to use. + */ + public void setSpeedAdjustmentAlgorithm(int algorithm) { + lock.lock(); + try { + this.speedAdjustmentAlgorithm = algorithm; + if (this.mpi != null) { + this.mpi.setSpeedAdjustmentAlgorithm(algorithm); + } + } finally { + lock.unlock(); + } + } + + private static float getPitchStepsAdjustment(float pitch) { + return (float) (Math.log(pitch) / (2 * Math.log(PITCH_STEP_CONSTANT))); + } + + /** + * Sets the percentage by which pitch is currently shifted. When greater + * than zero, pitch is shifted up. When less than zero, pitch is shifted + * down + * + * @param f The percentage to shift pitch + */ + public void setPlaybackPitch(float pitch) { + lock.lock(); + try { + this.mPitchStepsAdjustment = getPitchStepsAdjustment(pitch); + this.mpi.setPlaybackPitch(pitch); + } finally { + lock.unlock(); + } + } + + /** + * Set playback speed. 1.0 is normal speed, 2.0 is double speed, and so on. + * Speed should never be set to 0 or below. + * + * @param f The speed multiplier to use for further playback + */ + public void setPlaybackSpeed(float f) { + lock.lock(); + try { + this.mSpeedMultiplier = f; + this.mpi.setPlaybackSpeed(f); + } finally { + lock.unlock(); + } + } + + /** + * Sets whether to use speed adjustment or not. Speed adjustment on is more + * computation-intensive than with it off. + * + * @param enableSpeedAdjustment Whether speed adjustment should be supported. + */ + public void setUseService(boolean useService) { + lock.lock(); + try { + this.useService = useService; + setupMpi(this.mpi.mContext); + } finally { + lock.unlock(); + } + } + + /** + * Functions identically to android.media.MediaPlayer.setVolume(float + * leftVolume, float rightVolume) Sets the stereo volume + */ + public void setVolume(float leftVolume, float rightVolume) { + lock.lock(); + try { + this.mLeftVolume = leftVolume; + this.mRightVolume = rightVolume; + this.mpi.setVolume(leftVolume, rightVolume); + } finally { + lock.unlock(); + } + } + + /** + * Functions identically to android.media.MediaPlayer.setWakeMode(Context + * context, int mode) Acquires a wake lock in the context given. You must + * request the appropriate permissions in your AndroidManifest.xml file. + */ + public void setWakeMode(Context context, int mode) { + lock.lock(); + try { + this.mWakeMode = mode; + this.mpi.setWakeMode(context, mode); + } finally { + lock.unlock(); + } + } + + /** + * Functions identically to + * android.media.MediaPlayer.setOnCompletionListener(OnCompletionListener + * listener) Sets a listener to be used when a track completes playing. + */ + public void setOnBufferingUpdateListener(OnBufferingUpdateListener listener) { + lock.lock(); + try { + this.onBufferingUpdateListener = listener; + } finally { + lock.unlock(); + } + } + + /** + * Functions identically to + * android.media.MediaPlayer.setOnCompletionListener(OnCompletionListener + * listener) Sets a listener to be used when a track completes playing. + */ + public void setOnCompletionListener(OnCompletionListener listener) { + lock.lock(); + try { + this.onCompletionListener = listener; + } finally { + lock.unlock(); + } + } + + /** + * Functions identically to + * android.media.MediaPlayer.setOnErrorListener(OnErrorListener listener) + * Sets a listener to be used when a track encounters an error. + */ + public void setOnErrorListener(OnErrorListener listener) { + lock.lock(); + try { + this.onErrorListener = listener; + } finally { + lock.unlock(); + } + } + + /** + * Functions identically to + * android.media.MediaPlayer.setOnInfoListener(OnInfoListener listener) Sets + * a listener to be used when a track has info. + */ + public void setOnInfoListener(OnInfoListener listener) { + lock.lock(); + try { + this.onInfoListener = listener; + } finally { + lock.unlock(); + } + } + + /** + * Sets a listener that will fire when pitch adjustment becomes available or + * stops being available + */ + public void setOnPitchAdjustmentAvailableChangedListener( + OnPitchAdjustmentAvailableChangedListener listener) { + lock.lock(); + try { + this.pitchAdjustmentAvailableChangedListener = listener; + } finally { + lock.unlock(); + } + } + + /** + * Functions identically to + * android.media.MediaPlayer.setOnPreparedListener(OnPreparedListener + * listener) Sets a listener to be used when a track finishes preparing. + */ + public void setOnPreparedListener(OnPreparedListener listener) { + lock.lock(); + Log.d(MP_TAG, " ++++++++++++++++++++++++++++++++++++++++++++ setOnPreparedListener"); + try { + this.preparedListener = listener; + // For this one, we do not explicitly set the MediaPlayer or the + // Service listener. This is because in addition to calling the + // listener provided by the client, it's necessary to change + // state to PREPARED. See prepareAsync for implementation details + } finally { + lock.unlock(); + } + } + + /** + * Functions identically to + * android.media.MediaPlayer.setOnSeekCompleteListener + * (OnSeekCompleteListener listener) Sets a listener to be used when a track + * finishes seeking. + */ + public void setOnSeekCompleteListener(OnSeekCompleteListener listener) { + lock.lock(); + try { + this.onSeekCompleteListener = listener; + } finally { + lock.unlock(); + } + } + + /** + * Sets a listener that will fire when speed adjustment becomes available or + * stops being available + */ + public void setOnSpeedAdjustmentAvailableChangedListener( + OnSpeedAdjustmentAvailableChangedListener listener) { + lock.lock(); + try { + this.speedAdjustmentAvailableChangedListener = listener; + } finally { + lock.unlock(); + } + } + + /** + * Functions identically to android.media.MediaPlayer.start() Starts a track + * playing + */ + public void start() { + lock.lock(); + try { + Log.d(MP_TAG, "start() 1149"); + if (invalidServiceConnectionConfiguration()) { + setupMpi(this.mpi.mContext); + } + this.state = State.STARTED; + Log.d(MP_TAG, "start() 1154"); + this.mpi.start(); + } finally { + lock.unlock(); + } + } + + /** + * Functions identically to android.media.MediaPlayer.stop() Stops a track + * playing and resets its position to the start. + */ + public void stop() { + lock.lock(); + try { + if (invalidServiceConnectionConfiguration()) { + setupMpi(this.mpi.mContext); + } + this.state = State.STOPPED; + this.mpi.stop(); + } finally { + lock.unlock(); + } + } +} \ No newline at end of file diff --git a/core/src/main/java/com/aocate/media/MediaPlayerImpl.java b/core/src/main/java/com/aocate/media/MediaPlayerImpl.java new file mode 100644 index 000000000..856ab47ce --- /dev/null +++ b/core/src/main/java/com/aocate/media/MediaPlayerImpl.java @@ -0,0 +1,118 @@ +// Copyright 2011, Aocate, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.aocate.media; + +import java.io.IOException; +import java.util.concurrent.locks.ReentrantLock; + +import android.content.Context; +import android.net.Uri; +import android.util.Log; + +public abstract class MediaPlayerImpl { + private static final String MPI_TAG = "AocateMediaPlayerImpl"; + protected final MediaPlayer owningMediaPlayer; + protected final Context mContext; + protected int muteOnPreparedCount = 0; + protected int muteOnSeekCount = 0; + + public MediaPlayerImpl(MediaPlayer owningMediaPlayer, Context context) { + this.owningMediaPlayer = owningMediaPlayer; + + this.mContext = context; + } + + public abstract boolean canSetPitch(); + + public abstract boolean canSetSpeed(); + + public abstract float getCurrentPitchStepsAdjustment(); + + public abstract int getCurrentPosition(); + + public abstract float getCurrentSpeedMultiplier(); + + public abstract int getDuration(); + + public abstract float getMaxSpeedMultiplier(); + + public abstract float getMinSpeedMultiplier(); + + public abstract boolean isLooping(); + + public abstract boolean isPlaying(); + + public abstract void pause(); + + public abstract void prepare() throws IllegalStateException, IOException; + + public abstract void prepareAsync(); + + public abstract void release(); + + public abstract void reset(); + + public abstract void seekTo(int msec) throws IllegalStateException; + + public abstract void setAudioStreamType(int streamtype); + + public abstract void setDataSource(Context context, Uri uri) throws IllegalArgumentException, IllegalStateException, IOException; + + public abstract void setDataSource(String path) throws IllegalArgumentException, IllegalStateException, IOException; + + public abstract void setEnableSpeedAdjustment(boolean enableSpeedAdjustment); + + public abstract void setLooping(boolean loop); + + public abstract void setPitchStepsAdjustment(float pitchSteps); + + public abstract void setPlaybackPitch(float f); + + public abstract void setPlaybackSpeed(float f); + + public abstract void setSpeedAdjustmentAlgorithm(int algorithm); + + public abstract void setVolume(float leftVolume, float rightVolume); + + public abstract void setWakeMode(Context context, int mode); + + public abstract void start(); + + public abstract void stop(); + + protected ReentrantLock lockMuteOnPreparedCount = new ReentrantLock(); + public void muteNextOnPrepare() { + lockMuteOnPreparedCount.lock(); + Log.d(MPI_TAG, "muteNextOnPrepare()"); + try { + this.muteOnPreparedCount++; + } + finally { + lockMuteOnPreparedCount.unlock(); + } + } + + protected ReentrantLock lockMuteOnSeekCount = new ReentrantLock(); + public void muteNextSeek() { + lockMuteOnSeekCount.lock(); + Log.d(MPI_TAG, "muteNextOnSeek()"); + try { + this.muteOnSeekCount++; + } + finally { + lockMuteOnSeekCount.unlock(); + } + } +} diff --git a/core/src/main/java/com/aocate/media/ServiceBackedMediaPlayer.java b/core/src/main/java/com/aocate/media/ServiceBackedMediaPlayer.java new file mode 100644 index 000000000..ef4572d33 --- /dev/null +++ b/core/src/main/java/com/aocate/media/ServiceBackedMediaPlayer.java @@ -0,0 +1,1170 @@ +// Copyright 2011, Aocate, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.aocate.media; + +import java.io.IOException; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.media.AudioManager; +import android.net.Uri; +import android.os.IBinder; +import android.os.PowerManager; +import android.os.RemoteException; +import android.os.PowerManager.WakeLock; +import android.util.Log; + +import com.aocate.media.MediaPlayer.State; +import com.aocate.presto.service.IDeathCallback_0_8; +import com.aocate.presto.service.IOnBufferingUpdateListenerCallback_0_8; +import com.aocate.presto.service.IOnCompletionListenerCallback_0_8; +import com.aocate.presto.service.IOnErrorListenerCallback_0_8; +import com.aocate.presto.service.IOnInfoListenerCallback_0_8; +import com.aocate.presto.service.IOnPitchAdjustmentAvailableChangedListenerCallback_0_8; +import com.aocate.presto.service.IOnPreparedListenerCallback_0_8; +import com.aocate.presto.service.IOnSeekCompleteListenerCallback_0_8; +import com.aocate.presto.service.IOnSpeedAdjustmentAvailableChangedListenerCallback_0_8; +import com.aocate.presto.service.IPlayMedia_0_8; + +/** + * Class for connecting to remote speed-altering, media playing Service + * Note that there is unusually high coupling between MediaPlayer and this + * class. This is an unfortunate compromise, since the alternative was to + * track state in two different places in this code (plus the internal state + * of the remote media player). + * @author aocate + * + */ +public class ServiceBackedMediaPlayer extends MediaPlayerImpl { + static final String INTENT_NAME = "com.aocate.intent.PLAY_AUDIO_ADJUST_SPEED_0_8"; + + private static final String SBMP_TAG = "AocateServiceBackedMediaPlayer"; + + private ServiceConnection mPlayMediaServiceConnection = null; + protected IPlayMedia_0_8 pmInterface = null; + private Intent playMediaServiceIntent = null; + // In some cases, we're going to have to replace the + // android.media.MediaPlayer on the fly, and we don't want to touch the + // wrong media player. + + private long sessionId = 0; + private boolean isErroring = false; + private int mAudioStreamType = AudioManager.STREAM_MUSIC; + + private WakeLock mWakeLock = null; + + // So here's the major problem + // Sometimes the service won't exist or won't be connected, + // so start with an android.media.MediaPlayer, and when + // the service is connected, use that from then on + public ServiceBackedMediaPlayer(MediaPlayer owningMediaPlayer, final Context context, final ServiceConnection serviceConnection) { + super(owningMediaPlayer, context); + Log.d(SBMP_TAG, "Instantiating ServiceBackedMediaPlayer 87"); + this.playMediaServiceIntent = + new Intent(INTENT_NAME); + this.mPlayMediaServiceConnection = new ServiceConnection() { + public void onServiceConnected(ComponentName name, IBinder service) { + IPlayMedia_0_8 tmpPlayMediaInterface = IPlayMedia_0_8.Stub.asInterface((IBinder) service); + + Log.d(SBMP_TAG, "Setting up pmInterface 94"); + if (ServiceBackedMediaPlayer.this.sessionId == 0) { + try { + // The IDeathCallback isn't a conventional callback. + // It exists so that if the client ceases to exist, + // the Service becomes aware of that and can shut + // down whatever it needs to shut down + ServiceBackedMediaPlayer.this.sessionId = tmpPlayMediaInterface.startSession(new IDeathCallback_0_8.Stub() { + }); + // This is really bad if this fails + } catch (RemoteException e) { + e.printStackTrace(); + ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + + Log.d(SBMP_TAG, "Assigning pmInterface"); + + ServiceBackedMediaPlayer.this.setOnBufferingUpdateCallback(tmpPlayMediaInterface); + ServiceBackedMediaPlayer.this.setOnCompletionCallback(tmpPlayMediaInterface); + ServiceBackedMediaPlayer.this.setOnErrorCallback(tmpPlayMediaInterface); + ServiceBackedMediaPlayer.this.setOnInfoCallback(tmpPlayMediaInterface); + ServiceBackedMediaPlayer.this.setOnPitchAdjustmentAvailableChangedListener(tmpPlayMediaInterface); + ServiceBackedMediaPlayer.this.setOnPreparedCallback(tmpPlayMediaInterface); + ServiceBackedMediaPlayer.this.setOnSeekCompleteCallback(tmpPlayMediaInterface); + ServiceBackedMediaPlayer.this.setOnSpeedAdjustmentAvailableChangedCallback(tmpPlayMediaInterface); + + // In order to avoid race conditions from the sessionId or listener not being assigned + pmInterface = tmpPlayMediaInterface; + + Log.d(SBMP_TAG, "Invoking onServiceConnected"); + serviceConnection.onServiceConnected(name, service); + } + + public void onServiceDisconnected(ComponentName name) { + Log.d(SBMP_TAG, "onServiceDisconnected 114"); + + pmInterface = null; + + sessionId = 0; + + serviceConnection.onServiceDisconnected(name); + } + }; + + Log.d(SBMP_TAG, "Connecting PlayMediaService 124"); + if (!ConnectPlayMediaService()) { + ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + + private boolean ConnectPlayMediaService() { + Log.d(SBMP_TAG, "ConnectPlayMediaService()"); + + if (MediaPlayer.isIntentAvailable(mContext, INTENT_NAME)) { + Log.d(SBMP_TAG, INTENT_NAME + " is available"); + if (pmInterface == null) { + try { + Log.d(SBMP_TAG, "Binding service"); + return mContext.bindService(playMediaServiceIntent, mPlayMediaServiceConnection, Context.BIND_AUTO_CREATE); + } catch (Exception e) { + return false; + } + } else { + Log.d(SBMP_TAG, "Service already bound"); + return true; + } + } + else { + Log.d(SBMP_TAG, INTENT_NAME + " is not available"); + return false; + } + } + + /** + * Returns true if pitch can be changed at this moment + * @return True if pitch can be changed + */ + @Override + public boolean canSetPitch() { + Log.d(SBMP_TAG, "canSetPitch() 155"); + + if (pmInterface == null) { + if (!ConnectPlayMediaService()) { + ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + if (pmInterface != null) { + // Can't set pitch if the service isn't connected + try { + return pmInterface.canSetPitch(ServiceBackedMediaPlayer.this.sessionId); + } catch (RemoteException e) { + e.printStackTrace(); + ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + return false; + } + + /** + * Returns true if speed can be changed at this moment + * @return True if speed can be changed + */ + @Override + public boolean canSetSpeed() { + Log.d(SBMP_TAG, "canSetSpeed() 180"); + if (pmInterface == null) { + if (!ConnectPlayMediaService()) { + ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + if (pmInterface != null) { + // Can't set speed if the service isn't connected + try { + return pmInterface.canSetSpeed(ServiceBackedMediaPlayer.this.sessionId); + } catch (RemoteException e) { + e.printStackTrace(); + ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + return false; + } + + void error(int what, int extra) { + owningMediaPlayer.lock.lock(); + Log.e(SBMP_TAG, "error(" + what + ", " + extra + ")"); + try { + if (!this.isErroring) { + this.isErroring = true; + owningMediaPlayer.state = State.ERROR; + if (owningMediaPlayer.onErrorListener != null) { + if (owningMediaPlayer.onErrorListener.onError(owningMediaPlayer, what, extra)) { + return; + } + } + if (owningMediaPlayer.onCompletionListener != null) { + owningMediaPlayer.onCompletionListener.onCompletion(owningMediaPlayer); + } + } + } + finally { + this.isErroring = false; + owningMediaPlayer.lock.unlock(); + } + } + + protected void finalize() throws Throwable { + owningMediaPlayer.lock.lock(); + try { + Log.d(SBMP_TAG, "finalize() 224"); + this.release(); + } + finally { + owningMediaPlayer.lock.unlock(); + } + } + + /** + * Returns the number of steps (in a musical scale) by which playback is + * currently shifted. When greater than zero, pitch is shifted up. + * When less than zero, pitch is shifted down. + * @return The number of steps pitch is currently shifted by + */ + @Override + public float getCurrentPitchStepsAdjustment() { + Log.d(SBMP_TAG, "getCurrentPitchStepsAdjustment() 240"); + if (pmInterface == null) { + if (!ConnectPlayMediaService()) { + ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + if (pmInterface != null) { + // Can't set pitch if the service isn't connected + try { + return pmInterface.getCurrentPitchStepsAdjustment( + ServiceBackedMediaPlayer.this.sessionId); + } catch (RemoteException e) { + e.printStackTrace(); + ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + return 0f; + } + + /** + * Functions identically to android.media.MediaPlayer.getCurrentPosition() + * @return Current position (in milliseconds) + */ + @Override + public int getCurrentPosition() { + if (pmInterface == null) { + if (!ConnectPlayMediaService()) { + ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + try { + return pmInterface.getCurrentPosition( + ServiceBackedMediaPlayer.this.sessionId); + } catch (RemoteException e) { + e.printStackTrace(); + ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + return 0; + } + + /** + * Returns the current speed multiplier. Defaults to 1.0 (normal speed) + * @return The current speed multiplier + */ + @Override + public float getCurrentSpeedMultiplier() { + Log.d(SBMP_TAG, "getCurrentSpeedMultiplier() 286"); + if (pmInterface == null) { + if (!ConnectPlayMediaService()) { + ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + if (pmInterface != null) { + // Can't set speed if the service isn't connected + try { + return pmInterface.getCurrentSpeedMultiplier( + ServiceBackedMediaPlayer.this.sessionId); + } catch (RemoteException e) { + e.printStackTrace(); + ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + return 1; + } + + /** + * Functions identically to android.media.MediaPlayer.getDuration() + * @return Length of the track (in milliseconds) + */ + @Override + public int getDuration() { + Log.d(SBMP_TAG, "getDuration() 311"); + if (pmInterface == null) { + if (!ConnectPlayMediaService()) { + ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + try { + return pmInterface.getDuration(ServiceBackedMediaPlayer.this.sessionId); + } catch (RemoteException e) { + e.printStackTrace(); + ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + return 0; + } + + /** + * Get the maximum value that can be passed to setPlaybackSpeed + * @return The maximum speed multiplier + */ + @Override + public float getMaxSpeedMultiplier() { + Log.d(SBMP_TAG, "getMaxSpeedMultiplier() 332"); + if (pmInterface == null) { + if (!ConnectPlayMediaService()) { + ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + if (pmInterface != null) { + // Can't set speed if the Service isn't connected + try { + return pmInterface.getMaxSpeedMultiplier( + ServiceBackedMediaPlayer.this.sessionId); + } catch (RemoteException e) { + e.printStackTrace(); + ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + return 1f; + } + + /** + * Get the minimum value that can be passed to setPlaybackSpeed + * @return The minimum speed multiplier + */ + @Override + public float getMinSpeedMultiplier() { + Log.d(SBMP_TAG, "getMinSpeedMultiplier() 357"); + if (pmInterface == null) { + if (!ConnectPlayMediaService()) { + ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + if (pmInterface != null) { + // Can't set speed if the Service isn't connected + try { + return pmInterface.getMinSpeedMultiplier( + ServiceBackedMediaPlayer.this.sessionId); + } catch (RemoteException e) { + e.printStackTrace(); + ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + return 1f; + } + + public int getServiceVersionCode() { + Log.d(SBMP_TAG, "getVersionCode"); + if (pmInterface == null) { + if (!ConnectPlayMediaService()) { + ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + try { + return pmInterface.getVersionCode(); + } catch (RemoteException e) { + e.printStackTrace(); + ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + return 0; + } + + public String getServiceVersionName() { + Log.d(SBMP_TAG, "getVersionName"); + if (pmInterface == null) { + if (!ConnectPlayMediaService()) { + ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + try { + return pmInterface.getVersionName(); + } catch (RemoteException e) { + e.printStackTrace(); + ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + return ""; + } + + public boolean isConnected() { + return (pmInterface != null); + } + + /** + * Functions identically to android.media.MediaPlayer.isLooping() + * @return True if the track is looping + */ + @Override + public boolean isLooping() { + Log.d(SBMP_TAG, "isLooping() 382"); + if (pmInterface == null) { + if (!ConnectPlayMediaService()) { + ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + try { + return pmInterface.isLooping(ServiceBackedMediaPlayer.this.sessionId); + } catch (RemoteException e) { + e.printStackTrace(); + ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + return false; + } + + /** + * Functions identically to android.media.MediaPlayer.isPlaying() + * @return True if the track is playing + */ + @Override + public boolean isPlaying() { + if (pmInterface == null) { + if (!ConnectPlayMediaService()) { + ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + if (pmInterface != null) { + try { + return pmInterface.isPlaying(ServiceBackedMediaPlayer.this.sessionId); + } catch (RemoteException e) { + e.printStackTrace(); + ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + return false; + } + + /** + * Functions identically to android.media.MediaPlayer.pause() + * Pauses the track + */ + @Override + public void pause() { + Log.d(SBMP_TAG, "pause() 424"); + if (pmInterface == null) { + if (!ConnectPlayMediaService()) { + ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + try { + pmInterface.pause(ServiceBackedMediaPlayer.this.sessionId); + } catch (RemoteException e) { + e.printStackTrace(); + ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + + /** + * Functions identically to android.media.MediaPlayer.prepare() + * Prepares the track. This or prepareAsync must be called before start() + */ + @Override + public void prepare() throws IllegalStateException, IOException { + Log.d(SBMP_TAG, "prepare() 444"); + Log.d(SBMP_TAG, "onPreparedCallback is: " + ((this.mOnPreparedCallback == null) ? "null" : "non-null")); + if (pmInterface == null) { + Log.d(SBMP_TAG, "prepare: pmInterface is null"); + if (!ConnectPlayMediaService()) { + Log.d(SBMP_TAG, "prepare: Failed to connect play media service"); + ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + if (pmInterface != null) { + Log.d(SBMP_TAG, "prepare: pmInterface isn't null"); + try { + Log.d(SBMP_TAG, "prepare: Remote invoke pmInterface.prepare(" + ServiceBackedMediaPlayer.this.sessionId + ")"); + pmInterface.prepare(ServiceBackedMediaPlayer.this.sessionId); + Log.d(SBMP_TAG, "prepare: prepared"); + } catch (RemoteException e) { + Log.d(SBMP_TAG, "prepare: RemoteException"); + e.printStackTrace(); + ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + Log.d(SBMP_TAG, "Done with prepare()"); + } + + /** + * Functions identically to android.media.MediaPlayer.prepareAsync() + * Prepares the track. This or prepare must be called before start() + */ + @Override + public void prepareAsync() { + Log.d(SBMP_TAG, "prepareAsync() 469"); + if (pmInterface == null) { + if (!ConnectPlayMediaService()) { + ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + try { + pmInterface.prepareAsync(ServiceBackedMediaPlayer.this.sessionId); + } catch (RemoteException e) { + e.printStackTrace(); + ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + + /** + * Functions identically to android.media.MediaPlayer.release() + * Releases the underlying resources used by the media player. + */ + @Override + public void release() { + Log.d(SBMP_TAG, "release() 492"); + if (pmInterface == null) { + if (!ConnectPlayMediaService()) { + ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + if (pmInterface != null) { + Log.d(SBMP_TAG, "release() 500"); + try { + pmInterface.release(ServiceBackedMediaPlayer.this.sessionId); + } catch (RemoteException e) { + e.printStackTrace(); + ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + mContext.unbindService(this.mPlayMediaServiceConnection); + // Don't try to keep awake (if we were) + this.setWakeMode(mContext, 0); + pmInterface = null; + this.sessionId = 0; + } + + if ((this.mWakeLock != null) && this.mWakeLock.isHeld()) { + Log.d(SBMP_TAG, "Releasing wakelock"); + this.mWakeLock.release(); + } + } + + /** + * Functions identically to android.media.MediaPlayer.reset() + * Resets the track to idle state + */ + @Override + public void reset() { + Log.d(SBMP_TAG, "reset() 523"); + if (pmInterface == null) { + if (!ConnectPlayMediaService()) { + ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + try { + pmInterface.reset(ServiceBackedMediaPlayer.this.sessionId); + } catch (RemoteException e) { + e.printStackTrace(); + ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + + /** + * Functions identically to android.media.MediaPlayer.seekTo(int msec) + * Seeks to msec in the track + */ + @Override + public void seekTo(int msec) throws IllegalStateException { + Log.d(SBMP_TAG, "seekTo(" + msec + ")"); + if (pmInterface == null) { + if (!ConnectPlayMediaService()) { + ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + try { + pmInterface.seekTo(ServiceBackedMediaPlayer.this.sessionId, msec); + } catch (RemoteException e) { + e.printStackTrace(); + ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + + /** + * Functions identically to android.media.MediaPlayer.setAudioStreamType(int streamtype) + * Sets the audio stream type. + */ + @Override + public void setAudioStreamType(int streamtype) { + Log.d(SBMP_TAG, "setAudioStreamType(" + streamtype + ")"); + if (pmInterface == null) { + if (!ConnectPlayMediaService()) { + ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + try { + pmInterface.setAudioStreamType( + ServiceBackedMediaPlayer.this.sessionId, + this.mAudioStreamType); + } catch (RemoteException e) { + e.printStackTrace(); + ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + + + /** + * Functions identically to android.media.MediaPlayer.setDataSource(Context context, Uri uri) + * Sets uri as data source in the context given + */ + @Override + public void setDataSource(Context context, Uri uri) throws IllegalArgumentException, IllegalStateException, IOException { + Log.d(SBMP_TAG, "setDataSource(context, uri)"); + if (pmInterface == null) { + if (!ConnectPlayMediaService()) { + ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + try { + pmInterface.setDataSourceUri( + ServiceBackedMediaPlayer.this.sessionId, + uri); + } catch (RemoteException e) { + e.printStackTrace(); + ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + + /** + * Functions identically to android.media.MediaPlayer.setDataSource(String path) + * Sets the data source of the track to a file given. + */ + @Override + public void setDataSource(String path) throws IllegalArgumentException, IllegalStateException, IOException { + Log.d(SBMP_TAG, "setDataSource(path)"); + if (pmInterface == null) { + if (!ConnectPlayMediaService()) { + ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + if (pmInterface == null) { + ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + else { + try { + pmInterface.setDataSourceString( + ServiceBackedMediaPlayer.this.sessionId, + path); + } catch (RemoteException e) { + e.printStackTrace(); + ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + } + + /** + * Sets whether to use speed adjustment or not. Speed adjustment on is + * more computation-intensive than with it off. + * @param enableSpeedAdjustment Whether speed adjustment should be supported. + */ + @Override + public void setEnableSpeedAdjustment(boolean enableSpeedAdjustment) { + // TODO: This has no business being here, I think + owningMediaPlayer.lock.lock(); + Log.d(SBMP_TAG, "setEnableSpeedAdjustment(enableSpeedAdjustment)"); + try { + if (pmInterface == null) { + if (!ConnectPlayMediaService()) { + ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + if (pmInterface != null) { + // Can't set speed if the Service isn't connected + try { + pmInterface.setEnableSpeedAdjustment( + ServiceBackedMediaPlayer.this.sessionId, + enableSpeedAdjustment); + } catch (RemoteException e) { + e.printStackTrace(); + ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + } + finally { + owningMediaPlayer.lock.unlock(); + } + } + + + /** + * Functions identically to android.media.MediaPlayer.setLooping(boolean loop) + * Sets the track to loop infinitely if loop is true, play once if loop is false + */ + @Override + public void setLooping(boolean loop) { + Log.d(SBMP_TAG, "setLooping(" + loop + ")"); + if (pmInterface == null) { + if (!ConnectPlayMediaService()) { + ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + try { + pmInterface.setLooping(ServiceBackedMediaPlayer.this.sessionId, loop); + } catch (RemoteException e) { + e.printStackTrace(); + ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + + /** + * Sets the number of steps (in a musical scale) by which playback is + * currently shifted. When greater than zero, pitch is shifted up. + * When less than zero, pitch is shifted down. + * + * @param pitchSteps The number of steps by which to shift playback + */ + @Override + public void setPitchStepsAdjustment(float pitchSteps) { + Log.d(SBMP_TAG, "setPitchStepsAdjustment(" + pitchSteps + ")"); + if (pmInterface == null) { + if (!ConnectPlayMediaService()) { + ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + if (pmInterface != null) { + // Can't set speed if the Service isn't connected + try { + pmInterface.setPitchStepsAdjustment( + ServiceBackedMediaPlayer.this.sessionId, + pitchSteps); + } catch (RemoteException e) { + e.printStackTrace(); + ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + } + + /** + * Sets the percentage by which pitch is currently shifted. When + * greater than zero, pitch is shifted up. When less than zero, pitch + * is shifted down + * @param f The percentage to shift pitch + */ + @Override + public void setPlaybackPitch(float f) { + Log.d(SBMP_TAG, "setPlaybackPitch(" + f + ")"); + if (pmInterface == null) { + if (!ConnectPlayMediaService()) { + ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + if (pmInterface != null) { + // Can't set speed if the Service isn't connected + try { + pmInterface.setPlaybackPitch( + ServiceBackedMediaPlayer.this.sessionId, + f); + } catch (RemoteException e) { + e.printStackTrace(); + ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + } + + /** + * Set playback speed. 1.0 is normal speed, 2.0 is double speed, and so + * on. Speed should never be set to 0 or below. + * @param f The speed multiplier to use for further playback + */ + @Override + public void setPlaybackSpeed(float f) { + Log.d(SBMP_TAG, "setPlaybackSpeed(" + f + ")"); + if (pmInterface == null) { + if (!ConnectPlayMediaService()) { + ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + if (pmInterface != null) { + // Can't set speed if the Service isn't connected + try { + pmInterface.setPlaybackSpeed( + ServiceBackedMediaPlayer.this.sessionId, + f); + } catch (RemoteException e) { + e.printStackTrace(); + ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + } + + @Override + public void setSpeedAdjustmentAlgorithm(int algorithm) { + if (pmInterface == null) { + if (!ConnectPlayMediaService()) { + ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + try { + pmInterface.setSpeedAdjustmentAlgorithm( + ServiceBackedMediaPlayer.this.sessionId, + algorithm); + } catch (RemoteException e) { + e.printStackTrace(); + ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + + /** + * Functions identically to android.media.MediaPlayer.setVolume(float leftVolume, float rightVolume) + * Sets the stereo volume + */ + @Override + public void setVolume(float leftVolume, float rightVolume) { + Log.d(SBMP_TAG, "setVolume(" + leftVolume + ", " + rightVolume + ")"); + if (pmInterface == null) { + if (!ConnectPlayMediaService()) { + ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + try { + pmInterface.setVolume( + ServiceBackedMediaPlayer.this.sessionId, + leftVolume, + rightVolume); + } catch (RemoteException e) { + e.printStackTrace(); + ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + + /** + * Functions identically to android.media.MediaPlayer.setWakeMode(Context context, int mode) + * Acquires a wake lock in the context given. You must request the appropriate permissions + * in your AndroidManifest.xml file. + */ + @Override + // This does not just call .setWakeMode() in the Service because doing so + // would add a permission requirement to the Service. Do it here, and it's + // the client app's responsibility to request that permission + public void setWakeMode(Context context, int mode) { + Log.d(SBMP_TAG, "setWakeMode(context, " + mode + ")"); + if ((this.mWakeLock != null) + && (this.mWakeLock.isHeld())) { + this.mWakeLock.release(); + } + if (mode != 0) { + if (this.mWakeLock == null) { + PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE); + // Since mode can't be changed on the fly, we have to allocate a new one + this.mWakeLock = pm.newWakeLock(mode, this.getClass().getName()); + } + + this.mWakeLock.acquire(); + } + } + + private IOnBufferingUpdateListenerCallback_0_8.Stub mOnBufferingUpdateCallback = null; + private void setOnBufferingUpdateCallback(IPlayMedia_0_8 iface) { + try { + if (this.mOnBufferingUpdateCallback == null) { + mOnBufferingUpdateCallback = new IOnBufferingUpdateListenerCallback_0_8.Stub() { + public void onBufferingUpdate(int percent) + throws RemoteException { + owningMediaPlayer.lock.lock(); + try { + if ((owningMediaPlayer.onBufferingUpdateListener != null) + && (owningMediaPlayer.mpi == ServiceBackedMediaPlayer.this)) { + owningMediaPlayer.onBufferingUpdateListener.onBufferingUpdate(owningMediaPlayer, percent); + } + } + finally { + owningMediaPlayer.lock.unlock(); + } + } + }; + } + iface.registerOnBufferingUpdateCallback( + ServiceBackedMediaPlayer.this.sessionId, + mOnBufferingUpdateCallback); + } catch (RemoteException e) { + e.printStackTrace(); + ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + + private IOnCompletionListenerCallback_0_8.Stub mOnCompletionCallback = null; + private void setOnCompletionCallback(IPlayMedia_0_8 iface) { + try { + if (this.mOnCompletionCallback == null) { + this.mOnCompletionCallback = new IOnCompletionListenerCallback_0_8.Stub() { + public void onCompletion() throws RemoteException { + owningMediaPlayer.lock.lock(); + Log.d(SBMP_TAG, "onCompletionListener being called"); + try { + if (owningMediaPlayer.onCompletionListener != null) { + owningMediaPlayer.onCompletionListener.onCompletion(owningMediaPlayer); + } + } + finally { + owningMediaPlayer.lock.unlock(); + } + } + }; + } + iface.registerOnCompletionCallback( + ServiceBackedMediaPlayer.this.sessionId, + this.mOnCompletionCallback); + } catch (RemoteException e) { + e.printStackTrace(); + ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + + private IOnErrorListenerCallback_0_8.Stub mOnErrorCallback = null; + private void setOnErrorCallback(IPlayMedia_0_8 iface) { + try { + if (this.mOnErrorCallback == null) { + this.mOnErrorCallback = new IOnErrorListenerCallback_0_8.Stub() { + public boolean onError(int what, int extra) throws RemoteException { + owningMediaPlayer.lock.lock(); + try { + if (owningMediaPlayer.onErrorListener != null) { + return owningMediaPlayer.onErrorListener.onError(owningMediaPlayer, what, extra); + } + return false; + } + finally { + owningMediaPlayer.lock.unlock(); + } + } + }; + } + iface.registerOnErrorCallback( + ServiceBackedMediaPlayer.this.sessionId, + this.mOnErrorCallback); + } catch (RemoteException e) { + e.printStackTrace(); + ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + + private IOnInfoListenerCallback_0_8.Stub mOnInfoCallback = null; + private void setOnInfoCallback(IPlayMedia_0_8 iface) { + try { + if (this.mOnInfoCallback == null) { + this.mOnInfoCallback = new IOnInfoListenerCallback_0_8.Stub() { + public boolean onInfo(int what, int extra) throws RemoteException { + owningMediaPlayer.lock.lock(); + try { + if ((owningMediaPlayer.onInfoListener != null) + && (owningMediaPlayer.mpi == ServiceBackedMediaPlayer.this)) { + return owningMediaPlayer.onInfoListener.onInfo(owningMediaPlayer, what, extra); + } + } + finally { + owningMediaPlayer.lock.unlock(); + } + return false; + } + }; + } + iface.registerOnInfoCallback( + ServiceBackedMediaPlayer.this.sessionId, + this.mOnInfoCallback); + } catch (RemoteException e) { + e.printStackTrace(); + ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + + private IOnPitchAdjustmentAvailableChangedListenerCallback_0_8.Stub mOnPitchAdjustmentAvailableChangedCallback = null; + private void setOnPitchAdjustmentAvailableChangedListener(IPlayMedia_0_8 iface) { + try { + if (this.mOnPitchAdjustmentAvailableChangedCallback == null) { + this.mOnPitchAdjustmentAvailableChangedCallback = new IOnPitchAdjustmentAvailableChangedListenerCallback_0_8.Stub() { + public void onPitchAdjustmentAvailableChanged( + boolean pitchAdjustmentAvailable) + throws RemoteException { + owningMediaPlayer.lock.lock(); + try { + if (owningMediaPlayer.onPitchAdjustmentAvailableChangedListener != null) { + owningMediaPlayer.onPitchAdjustmentAvailableChangedListener.onPitchAdjustmentAvailableChanged(owningMediaPlayer, pitchAdjustmentAvailable); + } + } + finally { + owningMediaPlayer.lock.unlock(); + } + } + }; + } + iface.registerOnPitchAdjustmentAvailableChangedCallback( + ServiceBackedMediaPlayer.this.sessionId, + this.mOnPitchAdjustmentAvailableChangedCallback); + } catch (RemoteException e) { + e.printStackTrace(); + ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + + private IOnPreparedListenerCallback_0_8.Stub mOnPreparedCallback = null; + private void setOnPreparedCallback(IPlayMedia_0_8 iface) { + try { + if (this.mOnPreparedCallback == null) { + this.mOnPreparedCallback = new IOnPreparedListenerCallback_0_8.Stub() { + public void onPrepared() throws RemoteException { + owningMediaPlayer.lock.lock(); + Log.d(SBMP_TAG, "setOnPreparedCallback.mOnPreparedCallback.onPrepared 1050"); + try { + Log.d(SBMP_TAG, "owningMediaPlayer.onPreparedListener is " + ((owningMediaPlayer.onPreparedListener == null) ? "null" : "non-null")); + Log.d(SBMP_TAG, "owningMediaPlayer.mpi is " + ((owningMediaPlayer.mpi == ServiceBackedMediaPlayer.this) ? "this" : "not this")); + ServiceBackedMediaPlayer.this.lockMuteOnPreparedCount.lock(); + try { + if (ServiceBackedMediaPlayer.this.muteOnPreparedCount > 0) { + ServiceBackedMediaPlayer.this.muteOnPreparedCount--; + } + else { + ServiceBackedMediaPlayer.this.muteOnPreparedCount = 0; + if (ServiceBackedMediaPlayer.this.owningMediaPlayer.onPreparedListener != null) { + owningMediaPlayer.onPreparedListener.onPrepared(owningMediaPlayer); + } + } + } + finally { + ServiceBackedMediaPlayer.this.lockMuteOnPreparedCount.unlock(); + } + } + finally { + owningMediaPlayer.lock.unlock(); + } + } + }; + } + iface.registerOnPreparedCallback( + ServiceBackedMediaPlayer.this.sessionId, + this.mOnPreparedCallback); + } catch (RemoteException e) { + e.printStackTrace(); + ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + + private IOnSeekCompleteListenerCallback_0_8.Stub mOnSeekCompleteCallback = null; + private void setOnSeekCompleteCallback(IPlayMedia_0_8 iface) { + try { + if (this.mOnSeekCompleteCallback == null) { + this.mOnSeekCompleteCallback = new IOnSeekCompleteListenerCallback_0_8.Stub() { + public void onSeekComplete() throws RemoteException { + Log.d(SBMP_TAG, "onSeekComplete() 941"); + owningMediaPlayer.lock.lock(); + try { + if (ServiceBackedMediaPlayer.this.muteOnSeekCount > 0) { + Log.d(SBMP_TAG, "The next " + ServiceBackedMediaPlayer.this.muteOnSeekCount + " seek events are muted (counting this one)"); + ServiceBackedMediaPlayer.this.muteOnSeekCount--; + } + else { + ServiceBackedMediaPlayer.this.muteOnSeekCount = 0; + Log.d(SBMP_TAG, "Attempting to invoke next seek event"); + if (ServiceBackedMediaPlayer.this.owningMediaPlayer.onSeekCompleteListener != null) { + Log.d(SBMP_TAG, "Invoking onSeekComplete"); + owningMediaPlayer.onSeekCompleteListener.onSeekComplete(owningMediaPlayer); + } + } + } + finally { + owningMediaPlayer.lock.unlock(); + } + } + }; + } + iface.registerOnSeekCompleteCallback( + ServiceBackedMediaPlayer.this.sessionId, + this.mOnSeekCompleteCallback); + } catch (RemoteException e) { + e.printStackTrace(); + ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + + private IOnSpeedAdjustmentAvailableChangedListenerCallback_0_8.Stub mOnSpeedAdjustmentAvailableChangedCallback = null; + private void setOnSpeedAdjustmentAvailableChangedCallback(IPlayMedia_0_8 iface) { + try { + Log.d(SBMP_TAG, "Setting the service of on speed adjustment available changed"); + if (this.mOnSpeedAdjustmentAvailableChangedCallback == null) { + this.mOnSpeedAdjustmentAvailableChangedCallback = new IOnSpeedAdjustmentAvailableChangedListenerCallback_0_8.Stub() { + public void onSpeedAdjustmentAvailableChanged( + boolean speedAdjustmentAvailable) + throws RemoteException { + owningMediaPlayer.lock.lock(); + try { + if (owningMediaPlayer.onSpeedAdjustmentAvailableChangedListener != null) { + owningMediaPlayer.onSpeedAdjustmentAvailableChangedListener.onSpeedAdjustmentAvailableChanged(owningMediaPlayer, speedAdjustmentAvailable); + } + } + finally { + owningMediaPlayer.lock.unlock(); + } + } + }; + } + iface.registerOnSpeedAdjustmentAvailableChangedCallback( + ServiceBackedMediaPlayer.this.sessionId, + this.mOnSpeedAdjustmentAvailableChangedCallback); + } catch (RemoteException e) { + e.printStackTrace(); + ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + + /** + * Functions identically to android.media.MediaPlayer.start() + * Starts a track playing + */ + @Override + public void start() { + Log.d(SBMP_TAG, "start()"); + if (pmInterface == null) { + if (!ConnectPlayMediaService()) { + ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + try { + pmInterface.start(ServiceBackedMediaPlayer.this.sessionId); + } catch (RemoteException e) { + e.printStackTrace(); + ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + + /** + * Functions identically to android.media.MediaPlayer.stop() + * Stops a track playing and resets its position to the start. + */ + @Override + public void stop() { + Log.d(SBMP_TAG, "stop()"); + if (pmInterface == null) { + if (!ConnectPlayMediaService()) { + ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + try { + pmInterface.stop(ServiceBackedMediaPlayer.this.sessionId); + } catch (RemoteException e) { + e.printStackTrace(); + ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } +} \ No newline at end of file diff --git a/core/src/main/java/com/aocate/media/SpeedAdjustmentAlgorithm.java b/core/src/main/java/com/aocate/media/SpeedAdjustmentAlgorithm.java new file mode 100644 index 000000000..d337a0452 --- /dev/null +++ b/core/src/main/java/com/aocate/media/SpeedAdjustmentAlgorithm.java @@ -0,0 +1,31 @@ +// Copyright 2011, Aocate, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.aocate.media; + +public class SpeedAdjustmentAlgorithm { + /** + * Use this to use the user-specified algorithm + */ + public static int DEFAULT = 0; + + /** + * Better for voice audio + */ + public static int SONIC = 1; + /** + * Better for music audio + */ + public static int WSOLA = 2; +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/ApplicationCallbacks.java b/core/src/main/java/de/danoeh/antennapod/core/ApplicationCallbacks.java new file mode 100644 index 000000000..69a959ba8 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/ApplicationCallbacks.java @@ -0,0 +1,22 @@ +package de.danoeh.antennapod.core; + +import android.app.Application; +import android.content.Context; +import android.content.Intent; + +/** + * Callbacks related to the application in general + */ +public interface ApplicationCallbacks { + + /** + * Returns a non-null instance of the application class + */ + public Application getApplicationInstance(); + + /** + * Returns a non-null intent that starts the storage error + * activity. + */ + public Intent getStorageErrorActivity(Context context); +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/ClientConfig.java b/core/src/main/java/de/danoeh/antennapod/core/ClientConfig.java new file mode 100644 index 000000000..e5e609f5f --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/ClientConfig.java @@ -0,0 +1,25 @@ +package de.danoeh.antennapod.core; + +/** + * Stores callbacks for core classes like Services, DB classes etc. and other configuration variables. + * Apps using the core module of AntennaPod should register implementations of all interfaces here. + */ +public class ClientConfig { + + /** + * Should be used when setting User-Agent header for HTTP-requests. + */ + public static String USER_AGENT; + + public static ApplicationCallbacks applicationCallbacks; + + public static DownloadServiceCallbacks downloadServiceCallbacks; + + public static PlaybackServiceCallbacks playbackServiceCallbacks; + + public static GpodnetCallbacks gpodnetCallbacks; + + public static FlattrCallbacks flattrCallbacks; + + public static StorageCallbacks storageCallbacks; +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/DownloadServiceCallbacks.java b/core/src/main/java/de/danoeh/antennapod/core/DownloadServiceCallbacks.java new file mode 100644 index 000000000..55b69fdec --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/DownloadServiceCallbacks.java @@ -0,0 +1,44 @@ +package de.danoeh.antennapod.core; + +import android.app.PendingIntent; +import android.content.Context; + +import de.danoeh.antennapod.core.service.download.DownloadRequest; + +/** + * Callbacks for the DownloadService of the core module + */ +public interface DownloadServiceCallbacks { + + /** + * Returns a PendingIntent for a notification the main notification of the DownloadService. + *

+ * The PendingIntent takes the users to a screen where they can observe all currently running + * downloads. + * + * @return A non-null PendingIntent for the notification. + */ + public PendingIntent getNotificationContentIntent(Context context); + + /** + * Returns a PendingIntent for a notification that tells the user to enter a username + * or a password for a requested download. + *

+ * The PendingIntent takes users to an Activity that lets the user enter their username + * and password to retry the download. + * + * @return A non-null PendingIntent for the notification. + */ + public PendingIntent getAuthentificationNotificationContentIntent(Context context, DownloadRequest request); + + /** + * Returns a PendingIntent for notification that notifies the user about the completion of downloads + * along with information about failed and successful downloads. + *

+ * The PendingIntent takes users to an activity where they can look at all successful and failed downloads. + * + * @return A non-null PendingIntent for the notification. + */ + public PendingIntent getReportNotificationContentIntent(Context context); +} + diff --git a/core/src/main/java/de/danoeh/antennapod/core/FlattrCallbacks.java b/core/src/main/java/de/danoeh/antennapod/core/FlattrCallbacks.java new file mode 100644 index 000000000..cee1029d8 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/FlattrCallbacks.java @@ -0,0 +1,36 @@ +package de.danoeh.antennapod.core; + +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; + +import org.shredzone.flattr4j.oauth.AccessToken; + +/** + * Callbacks for the flattr integration of the app. + */ +public interface FlattrCallbacks { + + /** + * Returns if true if the flattr integration should be activated, + * false otherwise. + */ + public boolean flattrEnabled(); + + /** + * Returns an intent that starts the activity that is responsible for + * letting users log into their flattr account. + * + * @return The intent that starts the authentication activity or null + * if flattr integration is disabled (i.e. flattrEnabled() == false). + */ + public Intent getFlattrAuthenticationActivityIntent(Context context); + + public PendingIntent getFlattrFailedNotificationContentIntent(Context context); + + public String getFlattrAppKey(); + + public String getFlattrAppSecret(); + + public void handleFlattrAuthenticationSuccess(AccessToken token); +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/GpodnetCallbacks.java b/core/src/main/java/de/danoeh/antennapod/core/GpodnetCallbacks.java new file mode 100644 index 000000000..6174bce29 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/GpodnetCallbacks.java @@ -0,0 +1,27 @@ +package de.danoeh.antennapod.core; + +import android.app.PendingIntent; +import android.content.Context; + +/** + * Callbacks related to the gpodder.net integration of the core module + */ +public interface GpodnetCallbacks { + + + /** + * Returns if true if the gpodder.net integration should be activated, + * false otherwise. + */ + public boolean gpodnetEnabled(); + + /** + * Returns a PendingIntent for the error notification of the GpodnetSyncService. + *

+ * What the PendingIntent does may be implementation-specific. + * + * @return A PendingIntent for the notification or null if gpodder.net integration + * has been disabled (i.e. gpodnetEnabled() == false). + */ + public PendingIntent getGpodnetSyncServiceErrorNotificationPendingIntent(Context context); +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/PlaybackServiceCallbacks.java b/core/src/main/java/de/danoeh/antennapod/core/PlaybackServiceCallbacks.java new file mode 100644 index 000000000..e37c8fcfd --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/PlaybackServiceCallbacks.java @@ -0,0 +1,21 @@ +package de.danoeh.antennapod.core; + +import android.content.Context; +import android.content.Intent; + +import de.danoeh.antennapod.core.feed.MediaType; + +/** + * Callbacks for the PlaybackService of the core module + */ +public interface PlaybackServiceCallbacks { + + /** + * Returns an intent which starts an audio- or videoplayer, depending on the + * type of media that is being played. + * + * @param mediaType The type of media that is being played. + * @return A non-null activity intent. + */ + public Intent getPlayerActivityIntent(Context context, MediaType mediaType); +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/StorageCallbacks.java b/core/src/main/java/de/danoeh/antennapod/core/StorageCallbacks.java new file mode 100644 index 000000000..5d1a0fffc --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/StorageCallbacks.java @@ -0,0 +1,27 @@ +package de.danoeh.antennapod.core; + +import android.database.sqlite.SQLiteDatabase; + +/** + * Callbacks for the classes in the storage package of the core module. + */ +public interface StorageCallbacks { + + /** + * Returns the current version of the database. + * + * @return The non-negative version number of the database. + */ + public int getDatabaseVersion(); + + /** + * Upgrades the given database from an old version to a newer version. + * + * @param db The database that is supposed to be upgraded. + * @param oldVersion The old version of the database. + * @param newVersion The version that the database is supposed to be upgraded to. + */ + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion); + + +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/asynctask/DownloadObserver.java b/core/src/main/java/de/danoeh/antennapod/core/asynctask/DownloadObserver.java new file mode 100644 index 000000000..a13130082 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/asynctask/DownloadObserver.java @@ -0,0 +1,177 @@ +package de.danoeh.antennapod.core.asynctask; + +import android.app.Activity; +import android.content.*; +import android.os.Handler; +import android.os.IBinder; +import android.util.Log; + +import org.apache.commons.lang3.Validate; + +import de.danoeh.antennapod.core.BuildConfig; +import de.danoeh.antennapod.core.service.download.DownloadService; +import de.danoeh.antennapod.core.service.download.Downloader; + +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Provides access to the DownloadService's list of items that are currently being downloaded. + * The DownloadObserver object should be created in the activity's onCreate() method. resume() and pause() + * should be called in the activity's onResume() and onPause() methods + */ +public class DownloadObserver { + private static final String TAG = "DownloadObserver"; + + /** + * Time period between update notifications. + */ + public static final int WAITING_INTERVAL_MS = 3000; + + private volatile Activity activity; + private final Handler handler; + private final Callback callback; + + private DownloadService downloadService = null; + private AtomicBoolean mIsBound = new AtomicBoolean(false); + + private Thread refresherThread; + private AtomicBoolean refresherThreadRunning = new AtomicBoolean(false); + + + /** + * Creates a new download observer. + * + * @param activity Used for registering receivers + * @param handler All callback methods are executed on this handler. The handler MUST run on the GUI thread. + * @param callback Callback methods for posting content updates + * @throws java.lang.IllegalArgumentException if one of the arguments is null. + */ + public DownloadObserver(Activity activity, Handler handler, Callback callback) { + Validate.notNull(activity); + Validate.notNull(handler); + Validate.notNull(callback); + + this.activity = activity; + this.handler = handler; + this.callback = callback; + } + + public void onResume() { + if (BuildConfig.DEBUG) Log.d(TAG, "DownloadObserver resumed"); + activity.registerReceiver(contentChangedReceiver, new IntentFilter(DownloadService.ACTION_DOWNLOADS_CONTENT_CHANGED)); + connectToDownloadService(); + } + + public void onPause() { + if (BuildConfig.DEBUG) Log.d(TAG, "DownloadObserver paused"); + try { + activity.unregisterReceiver(contentChangedReceiver); + } catch (IllegalArgumentException e) { + e.printStackTrace(); + } + try { + activity.unbindService(mConnection); + } catch (IllegalArgumentException e) { + e.printStackTrace(); + } + stopRefresher(); + } + + private BroadcastReceiver contentChangedReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + // reconnect to DownloadService if connection has been closed + if (downloadService == null) { + connectToDownloadService(); + } + callback.onContentChanged(); + startRefresher(); + } + }; + + public interface Callback { + void onContentChanged(); + + void onDownloadDataAvailable(List 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/core/src/main/java/de/danoeh/antennapod/core/asynctask/FeedRemover.java b/core/src/main/java/de/danoeh/antennapod/core/asynctask/FeedRemover.java new file mode 100644 index 000000000..255b95119 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/asynctask/FeedRemover.java @@ -0,0 +1,74 @@ +package de.danoeh.antennapod.core.asynctask; + +import android.annotation.SuppressLint; +import android.app.ProgressDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnCancelListener; +import android.os.AsyncTask; +import de.danoeh.antennapod.core.R; +import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.core.storage.DBWriter; + +import java.util.concurrent.ExecutionException; + +/** Removes a feed in the background. */ +public class FeedRemover extends AsyncTask { + Context context; + ProgressDialog dialog; + Feed feed; + + public FeedRemover(Context context, Feed feed) { + super(); + this.context = context; + this.feed = feed; + } + + @Override + protected Void doInBackground(Void... params) { + try { + DBWriter.deleteFeed(context, feed.getId()).get(); + } catch (InterruptedException e) { + e.printStackTrace(); + } catch (ExecutionException e) { + e.printStackTrace(); + } + return null; + } + + @Override + protected void onCancelled() { + dialog.dismiss(); + } + + @Override + protected void onPostExecute(Void result) { + dialog.dismiss(); + } + + @Override + protected void onPreExecute() { + dialog = new ProgressDialog(context); + dialog.setMessage(context.getString(R.string.feed_remover_msg)); + dialog.setOnCancelListener(new OnCancelListener() { + + @Override + public void onCancel(DialogInterface dialog) { + cancel(true); + + } + + }); + dialog.show(); + } + + @SuppressLint("NewApi") + public void executeAsync() { + if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.GINGERBREAD_MR1) { + executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } else { + execute(); + } + } + +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/asynctask/FlattrClickWorker.java b/core/src/main/java/de/danoeh/antennapod/core/asynctask/FlattrClickWorker.java new file mode 100644 index 000000000..5d2d5d441 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/asynctask/FlattrClickWorker.java @@ -0,0 +1,237 @@ +package de.danoeh.antennapod.core.asynctask; + +import android.annotation.TargetApi; +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.os.AsyncTask; +import android.support.v4.app.NotificationCompat; +import android.util.Log; +import android.widget.Toast; + +import org.apache.commons.lang3.Validate; +import org.shredzone.flattr4j.exception.FlattrException; + +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; + +import de.danoeh.antennapod.core.BuildConfig; +import de.danoeh.antennapod.core.ClientConfig; +import de.danoeh.antennapod.core.R; +import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.core.util.NetworkUtils; +import de.danoeh.antennapod.core.util.flattr.FlattrThing; +import de.danoeh.antennapod.core.util.flattr.FlattrUtils; + +/** + * Performs a click action in a background thread. + *

+ * 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, + ClientConfig.flattrCallbacks.getFlattrAuthenticationActivityIntent(context), 0); + + Notification notification = new NotificationCompat.Builder(context) + .setStyle(new NotificationCompat.BigTextStyle().bigText(context.getString(R.string.no_flattr_token_notification_msg))) + .setContentIntent(contentIntent) + .setContentTitle(context.getString(R.string.no_flattr_token_title)) + .setTicker(context.getString(R.string.no_flattr_token_title)) + .setSmallIcon(R.drawable.stat_notify_sync_error) + .setOngoing(false) + .setAutoCancel(true) + .build(); + ((NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE)).notify(NOTIFICATION_ID, notification); + } + + private void postFlattrFailedNotification() { + if (countFailed == 0) { + return; + } + + PendingIntent contentIntent = ClientConfig.flattrCallbacks.getFlattrFailedNotificationContentIntent(context); + String title; + String subtext; + + if (countFailed == 1) { + title = context.getString(R.string.flattrd_failed_label); + String exceptionMsg = (exception.getMessage() != null) ? exception.getMessage() : ""; + subtext = context.getString(R.string.flattr_click_failure, extraFlattrThing.getTitle()) + + "\n" + exceptionMsg; + } else { + title = context.getString(R.string.flattrd_label); + subtext = context.getString(R.string.flattr_click_success_count, countSuccess) + "\n" + + context.getString(R.string.flattr_click_failure_count, countFailed); + } + + Notification notification = new NotificationCompat.Builder(context) + .setStyle(new NotificationCompat.BigTextStyle().bigText(subtext)) + .setContentIntent(contentIntent) + .setContentTitle(title) + .setTicker(title) + .setSmallIcon(R.drawable.stat_notify_sync_error) + .setOngoing(false) + .setAutoCancel(true) + .build(); + ((NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE)).notify(NOTIFICATION_ID, notification); + } + + + /** + * Starts the FlattrClickWorker as an AsyncTask. + */ + @TargetApi(11) + public void executeAsync() { + if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.GINGERBREAD_MR1) { + executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } else { + execute(); + } + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/asynctask/FlattrStatusFetcher.java b/core/src/main/java/de/danoeh/antennapod/core/asynctask/FlattrStatusFetcher.java new file mode 100644 index 000000000..c4aa76ac7 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/asynctask/FlattrStatusFetcher.java @@ -0,0 +1,47 @@ +package de.danoeh.antennapod.core.asynctask; + +import android.content.Context; +import android.util.Log; +import de.danoeh.antennapod.core.BuildConfig; +import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.core.util.flattr.FlattrUtils; +import org.shredzone.flattr4j.exception.FlattrException; +import org.shredzone.flattr4j.model.Flattr; + +import java.util.List; +import java.util.concurrent.ExecutionException; + +/** + * Fetch list of flattred things and flattr status in database in a background thread. + */ + +public class FlattrStatusFetcher extends Thread { + protected static final String TAG = "FlattrStatusFetcher"; + protected Context context; + + public FlattrStatusFetcher(Context context) { + super(); + this.context = context; + } + + @Override + public void run() { + if (BuildConfig.DEBUG) Log.d(TAG, "Starting background work: Retrieving Flattr status"); + + Thread.currentThread().setPriority(Thread.MIN_PRIORITY); + + try { + List flattredThings = FlattrUtils.retrieveFlattredThings(); + DBWriter.setFlattredStatus(context, flattredThings).get(); + } catch (FlattrException e) { + e.printStackTrace(); + Log.d(TAG, "flattrQueue exception retrieving list with flattred items " + e.getMessage()); + } catch (InterruptedException e) { + e.printStackTrace(); + } catch (ExecutionException e) { + e.printStackTrace(); + } + + if (BuildConfig.DEBUG) Log.d(TAG, "Finished background work: Retrieved Flattr status"); + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/asynctask/FlattrTokenFetcher.java b/core/src/main/java/de/danoeh/antennapod/core/asynctask/FlattrTokenFetcher.java new file mode 100644 index 000000000..2513d1abd --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/asynctask/FlattrTokenFetcher.java @@ -0,0 +1,92 @@ +package de.danoeh.antennapod.core.asynctask; + + +import android.annotation.SuppressLint; +import android.app.ProgressDialog; +import android.content.Context; +import android.net.Uri; +import android.os.AsyncTask; +import android.util.Log; + +import org.shredzone.flattr4j.exception.FlattrException; +import org.shredzone.flattr4j.oauth.AccessToken; +import org.shredzone.flattr4j.oauth.AndroidAuthenticator; + +import de.danoeh.antennapod.core.BuildConfig; +import de.danoeh.antennapod.core.ClientConfig; +import de.danoeh.antennapod.core.R; +import de.danoeh.antennapod.core.util.flattr.FlattrUtils; + +/** + * Fetches the access token in the background in order to avoid networkOnMainThread exception. + */ + +public class FlattrTokenFetcher extends AsyncTask { + private static final String TAG = "FlattrTokenFetcher"; + Context context; + AndroidAuthenticator auth; + AccessToken token; + Uri uri; + ProgressDialog dialog; + FlattrException exception; + + public FlattrTokenFetcher(Context context, AndroidAuthenticator auth, Uri uri) { + super(); + this.context = context; + this.auth = auth; + this.uri = uri; + } + + @Override + protected void onPostExecute(AccessToken result) { + if (result != null) { + FlattrUtils.storeToken(result); + } + dialog.dismiss(); + if (exception == null) { + ClientConfig.flattrCallbacks.handleFlattrAuthenticationSuccess(result); + } else { + FlattrUtils.showErrorDialog(context, exception.getMessage()); + } + } + + + @Override + protected void onPreExecute() { + super.onPreExecute(); + dialog = new ProgressDialog(context); + dialog.setMessage(context.getString(R.string.processing_label)); + dialog.setIndeterminate(true); + dialog.setCancelable(false); + dialog.show(); + } + + + @Override + protected AccessToken doInBackground(Void... params) { + try { + token = auth.fetchAccessToken(uri); + } catch (FlattrException e) { + e.printStackTrace(); + exception = e; + return null; + } + if (token != null) { + if (BuildConfig.DEBUG) Log.d(TAG, "Successfully got token"); + return token; + } else { + Log.w(TAG, "Flattr token was null"); + return null; + } + } + + @SuppressLint("NewApi") + public void executeAsync() { + if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.GINGERBREAD_MR1) { + executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } else { + execute(); + } + } + +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/asynctask/PicassoImageResource.java b/core/src/main/java/de/danoeh/antennapod/core/asynctask/PicassoImageResource.java new file mode 100644 index 000000000..c0d8049db --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/asynctask/PicassoImageResource.java @@ -0,0 +1,37 @@ +package de.danoeh.antennapod.core.asynctask; + +import android.net.Uri; + +/** + * Classes that implement this interface provide access to an image resource that can + * be loaded by the Picasso library. + */ +public interface PicassoImageResource { + + /** + * This scheme should be used by PicassoImageResources to + * indicate that the image Uri points to a file that is not an image + * (e.g. a media file). This workaround is needed so that the Picasso library + * loads these Uri with a Downloader instead of trying to load it directly. + *

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

+ * This instance should be used as long as no images from media files are loaded. + */ + public static synchronized Picasso getDefaultPicassoInstance(Context context) { + Validate.notNull(context); + if (defaultPicassoInstance == null) { + defaultPicassoInstance = new Picasso.Builder(context) + .indicatorsEnabled(DEBUG) + .loggingEnabled(DEBUG) + .downloader(new OkHttpDownloader(context)) + .executor(getExecutorService()) + .memoryCache(getMemoryCache(context)) + .listener(new Picasso.Listener() { + @Override + public void onImageLoadFailed(Picasso picasso, Uri uri, Exception e) { + Log.e(TAG, "Failed to load Uri:" + uri.toString()); + e.printStackTrace(); + } + }) + .build(); + } + return defaultPicassoInstance; + } + + /** + * Returns a Picasso instance that uses a MediaMetadataRetriever if the given Uri is a media file + * and a default OkHttpDownloader otherwise. + */ + public static synchronized Picasso getMediaMetadataPicassoInstance(Context context) { + Validate.notNull(context); + if (mediaMetadataPicassoInstance == null) { + mediaMetadataPicassoInstance = new Picasso.Builder(context) + .indicatorsEnabled(DEBUG) + .loggingEnabled(DEBUG) + .downloader(new MediaMetadataDownloader(context)) + .executor(getExecutorService()) + .memoryCache(getMemoryCache(context)) + .listener(new Picasso.Listener() { + @Override + public void onImageLoadFailed(Picasso picasso, Uri uri, Exception e) { + Log.e(TAG, "Failed to load Uri:" + uri.toString()); + e.printStackTrace(); + } + }) + .build(); + } + return mediaMetadataPicassoInstance; + } + + private static class MediaMetadataDownloader implements Downloader { + + private static final String TAG = "MediaMetadataDownloader"; + + private final OkHttpDownloader okHttpDownloader; + + public MediaMetadataDownloader(Context context) { + Validate.notNull(context); + okHttpDownloader = new OkHttpDownloader(context); + } + + @Override + public Response load(Uri uri, boolean b) throws IOException { + if (StringUtils.equals(uri.getScheme(), PicassoImageResource.SCHEME_MEDIA)) { + String type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(FilenameUtils.getExtension(uri.getLastPathSegment())); + if (StringUtils.startsWith(type, "image")) { + File imageFile = new File(uri.toString()); + return new Response(new BufferedInputStream(new FileInputStream(imageFile)), true, imageFile.length()); + } else { + MediaMetadataRetriever mmr = new MediaMetadataRetriever(); + mmr.setDataSource(uri.getPath()); + byte[] data = mmr.getEmbeddedPicture(); + mmr.release(); + + if (data != null) { + return new Response(new ByteArrayInputStream(data), true, data.length); + } else { + + // check for fallback Uri + String fallbackParam = uri.getQueryParameter(PicassoImageResource.PARAM_FALLBACK); + + if (fallbackParam != null) { + String fallback = Uri.decode(Uri.parse(fallbackParam).getPath()); + if (fallback != null) { + File imageFile = new File(fallback); + return new Response(new BufferedInputStream(new FileInputStream(imageFile)), true, imageFile.length()); + } + } + return null; + } + } + } + return okHttpDownloader.load(uri, b); + } + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/backup/OpmlBackupAgent.java b/core/src/main/java/de/danoeh/antennapod/core/backup/OpmlBackupAgent.java new file mode 100644 index 000000000..1535e2e9a --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/backup/OpmlBackupAgent.java @@ -0,0 +1,211 @@ +package de.danoeh.antennapod.core.backup; + +import android.app.backup.BackupAgentHelper; +import android.app.backup.BackupDataInputStream; +import android.app.backup.BackupDataOutput; +import android.app.backup.BackupHelper; +import android.content.Context; +import android.os.ParcelFileDescriptor; +import android.util.Log; + +import de.danoeh.antennapod.core.BuildConfig; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.ByteArrayOutputStream; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.io.Reader; +import java.io.Writer; +import java.math.BigInteger; +import java.security.DigestInputStream; +import java.security.DigestOutputStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; + +import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.core.opml.OpmlElement; +import de.danoeh.antennapod.core.opml.OpmlReader; +import de.danoeh.antennapod.core.opml.OpmlWriter; +import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.core.storage.DownloadRequestException; +import de.danoeh.antennapod.core.storage.DownloadRequester; +import de.danoeh.antennapod.core.util.LangUtils; + +public class OpmlBackupAgent extends BackupAgentHelper { + private static final String OPML_BACKUP_KEY = "opml"; + + @Override + public void onCreate() { + addHelper(OPML_BACKUP_KEY, new OpmlBackupHelper(this)); + } + + private static final void LOGD(String tag, String msg) { + if (BuildConfig.DEBUG && Log.isLoggable(tag, Log.DEBUG)) { + Log.d(tag, msg); + } + } + + private static final void LOGD(String tag, String msg, Throwable tr) { + if (BuildConfig.DEBUG && Log.isLoggable(tag, Log.DEBUG)) { + Log.d(tag, msg, tr); + } + } + + /** Class for backing up and restoring the OPML file. */ + private static class OpmlBackupHelper implements BackupHelper { + private static final String TAG = "OpmlBackupHelper"; + + private static final String OPML_ENTITY_KEY = "antennapod-feeds.opml"; + + private final Context mContext; + + /** Checksum of restored OPML file */ + private byte[] mChecksum; + + public OpmlBackupHelper(Context context) { + mContext = context; + } + + @Override + public void performBackup(ParcelFileDescriptor oldState, BackupDataOutput data, ParcelFileDescriptor newState) { + Log.d(TAG, "Performing backup"); + ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); + MessageDigest digester = null; + Writer writer; + + try { + digester = MessageDigest.getInstance("MD5"); + writer = new OutputStreamWriter(new DigestOutputStream(byteStream, digester), + LangUtils.UTF_8); + } catch (NoSuchAlgorithmException e) { + writer = new OutputStreamWriter(byteStream, LangUtils.UTF_8); + } + + try { + // Write OPML + new OpmlWriter().writeDocument(DBReader.getFeedList(mContext), writer); + + // Compare checksum of new and old file to see if we need to perform a backup at all + if (digester != null) { + byte[] newChecksum = digester.digest(); + LOGD(TAG, "New checksum: " + new BigInteger(1, newChecksum).toString(16)); + + // Get the old checksum + if (oldState != null) { + FileInputStream inState = new FileInputStream(oldState.getFileDescriptor()); + int len = inState.read(); + + if (len != -1) { + byte[] oldChecksum = new byte[len]; + inState.read(oldChecksum); + LOGD(TAG, "Old checksum: " + new BigInteger(1, oldChecksum).toString(16)); + + if (Arrays.equals(oldChecksum, newChecksum)) { + LOGD(TAG, "Checksums are the same; won't backup"); + return; + } + } + } + + writeNewStateDescription(newState, newChecksum); + } + + LOGD(TAG, "Backing up OPML"); + byte[] bytes = byteStream.toByteArray(); + data.writeEntityHeader(OPML_ENTITY_KEY, bytes.length); + data.writeEntityData(bytes, bytes.length); + } catch (IOException e) { + Log.e(TAG, "Error during backup", e); + } finally { + if (writer != null) { + try { + writer.close(); + } catch (IOException e) { + } + } + } + } + + @Override + public void restoreEntity(BackupDataInputStream data) { + LOGD(TAG, "Backup restore"); + + if (!OPML_ENTITY_KEY.equals(data.getKey())) { + LOGD(TAG, "Unknown entity key: " + data.getKey()); + return; + } + + MessageDigest digester = null; + Reader reader; + + try { + digester = MessageDigest.getInstance("MD5"); + reader = new InputStreamReader(new DigestInputStream(data, digester), + LangUtils.UTF_8); + } catch (NoSuchAlgorithmException e) { + reader = new InputStreamReader(data, LangUtils.UTF_8); + } + + try { + ArrayList opmlElements = new OpmlReader().readDocument(reader); + mChecksum = digester == null ? null : digester.digest(); + DownloadRequester downloader = DownloadRequester.getInstance(); + Date lastUpdated = new Date(); + + for (OpmlElement opmlElem : opmlElements) { + Feed feed = new Feed(opmlElem.getXmlUrl(), lastUpdated, opmlElem.getText()); + + try { + downloader.downloadFeed(mContext, feed); + } catch (DownloadRequestException e) { + LOGD(TAG, "Error while restoring/downloading feed", e); + } + } + } catch (XmlPullParserException e) { + Log.e(TAG, "Error while parsing the OPML file", e); + } catch (IOException e) { + Log.e(TAG, "Failed to restore OPML backup", e); + } finally { + if (reader != null) { + try { + reader.close(); + } catch (IOException e) { + } + } + } + } + + @Override + public void writeNewStateDescription(ParcelFileDescriptor newState) { + writeNewStateDescription(newState, mChecksum); + } + + /** + * Writes the new state description, which is the checksum of the OPML file. + * + * @param newState + * @param checksum + */ + private void writeNewStateDescription(ParcelFileDescriptor newState, byte[] checksum) { + if (checksum == null) { + return; + } + + try { + FileOutputStream outState = new FileOutputStream(newState.getFileDescriptor()); + outState.write(checksum.length); + outState.write(checksum); + outState.flush(); + outState.close(); + } catch (IOException e) { + Log.e(TAG, "Failed to write new state description", e); + } + } + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/dialog/ConfirmationDialog.java b/core/src/main/java/de/danoeh/antennapod/core/dialog/ConfirmationDialog.java new file mode 100644 index 000000000..ba1add895 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/dialog/ConfirmationDialog.java @@ -0,0 +1,64 @@ +package de.danoeh.antennapod.core.dialog; + +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.util.Log; +import de.danoeh.antennapod.core.BuildConfig; +import de.danoeh.antennapod.core.R; + +/** + * Creates an AlertDialog which asks the user to confirm something. Other + * classes can handle events like confirmation or cancellation. + */ +public abstract class ConfirmationDialog { + private static final String TAG = "ConfirmationDialog"; + + Context context; + int titleId; + int messageId; + + public ConfirmationDialog(Context context, int titleId, int messageId) { + this.context = context; + this.titleId = titleId; + this.messageId = messageId; + } + + public void onCancelButtonPressed(DialogInterface dialog) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Dialog was cancelled"); + dialog.dismiss(); + } + + public abstract void onConfirmButtonPressed(DialogInterface dialog); + + public final AlertDialog createNewDialog() { + AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setTitle(titleId); + builder.setMessage(messageId); + builder.setPositiveButton(R.string.confirm_label, + new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + onConfirmButtonPressed(dialog); + } + }); + builder.setNegativeButton(R.string.cancel_label, + new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + onCancelButtonPressed(dialog); + } + }); + builder.setOnCancelListener(new DialogInterface.OnCancelListener() { + + @Override + public void onCancel(DialogInterface dialog) { + onCancelButtonPressed(dialog); + } + }); + return builder.create(); + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/dialog/DownloadRequestErrorDialogCreator.java b/core/src/main/java/de/danoeh/antennapod/core/dialog/DownloadRequestErrorDialogCreator.java new file mode 100644 index 000000000..3d174bd8e --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/dialog/DownloadRequestErrorDialogCreator.java @@ -0,0 +1,30 @@ +package de.danoeh.antennapod.core.dialog; + +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import de.danoeh.antennapod.core.R; + +/** Creates Alert Dialogs if a DownloadRequestException has happened. */ +public class DownloadRequestErrorDialogCreator { + private DownloadRequestErrorDialogCreator() { + } + + public static void newRequestErrorDialog(Context context, + String errorMessage) { + AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setNeutralButton(android.R.string.ok, + new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.dismiss(); + } + }) + .setTitle(R.string.download_error_request_error) + .setMessage( + context.getString(R.string.download_request_error_dialog_message_prefix) + + errorMessage); + builder.create().show(); + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/Chapter.java b/core/src/main/java/de/danoeh/antennapod/core/feed/Chapter.java new file mode 100644 index 000000000..ce3352ed6 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/Chapter.java @@ -0,0 +1,55 @@ +package de.danoeh.antennapod.core.feed; + +public abstract class Chapter extends FeedComponent { + + /** Defines starting point in milliseconds. */ + protected long start; + protected String title; + protected String link; + + public Chapter() { + } + + public Chapter(long start) { + super(); + this.start = start; + } + + public Chapter(long start, String title, FeedItem item, String link) { + super(); + this.start = start; + this.title = title; + this.link = link; + } + + public abstract int getChapterType(); + + public long getStart() { + return start; + } + + public String getTitle() { + return title; + } + + public String getLink() { + return link; + } + + public void setStart(long start) { + this.start = start; + } + + public void setTitle(String title) { + this.title = title; + } + + public void setLink(String link) { + this.link = link; + } + + @Override + public String getHumanReadableIdentifier() { + return title; + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/EventDistributor.java b/core/src/main/java/de/danoeh/antennapod/core/feed/EventDistributor.java new file mode 100644 index 000000000..f8815dcf0 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/EventDistributor.java @@ -0,0 +1,140 @@ +package de.danoeh.antennapod.core.feed; + +import android.os.Handler; +import android.util.Log; + +import org.apache.commons.lang3.Validate; + +import de.danoeh.antennapod.core.BuildConfig; + +import java.util.AbstractQueue; +import java.util.Observable; +import java.util.Observer; +import java.util.concurrent.ConcurrentLinkedQueue; + +/** + * Notifies its observers about changes in the feed database. Observers can + * register by retrieving an instance of this class and registering an + * EventListener. When new events arrive, the EventDistributor will process the + * event queue in a handler that runs on the main thread. The observers will only + * be notified once if the event queue contains multiple elements. + * + * Events can be sent with the send* methods. + */ +public class EventDistributor extends Observable { + private static final String TAG = "EventDistributor"; + + public static final int FEED_LIST_UPDATE = 1; + public static final int UNREAD_ITEMS_UPDATE = 2; + public static final int QUEUE_UPDATE = 4; + public static final int DOWNLOADLOG_UPDATE = 8; + public static final int PLAYBACK_HISTORY_UPDATE = 16; + public static final int DOWNLOAD_QUEUED = 32; + public static final int DOWNLOAD_HANDLED = 64; + + private Handler handler; + private AbstractQueue 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/core/src/main/java/de/danoeh/antennapod/core/feed/Feed.java b/core/src/main/java/de/danoeh/antennapod/core/feed/Feed.java new file mode 100644 index 000000000..3f83ab8b6 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/Feed.java @@ -0,0 +1,445 @@ +package de.danoeh.antennapod.core.feed; + +import android.content.Context; +import android.net.Uri; + +import de.danoeh.antennapod.core.asynctask.PicassoImageResource; +import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.core.util.EpisodeFilter; +import de.danoeh.antennapod.core.util.flattr.FlattrStatus; +import de.danoeh.antennapod.core.util.flattr.FlattrThing; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +/** + * Data Object for a whole feed + * + * @author daniel + */ +public class Feed extends FeedFile implements FlattrThing, PicassoImageResource { + public static final int FEEDFILETYPE_FEED = 0; + public static final String TYPE_RSS2 = "rss"; + public static final String TYPE_RSS091 = "rss"; + public static final String TYPE_ATOM1 = "atom"; + + private String title; + /** + * Contains 'id'-element in Atom feed. + */ + private String feedIdentifier; + /** + * Link to the website. + */ + private String link; + private String description; + private String language; + /** + * Name of the author + */ + private String author; + private FeedImage image; + private List 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/core/src/main/java/de/danoeh/antennapod/core/feed/FeedComponent.java b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedComponent.java new file mode 100644 index 000000000..05115c1ea --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedComponent.java @@ -0,0 +1,66 @@ +package de.danoeh.antennapod.core.feed; + +/** + * Represents every possible component of a feed + * + * @author daniel + */ +public abstract class FeedComponent { + + protected long id; + + public FeedComponent() { + super(); + } + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + /** + * Update this FeedComponent's attributes with the attributes from another + * FeedComponent. This method should only update attributes which where read from + * the feed. + */ + public void updateFromOther(FeedComponent other) { + } + + /** + * Compare's this FeedComponent's attribute values with another FeedComponent's + * attribute values. This method will only compare attributes which were + * read from the feed. + * + * @return true if attribute values are different, false otherwise + */ + public boolean compareWithOther(FeedComponent other) { + return false; + } + + + /** + * Should return a non-null, human-readable String so that the item can be + * identified by the user. Can be title, download-url, etc. + */ + public abstract String getHumanReadableIdentifier(); + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + FeedComponent that = (FeedComponent) o; + + if (id != that.id) return false; + + return true; + } + + @Override + public int hashCode() { + return (int) (id ^ (id >>> 32)); + } +} \ No newline at end of file diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedFile.java b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedFile.java new file mode 100644 index 000000000..3dc58654b --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedFile.java @@ -0,0 +1,105 @@ +package de.danoeh.antennapod.core.feed; + +import java.io.File; + +/** + * Represents a component of a Feed that has to be downloaded + */ +public abstract class FeedFile extends FeedComponent { + + protected String file_url; + protected String download_url; + protected boolean downloaded; + + /** + * Creates a new FeedFile object. + * + * @param file_url The location of the FeedFile. If this is null, the downloaded-attribute + * will automatically be set to false. + * @param download_url The location where the FeedFile can be downloaded. + * @param downloaded true if the FeedFile has been downloaded, false otherwise. This parameter + * will automatically be interpreted as false if the file_url is null. + */ + public FeedFile(String file_url, String download_url, boolean downloaded) { + super(); + this.file_url = file_url; + this.download_url = download_url; + this.downloaded = (file_url != null) && downloaded; + } + + public FeedFile() { + this(null, null, false); + } + + public abstract int getTypeAsInt(); + + /** + * Update this FeedFile's attributes with the attributes from another + * FeedFile. This method should only update attributes which where read from + * the feed. + */ + public void updateFromOther(FeedFile other) { + super.updateFromOther(other); + this.download_url = other.download_url; + } + + /** + * Compare's this FeedFile's attribute values with another FeedFile's + * attribute values. This method will only compare attributes which were + * read from the feed. + * + * @return true if attribute values are different, false otherwise + */ + public boolean compareWithOther(FeedFile other) { + if (super.compareWithOther(other)) { + return true; + } + if (!download_url.equals(other.download_url)) { + return true; + } + return false; + } + + /** + * Returns true if the file exists at file_url. + */ + public boolean fileExists() { + if (file_url == null) { + return false; + } else { + File f = new File(file_url); + return f.exists(); + } + } + + public String getFile_url() { + return file_url; + } + + /** + * Changes the file_url of this FeedFile. Setting this value to + * null will also set the downloaded-attribute to false. + */ + public void setFile_url(String file_url) { + this.file_url = file_url; + if (file_url == null) { + downloaded = false; + } + } + + public String getDownload_url() { + return download_url; + } + + public void setDownload_url(String download_url) { + this.download_url = download_url; + } + + public boolean isDownloaded() { + return downloaded; + } + + public void setDownloaded(boolean downloaded) { + this.downloaded = downloaded; + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedImage.java b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedImage.java new file mode 100644 index 000000000..51605691d --- /dev/null +++ b/core/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/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItem.java b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItem.java new file mode 100644 index 000000000..8a513de43 --- /dev/null +++ b/core/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 java.util.Date; +import java.util.List; +import java.util.concurrent.Callable; + +import de.danoeh.antennapod.core.ClientConfig; +import de.danoeh.antennapod.core.asynctask.PicassoImageResource; +import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.core.util.ShownotesProvider; +import de.danoeh.antennapod.core.util.flattr.FlattrStatus; +import de.danoeh.antennapod.core.util.flattr.FlattrThing; + +/** + * Data Object for a XML message + * + * @author daniel + */ +public class FeedItem extends FeedComponent implements ShownotesProvider, FlattrThing, PicassoImageResource { + + /** + * The id/guid that can be found in the rss/atom feed. Might not be set. + */ + private String itemIdentifier; + private String title; + /** + * The description of a feeditem. + */ + private String description; + /** + * The content of the content-encoded tag of a feeditem. + */ + private String contentEncoded; + + private String link; + private Date pubDate; + private FeedMedia media; + + private Feed feed; + private long feedId; + + private boolean read; + private String paymentLink; + private FlattrStatus flattrStatus; + private List 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(ClientConfig.applicationCallbacks.getApplicationInstance(), 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/core/src/main/java/de/danoeh/antennapod/core/feed/FeedMedia.java b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedMedia.java new file mode 100644 index 000000000..37186ee79 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedMedia.java @@ -0,0 +1,410 @@ +package de.danoeh.antennapod.core.feed; + +import android.content.SharedPreferences; +import android.content.SharedPreferences.Editor; +import android.net.Uri; +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.Date; +import java.util.List; +import java.util.concurrent.Callable; + +import de.danoeh.antennapod.core.ClientConfig; +import de.danoeh.antennapod.core.preferences.PlaybackPreferences; +import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.core.util.ChapterUtils; +import de.danoeh.antennapod.core.util.playback.Playable; + +public class FeedMedia extends FeedFile implements Playable { + private static final String TAG = "FeedMedia"; + + public static final int FEEDFILETYPE_FEEDMEDIA = 2; + public static final int PLAYABLE_TYPE_FEEDMEDIA = 1; + + public static final String PREF_MEDIA_ID = "FeedMedia.PrefMediaId"; + public static final String PREF_FEED_ID = "FeedMedia.PrefFeedId"; + + private int duration; + private int position; // Current position in file + private int played_duration; // How many ms of this file have been played (for autoflattring) + private long size; // File size in Byte + private String mime_type; + private volatile FeedItem item; + private Date playbackCompletionDate; + + /* Used for loading item when restoring from parcel. */ + private long itemID; + + public FeedMedia(FeedItem i, String download_url, long size, + String mime_type) { + super(null, download_url, false); + this.item = i; + this.size = size; + this.mime_type = mime_type; + } + + public FeedMedia(long id, FeedItem item, int duration, int position, + long size, String mime_type, String file_url, String download_url, + boolean downloaded, Date playbackCompletionDate, int played_duration) { + super(file_url, download_url, downloaded); + this.id = id; + this.item = item; + this.duration = duration; + this.position = position; + this.played_duration = played_duration; + this.size = size; + this.mime_type = mime_type; + this.playbackCompletionDate = playbackCompletionDate == null + ? null : (Date) playbackCompletionDate.clone(); + } + + public FeedMedia(long id, FeedItem item) { + super(); + this.id = id; + this.item = item; + } + + @Override + public String getHumanReadableIdentifier() { + if (item != null && item.getTitle() != null) { + return item.getTitle(); + } else { + return download_url; + } + } + + /** + * Uses mimetype to determine the type of media. + */ + public MediaType getMediaType() { + if (mime_type == null || mime_type.isEmpty()) { + return MediaType.UNKNOWN; + } else { + if (mime_type.startsWith("audio")) { + return MediaType.AUDIO; + } else if (mime_type.startsWith("video")) { + return MediaType.VIDEO; + } else if (mime_type.equals("application/ogg")) { + return MediaType.AUDIO; + } + } + return MediaType.UNKNOWN; + } + + public void updateFromOther(FeedMedia other) { + super.updateFromOther(other); + if (other.size > 0) { + size = other.size; + } + if (other.mime_type != null) { + mime_type = other.mime_type; + } + } + + public boolean compareWithOther(FeedMedia other) { + if (super.compareWithOther(other)) { + return true; + } + if (other.mime_type != null) { + if (mime_type == null || !mime_type.equals(other.mime_type)) { + return true; + } + } + if (other.size > 0 && other.size != size) { + return true; + } + return false; + } + + /** + * Reads playback preferences to determine whether this FeedMedia object is + * currently being played. + */ + public boolean isPlaying() { + return PlaybackPreferences.getCurrentlyPlayingMedia() == FeedMedia.PLAYABLE_TYPE_FEEDMEDIA + && PlaybackPreferences.getCurrentlyPlayingFeedMediaId() == id; + } + + @Override + public int getTypeAsInt() { + return FEEDFILETYPE_FEEDMEDIA; + } + + public int getDuration() { + return duration; + } + + public void setDuration(int duration) { + this.duration = duration; + } + + public int getPlayedDuration() { + return played_duration; + } + + public void setPlayedDuration(int played_duration) { + this.played_duration = played_duration; + } + + public int getPosition() { + return position; + } + + public void setPosition(int position) { + this.position = position; + } + + public long getSize() { + return size; + } + + public void setSize(long size) { + this.size = size; + } + + public String getMime_type() { + return mime_type; + } + + public void setMime_type(String mime_type) { + this.mime_type = mime_type; + } + + public FeedItem getItem() { + return item; + } + + /** + * Sets the item object of this FeedMedia. If the given + * FeedItem object is not null, it's 'media'-attribute value + * will also be set to this media object. + */ + public void setItem(FeedItem item) { + this.item = item; + if (item != null && item.getMedia() != this) { + item.setMedia(this); + } + } + + public Date getPlaybackCompletionDate() { + return playbackCompletionDate == null + ? null : (Date) playbackCompletionDate.clone(); + } + + public void setPlaybackCompletionDate(Date playbackCompletionDate) { + this.playbackCompletionDate = playbackCompletionDate == null + ? null : (Date) playbackCompletionDate.clone(); + } + + public boolean isInProgress() { + return (this.position > 0); + } + + public FeedImage getImage() { + if (item != null) { + return (item.hasItemImageDownloaded()) ? item.getImage() : item.getFeed().getImage(); + } + return null; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeLong(id); + dest.writeLong(item.getId()); + + dest.writeInt(duration); + dest.writeInt(position); + dest.writeLong(size); + dest.writeString(mime_type); + dest.writeString(file_url); + dest.writeString(download_url); + dest.writeByte((byte) ((downloaded) ? 1 : 0)); + dest.writeLong((playbackCompletionDate != null) ? playbackCompletionDate.getTime() : 0); + dest.writeInt(played_duration); + } + + @Override + public void writeToPreferences(Editor prefEditor) { + prefEditor.putLong(PREF_FEED_ID, item.getFeed().getId()); + prefEditor.putLong(PREF_MEDIA_ID, id); + } + + @Override + public void loadMetadata() throws PlayableException { + if (item == null && itemID != 0) { + item = DBReader.getFeedItem(ClientConfig.applicationCallbacks.getApplicationInstance(), itemID); + } + } + + @Override + public void loadChapterMarks() { + if (getChapters() == null && !localFileAvailable()) { + ChapterUtils.loadChaptersFromStreamUrl(this); + if (getChapters() != null && item != null) { + DBWriter.setFeedItem(ClientConfig.applicationCallbacks.getApplicationInstance(), + item); + } + } + + } + + @Override + public String getEpisodeTitle() { + if (item == null) { + return null; + } + if (getItem().getTitle() != null) { + return getItem().getTitle(); + } else { + return getItem().getIdentifyingValue(); + } + } + + @Override + public List getChapters() { + if (item == null) { + return null; + } + return getItem().getChapters(); + } + + @Override + public String getWebsiteLink() { + if (item == null) { + return null; + } + return getItem().getLink(); + } + + @Override + public String getFeedTitle() { + if (item == null) { + return null; + } + return getItem().getFeed().getTitle(); + } + + @Override + public Object getIdentifier() { + return id; + } + + @Override + public String getLocalMediaUrl() { + return file_url; + } + + @Override + public String getStreamUrl() { + return download_url; + } + + @Override + public String getPaymentLink() { + if (item == null) { + return null; + } + return getItem().getPaymentLink(); + } + + @Override + public boolean localFileAvailable() { + return isDownloaded() && file_url != null; + } + + @Override + public boolean streamAvailable() { + return download_url != null; + } + + @Override + public void saveCurrentPosition(SharedPreferences pref, int newPosition) { + setPosition(newPosition); + DBWriter.setFeedMediaPlaybackInformation(ClientConfig.applicationCallbacks.getApplicationInstance(), this); + } + + @Override + public void onPlaybackStart() { + } + + @Override + public void onPlaybackCompleted() { + + } + + @Override + public int getPlayableType() { + return PLAYABLE_TYPE_FEEDMEDIA; + } + + @Override + public void setChapters(List chapters) { + getItem().setChapters(chapters); + } + + @Override + public Callable loadShownotes() { + return new Callable() { + @Override + public String call() throws Exception { + if (item == null) { + item = DBReader.getFeedItem( + ClientConfig.applicationCallbacks.getApplicationInstance(), itemID); + } + if (item.getContentEncoded() == null || item.getDescription() == null) { + DBReader.loadExtraInformationOfFeedItem( + ClientConfig.applicationCallbacks.getApplicationInstance(), item); + + } + return (item.getContentEncoded() != null) ? item.getContentEncoded() : item.getDescription(); + } + }; + } + + public static final Parcelable.Creator 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/core/src/main/java/de/danoeh/antennapod/core/feed/FeedPreferences.java b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedPreferences.java new file mode 100644 index 000000000..2f0304182 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedPreferences.java @@ -0,0 +1,89 @@ +package de.danoeh.antennapod.core.feed; + +import android.content.Context; +import de.danoeh.antennapod.core.storage.DBWriter; +import org.apache.commons.lang3.StringUtils; + +/** + * Contains preferences for a single feed. + */ +public class FeedPreferences { + + private long feedID; + private boolean autoDownload; + private String username; + private String password; + + public FeedPreferences(long feedID, boolean autoDownload, String username, String password) { + this.feedID = feedID; + this.autoDownload = autoDownload; + this.username = username; + this.password = password; + } + + + /** + * Compare another FeedPreferences with this one. The feedID and autoDownload attribute are excluded from the + * comparison. + * + * @return True if the two objects are different. + */ + public boolean compareWithOther(FeedPreferences other) { + if (other == null) + return true; + if (!StringUtils.equals(username, other.username)) { + return true; + } + if (!StringUtils.equals(password, other.password)) { + return true; + } + return false; + } + + /** + * Update this FeedPreferences object from another one. The feedID and autoDownload attributes are excluded + * from the update. + */ + public void updateFromOther(FeedPreferences other) { + if (other == null) + return; + this.username = other.username; + this.password = other.password; + } + + public long getFeedID() { + return feedID; + } + + public void setFeedID(long feedID) { + this.feedID = feedID; + } + + public boolean getAutoDownload() { + return autoDownload; + } + + public void setAutoDownload(boolean autoDownload) { + this.autoDownload = autoDownload; + } + + public void save(Context context) { + DBWriter.setFeedPreferences(context, this); + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/ID3Chapter.java b/core/src/main/java/de/danoeh/antennapod/core/feed/ID3Chapter.java new file mode 100644 index 000000000..f0ff03a93 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/ID3Chapter.java @@ -0,0 +1,36 @@ +package de.danoeh.antennapod.core.feed; + +public class ID3Chapter extends Chapter { + public static final int CHAPTERTYPE_ID3CHAPTER = 2; + + /** + * Identifies the chapter in its ID3 tag. This attribute does not have to be + * store in the DB and is only used for parsing. + */ + private String id3ID; + + public ID3Chapter(String id3ID, long start) { + super(start); + this.id3ID = id3ID; + } + + public ID3Chapter(long start, String title, FeedItem item, String link) { + super(start, title, item, link); + } + + @Override + public String toString() { + return "ID3Chapter [id3ID=" + id3ID + ", title=" + title + ", start=" + + start + ", url=" + link + "]"; + } + + @Override + public int getChapterType() { + return CHAPTERTYPE_ID3CHAPTER; + } + + public String getId3ID() { + return id3ID; + } + +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/MediaType.java b/core/src/main/java/de/danoeh/antennapod/core/feed/MediaType.java new file mode 100644 index 000000000..7b3cb829d --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/MediaType.java @@ -0,0 +1,5 @@ +package de.danoeh.antennapod.core.feed; + +public enum MediaType { + AUDIO, VIDEO, UNKNOWN +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/SearchResult.java b/core/src/main/java/de/danoeh/antennapod/core/feed/SearchResult.java new file mode 100644 index 000000000..9aa8d3170 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/SearchResult.java @@ -0,0 +1,34 @@ +package de.danoeh.antennapod.core.feed; + +public class SearchResult { + private FeedComponent component; + /** Additional information (e.g. where it was found) */ + private String subtitle; + /** Higher value means more importance */ + private int value; + + public SearchResult(FeedComponent component, int value, String subtitle) { + super(); + this.component = component; + this.value = value; + this.subtitle = subtitle; + } + + public FeedComponent getComponent() { + return component; + } + + public String getSubtitle() { + return subtitle; + } + + public void setSubtitle(String subtitle) { + this.subtitle = subtitle; + } + + public int getValue() { + return value; + } + + +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/SimpleChapter.java b/core/src/main/java/de/danoeh/antennapod/core/feed/SimpleChapter.java new file mode 100644 index 000000000..2dadd3ec8 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/SimpleChapter.java @@ -0,0 +1,25 @@ +package de.danoeh.antennapod.core.feed; + +public class SimpleChapter extends Chapter { + public static final int CHAPTERTYPE_SIMPLECHAPTER = 0; + + public SimpleChapter(long start, String title, FeedItem item, String link) { + super(start, title, item, link); + } + + @Override + public int getChapterType() { + return CHAPTERTYPE_SIMPLECHAPTER; + } + + public void updateFromOther(SimpleChapter other) { + super.updateFromOther(other); + start = other.start; + if (other.title != null) { + title = other.title; + } + if (other.link != null) { + link = other.link; + } + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/VorbisCommentChapter.java b/core/src/main/java/de/danoeh/antennapod/core/feed/VorbisCommentChapter.java new file mode 100644 index 000000000..5b54a2d59 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/VorbisCommentChapter.java @@ -0,0 +1,109 @@ +package de.danoeh.antennapod.core.feed; + +import de.danoeh.antennapod.core.util.vorbiscommentreader.VorbisCommentReaderException; + +import java.util.concurrent.TimeUnit; + +public class VorbisCommentChapter extends Chapter { + public static final int CHAPTERTYPE_VORBISCOMMENT_CHAPTER = 3; + + private static final int CHAPTERXXX_LENGTH = "chapterxxx".length(); + + private int vorbisCommentId; + + public VorbisCommentChapter(int vorbisCommentId) { + this.vorbisCommentId = vorbisCommentId; + } + + public VorbisCommentChapter(long start, String title, FeedItem item, + String link) { + super(start, title, item, link); + } + + @Override + public String toString() { + return "VorbisCommentChapter [id=" + id + ", title=" + title + + ", link=" + link + ", start=" + start + "]"; + } + + public static long getStartTimeFromValue(String value) + throws VorbisCommentReaderException { + String[] parts = value.split(":"); + if (parts.length >= 3) { + try { + long hours = TimeUnit.MILLISECONDS.convert( + Long.parseLong(parts[0]), TimeUnit.HOURS); + long minutes = TimeUnit.MILLISECONDS.convert( + Long.parseLong(parts[1]), TimeUnit.MINUTES); + if (parts[2].contains("-->")) { + parts[2] = parts[2].substring(0, parts[2].indexOf("-->")); + } + long seconds = TimeUnit.MILLISECONDS.convert( + ((long) Float.parseFloat(parts[2])), TimeUnit.SECONDS); + return hours + minutes + seconds; + } catch (NumberFormatException e) { + throw new VorbisCommentReaderException(e); + } + } else { + throw new VorbisCommentReaderException("Invalid time string"); + } + } + + /** + * Return the id of a vorbiscomment chapter from a string like CHAPTERxxx* + * + * @return the id of the chapter key or -1 if the id couldn't be read. + * @throws VorbisCommentReaderException + * */ + public static int getIDFromKey(String key) + throws VorbisCommentReaderException { + if (key.length() >= CHAPTERXXX_LENGTH) { // >= CHAPTERxxx + try { + String strId = key.substring(8, 10); + return Integer.parseInt(strId); + } catch (NumberFormatException e) { + throw new VorbisCommentReaderException(e); + } + } + throw new VorbisCommentReaderException("key is too short (" + key + ")"); + } + + /** + * Get the string that comes after 'CHAPTERxxx', for example 'name' or + * 'url'. + */ + public static String getAttributeTypeFromKey(String key) { + if (key.length() > CHAPTERXXX_LENGTH) { + return key.substring(CHAPTERXXX_LENGTH, key.length()); + } + return null; + } + + @Override + public int getChapterType() { + return CHAPTERTYPE_VORBISCOMMENT_CHAPTER; + } + + public void setTitle(String title) { + this.title = title; + } + + public void setLink(String link) { + this.link = link; + } + + public void setStart(long start) { + this.start = start; + } + + public int getVorbisCommentId() { + return vorbisCommentId; + } + + public void setVorbisCommentId(int vorbisCommentId) { + this.vorbisCommentId = vorbisCommentId; + } + + + +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetService.java b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetService.java new file mode 100644 index 000000000..117cbf96b --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetService.java @@ -0,0 +1,718 @@ +package de.danoeh.antennapod.core.gpoddernet; + +import org.apache.commons.lang3.Validate; +import org.apache.http.Header; +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.HttpStatus; +import org.apache.http.auth.AuthenticationException; +import org.apache.http.auth.UsernamePasswordCredentials; +import org.apache.http.client.ClientProtocolException; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpPut; +import org.apache.http.client.methods.HttpRequestBase; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.auth.BasicScheme; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; + +import de.danoeh.antennapod.core.gpoddernet.model.GpodnetDevice; +import de.danoeh.antennapod.core.gpoddernet.model.GpodnetPodcast; +import de.danoeh.antennapod.core.gpoddernet.model.GpodnetSubscriptionChange; +import de.danoeh.antennapod.core.gpoddernet.model.GpodnetTag; +import de.danoeh.antennapod.core.gpoddernet.model.GpodnetUploadChangesResponse; +import de.danoeh.antennapod.core.preferences.GpodnetPreferences; +import de.danoeh.antennapod.core.service.download.AntennapodHttpClient; + +/** + * Communicates with the gpodder.net service. + */ +public class GpodnetService { + + private static final String BASE_SCHEME = "https"; + + public static final String DEFAULT_BASE_HOST = "gpodder.net"; + private final String BASE_HOST; + + private final HttpClient httpClient; + + public GpodnetService() { + httpClient = AntennapodHttpClient.getHttpClient(); + BASE_HOST = GpodnetPreferences.getHostname(); + } + + /** + * Returns the [count] most used tags. + */ + public List 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/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetServiceAuthenticationException.java b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetServiceAuthenticationException.java new file mode 100644 index 000000000..8bd56218c --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetServiceAuthenticationException.java @@ -0,0 +1,21 @@ +package de.danoeh.antennapod.core.gpoddernet; + +public class GpodnetServiceAuthenticationException extends GpodnetServiceException { + + public GpodnetServiceAuthenticationException() { + super(); + } + + public GpodnetServiceAuthenticationException(String message, Throwable cause) { + super(message, cause); + } + + public GpodnetServiceAuthenticationException(String message) { + super(message); + } + + public GpodnetServiceAuthenticationException(Throwable cause) { + super(cause); + } + +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetServiceBadStatusCodeException.java b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetServiceBadStatusCodeException.java new file mode 100644 index 000000000..16f01f0f4 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetServiceBadStatusCodeException.java @@ -0,0 +1,12 @@ +package de.danoeh.antennapod.core.gpoddernet; + +public class GpodnetServiceBadStatusCodeException extends GpodnetServiceException { + int statusCode; + + public GpodnetServiceBadStatusCodeException(String message, int statusCode) { + super(message); + this.statusCode = statusCode; + } + + +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetServiceException.java b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetServiceException.java new file mode 100644 index 000000000..ce704f7e3 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetServiceException.java @@ -0,0 +1,19 @@ +package de.danoeh.antennapod.core.gpoddernet; + +public class GpodnetServiceException extends Exception { + + public GpodnetServiceException() { + } + + public GpodnetServiceException(String message) { + super(message); + } + + public GpodnetServiceException(Throwable cause) { + super(cause); + } + + public GpodnetServiceException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetDevice.java b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetDevice.java new file mode 100644 index 000000000..4885a243a --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetDevice.java @@ -0,0 +1,72 @@ +package de.danoeh.antennapod.core.gpoddernet.model; + +import org.apache.commons.lang3.Validate; + +public class GpodnetDevice { + + private String id; + private String caption; + private DeviceType type; + private int subscriptions; + + public GpodnetDevice(String id, String caption, String type, + int subscriptions) { + Validate.notNull(id); + + this.id = id; + this.caption = caption; + this.type = DeviceType.fromString(type); + this.subscriptions = subscriptions; + } + + @Override + public String toString() { + return "GpodnetDevice [id=" + id + ", caption=" + caption + ", type=" + + type + ", subscriptions=" + subscriptions + "]"; + } + + public static enum DeviceType { + DESKTOP, LAPTOP, MOBILE, SERVER, OTHER; + + static DeviceType fromString(String s) { + if (s == null) { + return OTHER; + } + + if (s.equals("desktop")) { + return DESKTOP; + } else if (s.equals("laptop")) { + return LAPTOP; + } else if (s.equals("mobile")) { + return MOBILE; + } else if (s.equals("server")) { + return SERVER; + } else { + return OTHER; + } + } + + @Override + public String toString() { + return super.toString().toLowerCase(); + } + + } + + public String getId() { + return id; + } + + public String getCaption() { + return caption; + } + + public DeviceType getType() { + return type; + } + + public int getSubscriptions() { + return subscriptions; + } + +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetPodcast.java b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetPodcast.java new file mode 100644 index 000000000..afebf66ac --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetPodcast.java @@ -0,0 +1,65 @@ +package de.danoeh.antennapod.core.gpoddernet.model; + +import org.apache.commons.lang3.Validate; + +public class GpodnetPodcast { + private String url; + private String title; + private String description; + private int subscribers; + private String logoUrl; + private String website; + private String mygpoLink; + + public GpodnetPodcast(String url, String title, String description, + int subscribers, String logoUrl, String website, String mygpoLink) { + Validate.notNull(url); + Validate.notNull(title); + Validate.notNull(description); + + this.url = url; + this.title = title; + this.description = description; + this.subscribers = subscribers; + this.logoUrl = logoUrl; + this.website = website; + this.mygpoLink = mygpoLink; + } + + @Override + public String toString() { + return "GpodnetPodcast [url=" + url + ", title=" + title + + ", description=" + description + ", subscribers=" + + subscribers + ", logoUrl=" + logoUrl + ", website=" + website + + ", mygpoLink=" + mygpoLink + "]"; + } + + public String getUrl() { + return url; + } + + public String getTitle() { + return title; + } + + public String getDescription() { + return description; + } + + public int getSubscribers() { + return subscribers; + } + + public String getLogoUrl() { + return logoUrl; + } + + public String getWebsite() { + return website; + } + + public String getMygpoLink() { + return mygpoLink; + } + +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetSubscriptionChange.java b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetSubscriptionChange.java new file mode 100644 index 000000000..a5cb8c0f0 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetSubscriptionChange.java @@ -0,0 +1,41 @@ +package de.danoeh.antennapod.core.gpoddernet.model; + +import org.apache.commons.lang3.Validate; + +import java.util.List; + +public class GpodnetSubscriptionChange { + private List 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/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetTag.java b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetTag.java new file mode 100644 index 000000000..7178f4be5 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetTag.java @@ -0,0 +1,46 @@ +package de.danoeh.antennapod.core.gpoddernet.model; + +import org.apache.commons.lang3.Validate; + +import java.util.Comparator; + +public class GpodnetTag { + + private String name; + private int usage; + + public GpodnetTag(String name, int usage) { + Validate.notNull(name); + + this.name = name; + this.usage = usage; + } + + public GpodnetTag(String name) { + super(); + this.name = name; + } + + @Override + public String toString() { + return "GpodnetTag [name=" + name + ", usage=" + usage + "]"; + } + + public String getName() { + return name; + } + + public int getUsage() { + return usage; + } + + public static class UsageComparator implements Comparator { + + @Override + public int compare(GpodnetTag o1, GpodnetTag o2) { + return o1.usage - o2.usage; + } + + } + +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetUploadChangesResponse.java b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetUploadChangesResponse.java new file mode 100644 index 000000000..5a37efa5e --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetUploadChangesResponse.java @@ -0,0 +1,56 @@ +package de.danoeh.antennapod.core.gpoddernet.model; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.HashMap; +import java.util.Map; + +/** + * Object returned by {@link de.danoeh.antennapod.core.gpoddernet.GpodnetService} in uploadChanges method. + */ +public class GpodnetUploadChangesResponse { + + /** + * timestamp/ID that can be used for requesting changes since this upload. + */ + public final long timestamp; + + /** + * URLs that should be updated. The key of the map is the original URL, the value of the map + * is the sanitized URL. + */ + public final Map 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/core/src/main/java/de/danoeh/antennapod/core/opml/OpmlElement.java b/core/src/main/java/de/danoeh/antennapod/core/opml/OpmlElement.java new file mode 100644 index 000000000..8d0a4a842 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/opml/OpmlElement.java @@ -0,0 +1,46 @@ +package de.danoeh.antennapod.core.opml; + +/** Represents a single feed in an OPML file. */ +public class OpmlElement { + private String text; + private String xmlUrl; + private String htmlUrl; + private String type; + + public OpmlElement() { + + } + + public String getText() { + return text; + } + + public void setText(String text) { + this.text = text; + } + + public String getXmlUrl() { + return xmlUrl; + } + + public void setXmlUrl(String xmlUrl) { + this.xmlUrl = xmlUrl; + } + + public String getHtmlUrl() { + return htmlUrl; + } + + public void setHtmlUrl(String htmlUrl) { + this.htmlUrl = htmlUrl; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/opml/OpmlReader.java b/core/src/main/java/de/danoeh/antennapod/core/opml/OpmlReader.java new file mode 100644 index 000000000..775129d09 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/opml/OpmlReader.java @@ -0,0 +1,87 @@ +package de.danoeh.antennapod.core.opml; + +import android.util.Log; +import de.danoeh.antennapod.core.BuildConfig; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlPullParserFactory; + +import java.io.IOException; +import java.io.Reader; +import java.util.ArrayList; + +/** Reads OPML documents. */ +public class OpmlReader { + private static final String TAG = "OpmlReader"; + + // ATTRIBUTES + private boolean isInOpml = false; + private ArrayList 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/core/src/main/java/de/danoeh/antennapod/core/opml/OpmlSymbols.java b/core/src/main/java/de/danoeh/antennapod/core/opml/OpmlSymbols.java new file mode 100644 index 000000000..2b831ca2a --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/opml/OpmlSymbols.java @@ -0,0 +1,21 @@ +package de.danoeh.antennapod.core.opml; + +/** Contains symbols for reading and writing OPML documents. */ +public final class OpmlSymbols { + + public static final String OPML = "opml"; + public static final String BODY = "body"; + public static final String OUTLINE = "outline"; + public static final String TEXT = "text"; + public static final String XMLURL = "xmlUrl"; + public static final String HTMLURL = "htmlUrl"; + public static final String TYPE = "type"; + public static final String VERSION = "version"; + public static final String HEAD = "head"; + public static final String TITLE = "title"; + + private OpmlSymbols() { + + } + +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/opml/OpmlWriter.java b/core/src/main/java/de/danoeh/antennapod/core/opml/OpmlWriter.java new file mode 100644 index 000000000..641190f62 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/opml/OpmlWriter.java @@ -0,0 +1,65 @@ +package de.danoeh.antennapod.core.opml; + +import android.util.Log; +import android.util.Xml; +import de.danoeh.antennapod.core.BuildConfig; +import de.danoeh.antennapod.core.feed.Feed; +import org.xmlpull.v1.XmlSerializer; + +import java.io.IOException; +import java.io.Writer; +import java.util.List; + +/** Writes OPML documents. */ +public class OpmlWriter { + private static final String TAG = "OpmlWriter"; + private static final String ENCODING = "UTF-8"; + private static final String OPML_VERSION = "2.0"; + private static final String OPML_TITLE = "AntennaPod Subscriptions"; + + /** + * Takes a list of feeds and a writer and writes those into an OPML + * document. + * + * @throws IOException + * @throws IllegalStateException + * @throws IllegalArgumentException + */ + public void writeDocument(List feeds, Writer writer) + throws IllegalArgumentException, IllegalStateException, IOException { + if (BuildConfig.DEBUG) + Log.d(TAG, "Starting to write document"); + XmlSerializer xs = Xml.newSerializer(); + xs.setOutput(writer); + + xs.startDocument(ENCODING, false); + xs.startTag(null, OpmlSymbols.OPML); + xs.attribute(null, OpmlSymbols.VERSION, OPML_VERSION); + + xs.startTag(null, OpmlSymbols.HEAD); + xs.startTag(null, OpmlSymbols.TITLE); + xs.text(OPML_TITLE); + xs.endTag(null, OpmlSymbols.TITLE); + xs.endTag(null, OpmlSymbols.HEAD); + + xs.startTag(null, OpmlSymbols.BODY); + for (Feed feed : feeds) { + xs.startTag(null, OpmlSymbols.OUTLINE); + xs.attribute(null, OpmlSymbols.TEXT, feed.getTitle()); + xs.attribute(null, OpmlSymbols.TITLE, feed.getTitle()); + if (feed.getType() != null) { + xs.attribute(null, OpmlSymbols.TYPE, feed.getType()); + } + xs.attribute(null, OpmlSymbols.XMLURL, feed.getDownload_url()); + if (feed.getLink() != null) { + xs.attribute(null, OpmlSymbols.HTMLURL, feed.getLink()); + } + xs.endTag(null, OpmlSymbols.OUTLINE); + } + xs.endTag(null, OpmlSymbols.BODY); + xs.endTag(null, OpmlSymbols.OPML); + xs.endDocument(); + if (BuildConfig.DEBUG) + Log.d(TAG, "Finished writing document"); + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/preferences/GpodnetPreferences.java b/core/src/main/java/de/danoeh/antennapod/core/preferences/GpodnetPreferences.java new file mode 100644 index 000000000..af04df017 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/preferences/GpodnetPreferences.java @@ -0,0 +1,247 @@ +package de.danoeh.antennapod.core.preferences; + +import android.content.Context; +import android.content.SharedPreferences; +import android.util.Log; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.locks.ReentrantLock; + +import de.danoeh.antennapod.core.BuildConfig; +import de.danoeh.antennapod.core.ClientConfig; +import de.danoeh.antennapod.core.gpoddernet.GpodnetService; +import de.danoeh.antennapod.core.service.GpodnetSyncService; + +/** + * Manages preferences for accessing gpodder.net service + */ +public class GpodnetPreferences { + + private static final String TAG = "GpodnetPreferences"; + + private static final String PREF_NAME = "gpodder.net"; + public static final String PREF_GPODNET_USERNAME = "de.danoeh.antennapod.preferences.gpoddernet.username"; + public static final String PREF_GPODNET_PASSWORD = "de.danoeh.antennapod.preferences.gpoddernet.password"; + public static final String PREF_GPODNET_DEVICEID = "de.danoeh.antennapod.preferences.gpoddernet.deviceID"; + public static final String PREF_GPODNET_HOSTNAME = "prefGpodnetHostname"; + + + public static final String PREF_LAST_SYNC_TIMESTAMP = "de.danoeh.antennapod.preferences.gpoddernet.last_sync_timestamp"; + public static final String PREF_SYNC_ADDED = "de.danoeh.antennapod.preferences.gpoddernet.sync_added"; + public static final String PREF_SYNC_REMOVED = "de.danoeh.antennapod.preferences.gpoddernet.sync_removed"; + + private static String username; + private static String password; + private static String deviceID; + private static String hostname; + + private static ReentrantLock feedListLock = new ReentrantLock(); + private static Set 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 ClientConfig.applicationCallbacks.getApplicationInstance().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); + } + + private static synchronized void ensurePreferencesLoaded() { + if (!preferencesLoaded) { + SharedPreferences prefs = getPreferences(); + username = prefs.getString(PREF_GPODNET_USERNAME, null); + password = prefs.getString(PREF_GPODNET_PASSWORD, null); + deviceID = prefs.getString(PREF_GPODNET_DEVICEID, null); + lastSyncTimestamp = prefs.getLong(PREF_LAST_SYNC_TIMESTAMP, 0); + addedFeeds = readListFromString(prefs.getString(PREF_SYNC_ADDED, "")); + removedFeeds = readListFromString(prefs.getString(PREF_SYNC_REMOVED, "")); + hostname = checkGpodnetHostname(prefs.getString(PREF_GPODNET_HOSTNAME, GpodnetService.DEFAULT_BASE_HOST)); + + preferencesLoaded = true; + } + } + + private static void writePreference(String key, String value) { + SharedPreferences.Editor editor = getPreferences().edit(); + editor.putString(key, value); + editor.commit(); + } + + private static void writePreference(String key, long value) { + SharedPreferences.Editor editor = getPreferences().edit(); + editor.putLong(key, value); + editor.commit(); + } + + private static void writePreference(String key, Collection value) { + SharedPreferences.Editor editor = getPreferences().edit(); + editor.putString(key, writeListToString(value)); + editor.commit(); + } + + public static String getUsername() { + ensurePreferencesLoaded(); + return username; + } + + public static void setUsername(String username) { + GpodnetPreferences.username = username; + writePreference(PREF_GPODNET_USERNAME, username); + } + + public static String getPassword() { + ensurePreferencesLoaded(); + return password; + } + + public static void setPassword(String password) { + GpodnetPreferences.password = password; + writePreference(PREF_GPODNET_PASSWORD, password); + } + + public static String getDeviceID() { + ensurePreferencesLoaded(); + return deviceID; + } + + public static void setDeviceID(String deviceID) { + GpodnetPreferences.deviceID = deviceID; + writePreference(PREF_GPODNET_DEVICEID, deviceID); + } + + public static long getLastSyncTimestamp() { + ensurePreferencesLoaded(); + return lastSyncTimestamp; + } + + public static void setLastSyncTimestamp(long lastSyncTimestamp) { + GpodnetPreferences.lastSyncTimestamp = lastSyncTimestamp; + writePreference(PREF_LAST_SYNC_TIMESTAMP, lastSyncTimestamp); + } + + public static String getHostname() { + ensurePreferencesLoaded(); + return hostname; + } + + public static void setHostname(String value) { + value = checkGpodnetHostname(value); + if (!value.equals(hostname)) { + logout(); + writePreference(PREF_GPODNET_HOSTNAME, value); + hostname = value; + } + } + + public static void addAddedFeed(String feed) { + ensurePreferencesLoaded(); + feedListLock.lock(); + if (addedFeeds.add(feed)) { + writePreference(PREF_SYNC_ADDED, addedFeeds); + } + if (removedFeeds.remove(feed)) { + writePreference(PREF_SYNC_REMOVED, removedFeeds); + } + feedListLock.unlock(); + GpodnetSyncService.sendSyncIntent(ClientConfig.applicationCallbacks.getApplicationInstance()); + } + + public static void addRemovedFeed(String feed) { + ensurePreferencesLoaded(); + feedListLock.lock(); + if (removedFeeds.add(feed)) { + writePreference(PREF_SYNC_REMOVED, removedFeeds); + } + if (addedFeeds.remove(feed)) { + writePreference(PREF_SYNC_ADDED, addedFeeds); + } + feedListLock.unlock(); + GpodnetSyncService.sendSyncIntent(ClientConfig.applicationCallbacks.getApplicationInstance()); + } + + public static Set 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/core/src/main/java/de/danoeh/antennapod/core/preferences/PlaybackPreferences.java b/core/src/main/java/de/danoeh/antennapod/core/preferences/PlaybackPreferences.java new file mode 100644 index 000000000..d88543f73 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/preferences/PlaybackPreferences.java @@ -0,0 +1,146 @@ +package de.danoeh.antennapod.core.preferences; + +import android.content.Context; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; +import android.util.Log; + +import org.apache.commons.lang3.Validate; + +import de.danoeh.antennapod.core.BuildConfig; + +/** + * Provides access to preferences set by the playback service. A private + * instance of this class must first be instantiated via createInstance() or + * otherwise every public method will throw an Exception when called. + */ +public class PlaybackPreferences implements + SharedPreferences.OnSharedPreferenceChangeListener { + private static final String TAG = "PlaybackPreferences"; + + /** + * Contains the feed id of the currently playing item if it is a FeedMedia + * object. + */ + public static final String PREF_CURRENTLY_PLAYING_FEED_ID = "de.danoeh.antennapod.preferences.lastPlayedFeedId"; + + /** + * Contains the id of the currently playing FeedMedia object or + * NO_MEDIA_PLAYING if the currently playing media is no FeedMedia object. + */ + public static final String PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID = "de.danoeh.antennapod.preferences.lastPlayedFeedMediaId"; + + /** + * Type of the media object that is currently being played. This preference + * is set to NO_MEDIA_PLAYING after playback has been completed and is set + * as soon as the 'play' button is pressed. + */ + public static final String PREF_CURRENTLY_PLAYING_MEDIA = "de.danoeh.antennapod.preferences.currentlyPlayingMedia"; + + /** True if last played media was streamed. */ + public static final String PREF_CURRENT_EPISODE_IS_STREAM = "de.danoeh.antennapod.preferences.lastIsStream"; + + /** True if last played media was a video. */ + public static final String PREF_CURRENT_EPISODE_IS_VIDEO = "de.danoeh.antennapod.preferences.lastIsVideo"; + + /** Value of PREF_CURRENTLY_PLAYING_MEDIA if no media is playing. */ + public static final long NO_MEDIA_PLAYING = -1; + + private long currentlyPlayingFeedId; + private long currentlyPlayingFeedMediaId; + private long currentlyPlayingMedia; + private boolean currentEpisodeIsStream; + private boolean currentEpisodeIsVideo; + + private static PlaybackPreferences instance; + private Context context; + + private PlaybackPreferences(Context context) { + this.context = context; + loadPreferences(); + } + + /** + * Sets up the UserPreferences class. + * + * @throws IllegalArgumentException + * if context is null + * */ + public static void createInstance(Context context) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Creating new instance of UserPreferences"); + Validate.notNull(context); + + instance = new PlaybackPreferences(context); + + PreferenceManager.getDefaultSharedPreferences(context) + .registerOnSharedPreferenceChangeListener(instance); + } + + private void loadPreferences() { + SharedPreferences sp = PreferenceManager + .getDefaultSharedPreferences(context); + currentlyPlayingFeedId = sp.getLong(PREF_CURRENTLY_PLAYING_FEED_ID, -1); + currentlyPlayingFeedMediaId = sp.getLong( + PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID, NO_MEDIA_PLAYING); + currentlyPlayingMedia = sp.getLong(PREF_CURRENTLY_PLAYING_MEDIA, + NO_MEDIA_PLAYING); + currentEpisodeIsStream = sp.getBoolean(PREF_CURRENT_EPISODE_IS_STREAM, true); + currentEpisodeIsVideo = sp.getBoolean(PREF_CURRENT_EPISODE_IS_VIDEO, false); + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences sp, String key) { + if (key.equals(PREF_CURRENTLY_PLAYING_FEED_ID)) { + currentlyPlayingFeedId = sp.getLong(PREF_CURRENTLY_PLAYING_FEED_ID, + -1); + + } else if (key.equals(PREF_CURRENTLY_PLAYING_MEDIA)) { + currentlyPlayingMedia = sp + .getLong(PREF_CURRENTLY_PLAYING_MEDIA, -1); + + } else if (key.equals(PREF_CURRENT_EPISODE_IS_STREAM)) { + currentEpisodeIsStream = sp.getBoolean(PREF_CURRENT_EPISODE_IS_STREAM, true); + + } else if (key.equals(PREF_CURRENT_EPISODE_IS_VIDEO)) { + currentEpisodeIsVideo = sp.getBoolean(PREF_CURRENT_EPISODE_IS_VIDEO, false); + + } else if (key.equals(PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID)) { + currentlyPlayingFeedMediaId = sp.getLong( + PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID, NO_MEDIA_PLAYING); + } + } + + private static void instanceAvailable() { + if (instance == null) { + throw new IllegalStateException( + "UserPreferences was used before being set up"); + } + } + + + public static long getLastPlayedFeedId() { + instanceAvailable(); + return instance.currentlyPlayingFeedId; + } + + public static long getCurrentlyPlayingMedia() { + instanceAvailable(); + return instance.currentlyPlayingMedia; + } + + public static long getCurrentlyPlayingFeedMediaId() { + return instance.currentlyPlayingFeedMediaId; + } + + public static boolean getCurrentEpisodeIsStream() { + instanceAvailable(); + return instance.currentEpisodeIsStream; + } + + public static boolean getCurrentEpisodeIsVideo() { + instanceAvailable(); + return instance.currentEpisodeIsVideo; + } + +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/preferences/UserPreferences.java b/core/src/main/java/de/danoeh/antennapod/core/preferences/UserPreferences.java new file mode 100644 index 000000000..5cac4837d --- /dev/null +++ b/core/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.core.BuildConfig; +import de.danoeh.antennapod.core.R; +import de.danoeh.antennapod.core.receiver.FeedUpdateReceiver; + +/** + * Provides access to preferences set by the user in the settings screen. A + * private instance of this class must first be instantiated via + * createInstance() or otherwise every public method will throw an Exception + * when called. + */ +public class UserPreferences implements + SharedPreferences.OnSharedPreferenceChangeListener { + public static final String IMPORT_DIR = "import/"; + private static final String TAG = "UserPreferences"; + + public static final String PREF_PAUSE_ON_HEADSET_DISCONNECT = "prefPauseOnHeadsetDisconnect"; + public static final String PREF_FOLLOW_QUEUE = "prefFollowQueue"; + public static final String PREF_DOWNLOAD_MEDIA_ON_WIFI_ONLY = "prefDownloadMediaOnWifiOnly"; + public static final String PREF_UPDATE_INTERVAL = "prefAutoUpdateIntervall"; + public static final String PREF_MOBILE_UPDATE = "prefMobileUpdate"; + public static final String PREF_DISPLAY_ONLY_EPISODES = "prefDisplayOnlyEpisodes"; + public static final String PREF_AUTO_DELETE = "prefAutoDelete"; + public static final String PREF_AUTO_FLATTR = "pref_auto_flattr"; + public static final String PREF_AUTO_FLATTR_PLAYED_DURATION_THRESHOLD = "prefAutoFlattrPlayedDurationThreshold"; + public static final String PREF_THEME = "prefTheme"; + public static final String PREF_DATA_FOLDER = "prefDataFolder"; + public static final String PREF_ENABLE_AUTODL = "prefEnableAutoDl"; + public static final String PREF_ENABLE_AUTODL_WIFI_FILTER = "prefEnableAutoDownloadWifiFilter"; + private static final String PREF_AUTODL_SELECTED_NETWORKS = "prefAutodownloadSelectedNetworks"; + public static final String PREF_EPISODE_CACHE_SIZE = "prefEpisodeCacheSize"; + private static final String PREF_PLAYBACK_SPEED = "prefPlaybackSpeed"; + private static final String PREF_PLAYBACK_SPEED_ARRAY = "prefPlaybackSpeedArray"; + public static final String PREF_PAUSE_PLAYBACK_FOR_FOCUS_LOSS = "prefPauseForFocusLoss"; + private static final String PREF_SEEK_DELTA_SECS = "prefSeekDeltaSecs"; + + // 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/core/src/main/java/de/danoeh/antennapod/core/receiver/AlarmUpdateReceiver.java b/core/src/main/java/de/danoeh/antennapod/core/receiver/AlarmUpdateReceiver.java new file mode 100644 index 000000000..0777a7a2e --- /dev/null +++ b/core/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.core.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/core/src/main/java/de/danoeh/antennapod/core/receiver/ConnectivityActionReceiver.java b/core/src/main/java/de/danoeh/antennapod/core/receiver/ConnectivityActionReceiver.java new file mode 100644 index 000000000..6a9a4166a --- /dev/null +++ b/core/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.core.BuildConfig; +import de.danoeh.antennapod.core.storage.DBTasks; +import de.danoeh.antennapod.core.storage.DownloadRequester; +import de.danoeh.antennapod.core.util.NetworkUtils; + +public class ConnectivityActionReceiver extends BroadcastReceiver { + private static final String TAG = "ConnectivityActionReceiver"; + + @Override + public void onReceive(final Context context, Intent intent) { + if (StringUtils.equals(intent.getAction(), ConnectivityManager.CONNECTIVITY_ACTION)) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Received intent"); + + if (NetworkUtils.autodownloadNetworkAvailable(context)) { + if (BuildConfig.DEBUG) + Log.d(TAG, + "auto-dl network available, starting auto-download"); + DBTasks.autodownloadUndownloadedItems(context); + } else { // if new network is Wi-Fi, finish ongoing downloads, + // otherwise cancel all downloads + ConnectivityManager cm = (ConnectivityManager) context + .getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo ni = cm.getActiveNetworkInfo(); + if (ni == null || ni.getType() != ConnectivityManager.TYPE_WIFI) { + if (BuildConfig.DEBUG) + Log.i(TAG, + "Device is no longer connected to Wi-Fi. Cancelling ongoing downloads"); + DownloadRequester.getInstance().cancelAllDownloads(context); + } + + } + } + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/receiver/FeedUpdateReceiver.java b/core/src/main/java/de/danoeh/antennapod/core/receiver/FeedUpdateReceiver.java new file mode 100644 index 000000000..6ce30763d --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/receiver/FeedUpdateReceiver.java @@ -0,0 +1,46 @@ +package de.danoeh.antennapod.core.receiver; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.util.Log; + +import org.apache.commons.lang3.StringUtils; + +import de.danoeh.antennapod.core.BuildConfig; +import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.core.storage.DBTasks; + +/** Refreshes all feeds when it receives an intent */ +public class FeedUpdateReceiver extends BroadcastReceiver { + private static final String TAG = "FeedUpdateReceiver"; + public static final String ACTION_REFRESH_FEEDS = "de.danoeh.antennapod.feedupdatereceiver.refreshFeeds"; + + @Override + public void onReceive(Context context, Intent intent) { + if (StringUtils.equals(intent.getAction(), ACTION_REFRESH_FEEDS)) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Received intent"); + boolean mobileUpdate = UserPreferences.isAllowMobileUpdate(); + if (mobileUpdate || connectedToWifi(context)) { + DBTasks.refreshExpiredFeeds(context); + } else { + if (BuildConfig.DEBUG) + Log.d(TAG, + "Blocking automatic update: no wifi available / no mobile updates allowed"); + } + } + } + + private boolean connectedToWifi(Context context) { + ConnectivityManager connManager = (ConnectivityManager) context + .getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo mWifi = connManager + .getNetworkInfo(ConnectivityManager.TYPE_WIFI); + + return mWifi.isConnected(); + } + +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/receiver/MediaButtonReceiver.java b/core/src/main/java/de/danoeh/antennapod/core/receiver/MediaButtonReceiver.java new file mode 100644 index 000000000..a900248d2 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/receiver/MediaButtonReceiver.java @@ -0,0 +1,32 @@ +package de.danoeh.antennapod.core.receiver; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.util.Log; +import android.view.KeyEvent; +import de.danoeh.antennapod.core.BuildConfig; +import de.danoeh.antennapod.core.service.playback.PlaybackService; + +/** Receives media button events. */ +public class MediaButtonReceiver extends BroadcastReceiver { + private static final String TAG = "MediaButtonReceiver"; + public static final String EXTRA_KEYCODE = "de.danoeh.antennapod.core.service.extra.MediaButtonReceiver.KEYCODE"; + + public static final String NOTIFY_BUTTON_RECEIVER = "de.danoeh.antennapod.NOTIFY_BUTTON_RECEIVER"; + + @Override + public void onReceive(Context context, Intent intent) { + if (BuildConfig.DEBUG) Log.d(TAG, "Received intent"); + KeyEvent event = (KeyEvent) intent.getExtras().get( + Intent.EXTRA_KEY_EVENT); + if (event.getAction() == KeyEvent.ACTION_DOWN) { + Intent serviceIntent = new Intent(context, PlaybackService.class); + int keycode = event.getKeyCode(); + serviceIntent.putExtra(EXTRA_KEYCODE, keycode); + context.startService(serviceIntent); + } + + } + +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/GpodnetSyncService.java b/core/src/main/java/de/danoeh/antennapod/core/service/GpodnetSyncService.java new file mode 100644 index 000000000..0f2a81dfb --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/service/GpodnetSyncService.java @@ -0,0 +1,251 @@ +package de.danoeh.antennapod.core.service; + +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.os.IBinder; +import android.support.v4.app.NotificationCompat; +import android.util.Log; + +import java.util.Date; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +import de.danoeh.antennapod.core.BuildConfig; +import de.danoeh.antennapod.core.ClientConfig; +import de.danoeh.antennapod.core.R; +import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.core.gpoddernet.GpodnetService; +import de.danoeh.antennapod.core.gpoddernet.GpodnetServiceAuthenticationException; +import de.danoeh.antennapod.core.gpoddernet.GpodnetServiceException; +import de.danoeh.antennapod.core.gpoddernet.model.GpodnetSubscriptionChange; +import de.danoeh.antennapod.core.gpoddernet.model.GpodnetUploadChangesResponse; +import de.danoeh.antennapod.core.preferences.GpodnetPreferences; +import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.core.storage.DBTasks; +import de.danoeh.antennapod.core.storage.DownloadRequestException; +import de.danoeh.antennapod.core.storage.DownloadRequester; +import de.danoeh.antennapod.core.util.NetworkUtils; + +/** + * Synchronizes local subscriptions with gpodder.net service. The service should be started with ACTION_SYNC as an action argument. + * This class also provides static methods for starting the GpodnetSyncService. + */ +public class GpodnetSyncService extends Service { + private static final String TAG = "GpodnetSyncService"; + + private static final long WAIT_INTERVAL = 5000L; + + public static final String ARG_ACTION = "action"; + + public static final String ACTION_SYNC = "de.danoeh.antennapod.intent.action.sync"; + + private GpodnetService service; + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + final String action = (intent != null) ? intent.getStringExtra(ARG_ACTION) : null; + if (action != null && action.equals(ACTION_SYNC)) { + Log.d(TAG, String.format("Waiting %d milliseconds before uploading changes", WAIT_INTERVAL)); + syncWaiterThread.restart(); + } else { + Log.e(TAG, "Received invalid intent: action argument is null or invalid"); + } + return START_FLAG_REDELIVERY; + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (BuildConfig.DEBUG) Log.d(TAG, "onDestroy"); + syncWaiterThread.interrupt(); + + } + + @Override + public IBinder onBind(Intent intent) { + return null; + } + + private synchronized GpodnetService tryLogin() throws GpodnetServiceException { + if (service == null) { + service = new GpodnetService(); + service.authenticate(GpodnetPreferences.getUsername(), GpodnetPreferences.getPassword()); + } + return service; + } + + private synchronized void syncChanges() { + if (GpodnetPreferences.loggedIn() && NetworkUtils.networkAvailable(this)) { + final long timestamp = GpodnetPreferences.getLastSyncTimestamp(); + try { + final List 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 = ClientConfig.gpodnetCallbacks.getGpodnetSyncServiceErrorNotificationPendingIntent(this); + Notification notification = builder.setContentTitle(title) + .setContentText(description) + .setContentIntent(activityIntent) + .setSmallIcon(R.drawable.stat_notify_sync_error) + .setAutoCancel(true) + .build(); + NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + nm.notify(id, notification); + } + + private WaiterThread syncWaiterThread = new WaiterThread(WAIT_INTERVAL) { + @Override + public void onWaitCompleted() { + syncChanges(); + } + }; + + private abstract class WaiterThread { + private long waitInterval; + private Thread thread; + + private WaiterThread(long waitInterval) { + this.waitInterval = waitInterval; + reinit(); + } + + public abstract void onWaitCompleted(); + + public void exec() { + if (!thread.isAlive()) { + thread.start(); + } + } + + private void reinit() { + if (thread != null && thread.isAlive()) { + Log.d(TAG, "Interrupting waiter thread"); + thread.interrupt(); + } + thread = new Thread() { + @Override + public void run() { + try { + Thread.sleep(waitInterval); + } catch (InterruptedException e) { + e.printStackTrace(); + } + if (!isInterrupted()) { + synchronized (this) { + onWaitCompleted(); + } + } + } + }; + } + + public void restart() { + reinit(); + exec(); + } + + public void interrupt() { + if (thread != null && thread.isAlive()) { + thread.interrupt(); + } + } + } + + public static void sendSyncIntent(Context context) { + if (GpodnetPreferences.loggedIn()) { + Intent intent = new Intent(context, GpodnetSyncService.class); + intent.putExtra(ARG_ACTION, ACTION_SYNC); + context.startService(intent); + } + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/APRedirectHandler.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/APRedirectHandler.java new file mode 100644 index 000000000..3efcf4da8 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/APRedirectHandler.java @@ -0,0 +1,54 @@ +package de.danoeh.antennapod.core.service.download; + +import android.util.Log; +import de.danoeh.antennapod.core.BuildConfig; +import org.apache.http.Header; +import org.apache.http.HttpResponse; +import org.apache.http.impl.client.DefaultRedirectHandler; +import org.apache.http.protocol.HttpContext; + +import java.net.URI; + +public class APRedirectHandler extends DefaultRedirectHandler { + // Identifier for logger + private static final String TAG = "APRedirectHandler"; + // Header field, which has to be potentially fixed + private static final String LOC = "Location"; + // Regular expressions for character strings, which should not appear in URLs + private static final String CHi[] = { "\\{", "\\}", "\\|", "\\\\", "\\^", "~", "\\[", "\\]", "\\`"}; + private static final String CHo[] = { "%7B", "%7D", "%7C", "%5C", "%5E", "%7E", "%5B", "%5D", "%60"}; + + /** + * Workaround for broken URLs in redirection. + * Proper solution involves LaxRedirectStrategy() which is not available in + * current API yet. + */ + @Override + public URI getLocationURI(HttpResponse response, HttpContext context) + throws org.apache.http.ProtocolException { + + Header h[] = response.getHeaders(LOC); + if (h.length>0) { + String s = h[0].getValue(); + + // Fix broken URL + for(int i=0; i 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/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadService.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadService.java new file mode 100644 index 000000000..9229622ed --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadService.java @@ -0,0 +1,1200 @@ +package de.danoeh.antennapod.core.service.download; + +import android.annotation.SuppressLint; +import android.app.Notification; +import android.app.NotificationManager; +import android.app.Service; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.media.MediaMetadataRetriever; +import android.os.Binder; +import android.os.Handler; +import android.os.IBinder; +import android.support.v4.app.NotificationCompat; +import android.util.Log; +import android.webkit.URLUtil; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.http.HttpStatus; +import org.xml.sax.SAXException; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.Callable; +import java.util.concurrent.CompletionService; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorCompletionService; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.RejectedExecutionHandler; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import javax.xml.parsers.ParserConfigurationException; + +import de.danoeh.antennapod.core.BuildConfig; +import de.danoeh.antennapod.core.ClientConfig; +import de.danoeh.antennapod.core.R; +import de.danoeh.antennapod.core.feed.EventDistributor; +import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.core.feed.FeedImage; +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.feed.FeedMedia; +import de.danoeh.antennapod.core.feed.FeedPreferences; +import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.core.storage.DBTasks; +import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.core.storage.DownloadRequestException; +import de.danoeh.antennapod.core.storage.DownloadRequester; +import de.danoeh.antennapod.core.syndication.handler.FeedHandler; +import de.danoeh.antennapod.core.syndication.handler.UnsupportedFeedtypeException; +import de.danoeh.antennapod.core.util.ChapterUtils; +import de.danoeh.antennapod.core.util.DownloadError; +import de.danoeh.antennapod.core.util.InvalidFeedException; + +/** + * Manages the download of feedfiles in the app. Downloads can be enqueued viathe startService intent. + * The argument of the intent is an instance of DownloadRequest in the EXTRA_REQUEST field of + * the intent. + * After the downloads have finished, the downloaded object will be passed on to a specific handler, depending on the + * type of the feedfile. + */ +public class DownloadService extends Service { + private static final String TAG = "DownloadService"; + + /** + * Cancels one download. The intent MUST have an EXTRA_DOWNLOAD_URL extra that contains the download URL of the + * object whose download should be cancelled. + */ + public static final String ACTION_CANCEL_DOWNLOAD = "action.de.danoeh.antennapod.core.service.cancelDownload"; + + /** + * Cancels all running downloads. + */ + public static final String ACTION_CANCEL_ALL_DOWNLOADS = "action.de.danoeh.antennapod.core.service.cancelAllDownloads"; + + /** + * Extra for ACTION_CANCEL_DOWNLOAD + */ + public static final String EXTRA_DOWNLOAD_URL = "downloadUrl"; + + /** + * Sent by the DownloadService when the content of the downloads list + * changes. + */ + public static final String ACTION_DOWNLOADS_CONTENT_CHANGED = "action.de.danoeh.antennapod.core.service.downloadsContentChanged"; + + /** + * Extra for ACTION_ENQUEUE_DOWNLOAD intent. + */ + public static final String EXTRA_REQUEST = "request"; + + /** + * Stores new media files that will be queued for auto-download if possible. + */ + private List 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() { + Bitmap icon = BitmapFactory.decodeResource(getResources(), + R.drawable.stat_notify_sync); + + if (android.os.Build.VERSION.SDK_INT >= 16) { + notificationBuilder = new Notification.BigTextStyle( + new Notification.Builder(this).setOngoing(true) + .setContentIntent(ClientConfig.downloadServiceCallbacks.getNotificationContentIntent(this)).setLargeIcon(icon) + .setSmallIcon(R.drawable.stat_notify_sync) + ); + } else { + notificationCompatBuilder = new NotificationCompat.Builder(this) + .setOngoing(true).setContentIntent(ClientConfig.downloadServiceCallbacks.getNotificationContentIntent(this)) + .setLargeIcon(icon) + .setSmallIcon(R.drawable.stat_notify_sync); + } + if (BuildConfig.DEBUG) + Log.d(TAG, "Notification set up"); + } + + /** + * Updates the contents of the service's notifications. Should be called + * before setupNotificationBuilders. + */ + @SuppressLint("NewApi") + private Notification updateNotifications() { + String contentTitle = getString(R.string.download_notification_title); + int numDownloads = requester.getNumberOfDownloads(); + String downloadsLeft; + if (numDownloads > 0) { + downloadsLeft = requester.getNumberOfDownloads() + + getString(R.string.downloads_left); + } else { + downloadsLeft = getString(R.string.downloads_processing); + } + if (android.os.Build.VERSION.SDK_INT >= 16) { + + if (notificationBuilder != null) { + + StringBuilder bigText = new StringBuilder(""); + for (int i = 0; i < downloads.size(); i++) { + Downloader downloader = downloads.get(i); + final DownloadRequest request = downloader + .getDownloadRequest(); + if (request.getFeedfileType() == Feed.FEEDFILETYPE_FEED) { + if (request.getTitle() != null) { + if (i > 0) { + bigText.append("\n"); + } + bigText.append("\u2022 " + request.getTitle()); + } + } else if (request.getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA) { + if (request.getTitle() != null) { + if (i > 0) { + bigText.append("\n"); + } + bigText.append("\u2022 " + request.getTitle() + + " (" + request.getProgressPercent() + + "%)"); + } + } + + } + notificationBuilder.setSummaryText(downloadsLeft); + notificationBuilder.setBigContentTitle(contentTitle); + if (bigText != null) { + notificationBuilder.bigText(bigText.toString()); + } + return notificationBuilder.build(); + } + } else { + if (notificationCompatBuilder != null) { + notificationCompatBuilder.setContentTitle(contentTitle); + notificationCompatBuilder.setContentText(downloadsLeft); + return notificationCompatBuilder.build(); + } + } + return null; + } + + private Downloader getDownloader(String downloadUrl) { + for (Downloader downloader : downloads) { + if (downloader.getDownloadRequest().getSource().equals(downloadUrl)) { + return downloader; + } + } + return null; + } + + private BroadcastReceiver cancelDownloadReceiver = new BroadcastReceiver() { + + @Override + public void onReceive(Context context, Intent intent) { + if (StringUtils.equals(intent.getAction(), ACTION_CANCEL_DOWNLOAD)) { + String url = intent.getStringExtra(EXTRA_DOWNLOAD_URL); + Validate.notNull(url, "ACTION_CANCEL_DOWNLOAD intent needs download url extra"); + + if (BuildConfig.DEBUG) + Log.d(TAG, "Cancelling download with url " + url); + Downloader d = getDownloader(url); + if (d != null) { + d.cancel(); + } else { + Log.e(TAG, "Could not cancel download with url " + url); + } + + } else if (StringUtils.equals(intent.getAction(), ACTION_CANCEL_ALL_DOWNLOADS)) { + for (Downloader d : downloads) { + d.cancel(); + if (BuildConfig.DEBUG) + Log.d(TAG, "Cancelled all downloads"); + } + sendBroadcast(new Intent(ACTION_DOWNLOADS_CONTENT_CHANGED)); + + } + queryDownloads(); + } + + }; + + private void onDownloadQueued(Intent intent) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Received enqueue request"); + DownloadRequest request = intent.getParcelableExtra(EXTRA_REQUEST); + if (request == null) { + throw new IllegalArgumentException( + "ACTION_ENQUEUE_DOWNLOAD intent needs request extra"); + } + + Downloader downloader = getDownloader(request); + if (downloader != null) { + numberOfDownloads.incrementAndGet(); + downloads.add(downloader); + downloadExecutor.submit(downloader); + sendBroadcast(new Intent(ACTION_DOWNLOADS_CONTENT_CHANGED)); + } + + queryDownloads(); + } + + private Downloader getDownloader(DownloadRequest request) { + if (URLUtil.isHttpUrl(request.getSource()) + || URLUtil.isHttpsUrl(request.getSource())) { + return new HttpDownloader(request); + } + Log.e(TAG, + "Could not find appropriate downloader for " + + request.getSource() + ); + return null; + } + + /** + * Remove download from the DownloadRequester list and from the + * DownloadService list. + */ + private void removeDownload(final Downloader d) { + handler.post(new Runnable() { + @Override + public void run() { + if (BuildConfig.DEBUG) + Log.d(TAG, "Removing downloader: " + + d.getDownloadRequest().getSource()); + boolean rc = downloads.remove(d); + if (BuildConfig.DEBUG) + Log.d(TAG, "Result of downloads.remove: " + rc); + DownloadRequester.getInstance().removeDownload(d.getDownloadRequest()); + sendBroadcast(new Intent(ACTION_DOWNLOADS_CONTENT_CHANGED)); + } + }); + } + + /** + * Adds a new DownloadStatus object to the list of completed downloads and + * saves it in the database + * + * @param status the download that is going to be saved + */ + private void saveDownloadStatus(DownloadStatus status) { + reportQueue.add(status); + DBWriter.addDownloadStatus(this, status); + } + + private void sendDownloadHandledIntent() { + EventDistributor.getInstance().sendDownloadHandledBroadcast(); + } + + /** + * Creates a notification at the end of the service lifecycle to notify the + * user about the number of completed downloads. A report will only be + * created if the number of successfully downloaded feeds is bigger than 1 + * or if there is at least one failed download which is not an image or if + * there is at least one downloaded media file. + */ + private void updateReport() { + // check if report should be created + boolean createReport = false; + int successfulDownloads = 0; + int failedDownloads = 0; + + // a download report is created if at least one download has failed + // (excluding failed image downloads) + for (DownloadStatus status : reportQueue) { + if (status.isSuccessful()) { + successfulDownloads++; + } else if (!status.isCancelled()) { + if (status.getFeedfileType() != FeedImage.FEEDFILETYPE_FEEDIMAGE) { + createReport = true; + } + failedDownloads++; + } + } + + if (createReport) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Creating report"); + // create notification object + Notification notification = new NotificationCompat.Builder(this) + .setTicker( + getString(R.string.download_report_title)) + .setContentTitle( + getString(R.string.download_report_title)) + .setContentText( + String.format( + getString(R.string.download_report_content), + successfulDownloads, failedDownloads) + ) + .setSmallIcon(R.drawable.stat_notify_sync) + .setLargeIcon( + BitmapFactory.decodeResource(getResources(), + R.drawable.stat_notify_sync) + ) + .setContentIntent( + ClientConfig.downloadServiceCallbacks.getReportNotificationContentIntent(this) + ) + .setAutoCancel(true).build(); + NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + nm.notify(REPORT_ID, notification); + } else { + if (BuildConfig.DEBUG) + Log.d(TAG, "No report is created"); + } + reportQueue.clear(); + } + + /** + * Calls query downloads on the services main thread. This method should be used instead of queryDownloads if it is + * used from a thread other than the main thread. + */ + void queryDownloadsAsync() { + handler.post(new Runnable() { + public void run() { + queryDownloads(); + ; + } + }); + } + + /** + * Check if there's something else to download, otherwise stop + */ + void queryDownloads() { + if (BuildConfig.DEBUG) { + Log.d(TAG, numberOfDownloads.get() + " downloads left"); + } + + if (numberOfDownloads.get() <= 0 && DownloadRequester.getInstance().hasNoDownloads()) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Number of downloads is " + numberOfDownloads.get() + ", attempting shutdown"); + stopSelf(); + } else { + setupNotificationUpdater(); + startForeground(NOTIFICATION_ID, updateNotifications()); + } + } + + private void postAuthenticationNotification(final DownloadRequest downloadRequest) { + handler.post(new Runnable() { + @Override + public void run() { + final String resourceTitle = (downloadRequest.getTitle() != null) + ? downloadRequest.getTitle() : downloadRequest.getSource(); + + NotificationCompat.Builder builder = new NotificationCompat.Builder(DownloadService.this); + builder.setTicker(getText(R.string.authentication_notification_title)) + .setContentTitle(getText(R.string.authentication_notification_title)) + .setContentText(getText(R.string.authentication_notification_msg)) + .setStyle(new NotificationCompat.BigTextStyle().bigText(getText(R.string.authentication_notification_msg) + + ": " + resourceTitle)) + .setSmallIcon(R.drawable.ic_stat_authentication) + .setLargeIcon(BitmapFactory.decodeResource(getResources(), R.drawable.ic_stat_authentication)) + .setAutoCancel(true) + .setContentIntent(ClientConfig.downloadServiceCallbacks.getAuthentificationNotificationContentIntent(DownloadService.this, downloadRequest)); + Notification n = builder.build(); + NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + nm.notify(downloadRequest.getSource().hashCode(), n); + } + }); + } + + /** + * Is called whenever a Feed is downloaded + */ + private void handleCompletedFeedDownload(DownloadRequest request) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Handling completed Feed Download"); + feedSyncThread.submitCompletedDownload(request); + + } + + /** + * Is called whenever a Feed-Image is downloaded + */ + private void handleCompletedImageDownload(DownloadStatus status, DownloadRequest request) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Handling completed Image Download"); + syncExecutor.execute(new ImageHandlerThread(status, request)); + } + + /** + * Is called whenever a FeedMedia is downloaded. + */ + private void handleCompletedFeedMediaDownload(DownloadStatus status, DownloadRequest request) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Handling completed FeedMedia Download"); + syncExecutor.execute(new MediaHandlerThread(status, request)); + } + + private void handleFailedDownload(DownloadStatus status, DownloadRequest request) { + if (BuildConfig.DEBUG) Log.d(TAG, "Handling failed download"); + syncExecutor.execute(new FailedDownloadHandler(status, request)); + } + + /** + * Takes a single Feed, parses the corresponding file and refreshes + * information in the manager + */ + class FeedSyncThread extends Thread { + private static final String TAG = "FeedSyncThread"; + + private BlockingQueue 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/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadStatus.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadStatus.java new file mode 100644 index 000000000..d05650d10 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadStatus.java @@ -0,0 +1,181 @@ +package de.danoeh.antennapod.core.service.download; + +import org.apache.commons.lang3.Validate; + +import de.danoeh.antennapod.core.feed.FeedFile; +import de.danoeh.antennapod.core.util.DownloadError; + +import java.util.Date; + +/** Contains status attributes for one download */ +public class DownloadStatus { + /** + * Downloaders should use this constant for the size attribute if necessary + * so that the listadapters etc. can react properly. + */ + public static final int SIZE_UNKNOWN = -1; + + // ----------------------------------- ATTRIBUTES STORED IN DB + /** Unique id for storing the object in database. */ + protected long id; + /** + * A human-readable string which is shown to the user so that he can + * identify the download. Should be the title of the item/feed/media or the + * URL if the download has no other title. + */ + protected String title; + protected DownloadError reason; + /** + * A message which can be presented to the user to give more information. + * Should be null if Download was successful. + */ + protected String reasonDetailed; + protected boolean successful; + protected Date completionDate; + protected long feedfileId; + /** + * Is used to determine the type of the feedfile even if the feedfile does + * not exist anymore. The value should be FEEDFILETYPE_FEED, + * FEEDFILETYPE_FEEDIMAGE or FEEDFILETYPE_FEEDMEDIA + */ + protected int feedfileType; + + // ------------------------------------ NOT STORED IN DB + protected boolean done; + protected boolean cancelled; + + /** Constructor for restoring Download status entries from DB. */ + public DownloadStatus(long id, String title, long feedfileId, + int feedfileType, boolean successful, DownloadError reason, + Date completionDate, String reasonDetailed) { + this.id = id; + this.title = title; + this.done = true; + this.feedfileId = feedfileId; + this.reason = reason; + this.successful = successful; + this.completionDate = (Date) completionDate.clone(); + this.reasonDetailed = reasonDetailed; + this.feedfileType = feedfileType; + } + + public DownloadStatus(DownloadRequest request, DownloadError reason, + boolean successful, boolean cancelled, String reasonDetailed) { + Validate.notNull(request); + + this.title = request.getTitle(); + this.feedfileId = request.getFeedfileId(); + this.feedfileType = request.getFeedfileType(); + this.reason = reason; + this.successful = successful; + this.cancelled = cancelled; + this.reasonDetailed = reasonDetailed; + this.completionDate = new Date(); + } + + /** Constructor for creating new completed downloads. */ + public DownloadStatus(FeedFile feedfile, String title, DownloadError reason, + boolean successful, String reasonDetailed) { + Validate.notNull(feedfile); + + this.title = title; + this.done = true; + this.feedfileId = feedfile.getId(); + this.feedfileType = feedfile.getTypeAsInt(); + this.reason = reason; + this.successful = successful; + this.completionDate = new Date(); + this.reasonDetailed = reasonDetailed; + } + + /** Constructor for creating new completed downloads. */ + public DownloadStatus(long feedfileId, int feedfileType, String title, + DownloadError reason, boolean successful, String reasonDetailed) { + this.title = title; + this.done = true; + this.feedfileId = feedfileId; + this.feedfileType = feedfileType; + this.reason = reason; + this.successful = successful; + this.completionDate = new Date(); + this.reasonDetailed = reasonDetailed; + } + + @Override + public String toString() { + return "DownloadStatus [id=" + id + ", title=" + title + ", reason=" + + reason + ", reasonDetailed=" + reasonDetailed + + ", successful=" + successful + ", completionDate=" + + completionDate + ", feedfileId=" + feedfileId + + ", feedfileType=" + feedfileType + ", done=" + done + + ", cancelled=" + cancelled + "]"; + } + + public long getId() { + return id; + } + + public String getTitle() { + return title; + } + + public DownloadError getReason() { + return reason; + } + + public String getReasonDetailed() { + return reasonDetailed; + } + + public boolean isSuccessful() { + return successful; + } + + public Date getCompletionDate() { + return (Date) completionDate.clone(); + } + + public long getFeedfileId() { + return feedfileId; + } + + public int getFeedfileType() { + return feedfileType; + } + + public boolean isDone() { + return done; + } + + public boolean isCancelled() { + return cancelled; + } + + public void setSuccessful() { + this.successful = true; + this.reason = DownloadError.SUCCESS; + this.done = true; + } + + public void setFailed(DownloadError reason, String reasonDetailed) { + this.successful = false; + this.reason = reason; + this.reasonDetailed = reasonDetailed; + this.done = true; + } + + public void setCancelled() { + this.successful = false; + this.reason = DownloadError.ERROR_DOWNLOAD_CANCELLED; + this.done = true; + this.cancelled = true; + } + + public void setCompletionDate(Date completionDate) { + this.completionDate = (Date) completionDate.clone(); + } + + public void setId(long id) { + this.id = id; + } +} \ No newline at end of file diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/Downloader.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/Downloader.java new file mode 100644 index 000000000..d8042d202 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/Downloader.java @@ -0,0 +1,73 @@ +package de.danoeh.antennapod.core.service.download; + +import android.content.Context; +import android.net.wifi.WifiManager; + +import java.util.concurrent.Callable; + +import de.danoeh.antennapod.core.ClientConfig; +import de.danoeh.antennapod.core.R; + +/** + * Downloads files + */ +public abstract class Downloader implements Callable { + private static final String TAG = "Downloader"; + + protected volatile boolean finished; + + protected volatile boolean cancelled; + + protected DownloadRequest request; + protected DownloadStatus result; + + public Downloader(DownloadRequest request) { + super(); + this.request = request; + this.request.setStatusMsg(R.string.download_pending); + this.cancelled = false; + this.result = new DownloadStatus(request, null, false, false, null); + } + + protected abstract void download(); + + public final Downloader call() { + WifiManager wifiManager = (WifiManager) + ClientConfig.applicationCallbacks.getApplicationInstance().getSystemService(Context.WIFI_SERVICE); + WifiManager.WifiLock wifiLock = null; + if (wifiManager != null) { + wifiLock = wifiManager.createWifiLock(TAG); + wifiLock.acquire(); + } + + download(); + + if (wifiLock != null) { + wifiLock.release(); + } + + if (result == null) { + throw new IllegalStateException( + "Downloader hasn't created DownloadStatus object"); + } + finished = true; + return this; + } + + public DownloadRequest getDownloadRequest() { + return request; + } + + public DownloadStatus getResult() { + return result; + } + + public boolean isFinished() { + return finished; + } + + public void cancel() { + cancelled = true; + } + +} \ No newline at end of file diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloaderCallback.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloaderCallback.java new file mode 100644 index 000000000..2d9347b0a --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloaderCallback.java @@ -0,0 +1,10 @@ +package de.danoeh.antennapod.core.service.download; + +/** + * Callback used by the Downloader-classes to notify the requester that the + * download has completed. + */ +public interface DownloaderCallback { + + public void onDownloadCompleted(Downloader downloader); +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/HttpDownloader.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/HttpDownloader.java new file mode 100644 index 000000000..32d0d351a --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/HttpDownloader.java @@ -0,0 +1,252 @@ +package de.danoeh.antennapod.core.service.download; + +import android.net.http.AndroidHttpClient; +import android.util.Log; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.http.Header; +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.HttpStatus; +import org.apache.http.auth.UsernamePasswordCredentials; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.auth.BasicScheme; +import org.apache.http.message.BasicHeader; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.RandomAccessFile; +import java.net.HttpURLConnection; +import java.net.SocketTimeoutException; +import java.net.UnknownHostException; + +import de.danoeh.antennapod.core.BuildConfig; +import de.danoeh.antennapod.core.ClientConfig; +import de.danoeh.antennapod.core.R; +import de.danoeh.antennapod.core.feed.FeedImage; +import de.danoeh.antennapod.core.util.DownloadError; +import de.danoeh.antennapod.core.util.StorageUtils; +import de.danoeh.antennapod.core.util.URIUtil; + +public class HttpDownloader extends Downloader { + private static final String TAG = "HttpDownloader"; + + private static final int BUFFER_SIZE = 8 * 1024; + + public HttpDownloader(DownloadRequest request) { + super(request); + } + + @Override + protected void download() { + File destination = new File(request.getDestination()); + final boolean fileExists = destination.exists(); + + if (request.isDeleteOnFailure() && fileExists) { + Log.w(TAG, "File already exists"); + if (request.getFeedfileType() != FeedImage.FEEDFILETYPE_FEEDIMAGE) { + onFail(DownloadError.ERROR_FILE_EXISTS, null); + return; + } else { + onSuccess(); + return; + } + } + + HttpClient httpClient = AntennapodHttpClient.getHttpClient(); + RandomAccessFile out = null; + InputStream connection = null; + try { + HttpGet httpGet = new HttpGet(URIUtil.getURIFromRequestUrl(request.getSource())); + + // add authentication information + String userInfo = httpGet.getURI().getUserInfo(); + if (userInfo != null) { + String[] parts = userInfo.split(":"); + if (parts.length == 2) { + httpGet.addHeader(BasicScheme.authenticate( + new UsernamePasswordCredentials(parts[0], parts[1]), + "UTF-8", false)); + } + } else if (!StringUtils.isEmpty(request.getUsername()) && request.getPassword() != null) { + httpGet.addHeader(BasicScheme.authenticate(new UsernamePasswordCredentials(request.getUsername(), + request.getPassword()), "UTF-8", false)); + } + + // add range header if necessary + if (fileExists) { + request.setSoFar(destination.length()); + httpGet.addHeader(new BasicHeader("Range", + "bytes=" + request.getSoFar() + "-")); + if (BuildConfig.DEBUG) Log.d(TAG, "Adding range header: " + request.getSoFar()); + } + + HttpResponse response = httpClient.execute(httpGet); + HttpEntity httpEntity = response.getEntity(); + int responseCode = response.getStatusLine().getStatusCode(); + Header contentEncodingHeader = response.getFirstHeader("Content-Encoding"); + + final boolean isGzip = contentEncodingHeader != null && + contentEncodingHeader.getValue().equalsIgnoreCase("gzip"); + + if (BuildConfig.DEBUG) + Log.d(TAG, "Response code is " + responseCode); + + if (responseCode / 100 != 2 || httpEntity == null) { + final DownloadError error; + final String details; + if (responseCode == HttpURLConnection.HTTP_UNAUTHORIZED) { + error = DownloadError.ERROR_UNAUTHORIZED; + details = String.valueOf(responseCode); + } else { + error = DownloadError.ERROR_HTTP_DATA_ERROR; + details = String.valueOf(responseCode); + } + onFail(error, details); + return; + } + + if (!StorageUtils.storageAvailable(ClientConfig.applicationCallbacks.getApplicationInstance())) { + onFail(DownloadError.ERROR_DEVICE_NOT_FOUND, null); + return; + } + + connection = new BufferedInputStream(AndroidHttpClient + .getUngzippedContent(httpEntity)); + + Header[] contentRangeHeaders = (fileExists) ? response.getHeaders("Content-Range") : null; + + if (fileExists && responseCode == HttpStatus.SC_PARTIAL_CONTENT + && contentRangeHeaders != null && contentRangeHeaders.length > 0) { + String start = contentRangeHeaders[0].getValue().substring("bytes ".length(), + contentRangeHeaders[0].getValue().indexOf("-")); + request.setSoFar(Long.valueOf(start)); + Log.d(TAG, "Starting download at position " + request.getSoFar()); + + out = new RandomAccessFile(destination, "rw"); + out.seek(request.getSoFar()); + } else { + destination.delete(); + destination.createNewFile(); + out = new RandomAccessFile(destination, "rw"); + } + + + byte[] buffer = new byte[BUFFER_SIZE]; + int count = 0; + request.setStatusMsg(R.string.download_running); + if (BuildConfig.DEBUG) + Log.d(TAG, "Getting size of download"); + request.setSize(httpEntity.getContentLength() + request.getSoFar()); + if (BuildConfig.DEBUG) + Log.d(TAG, "Size is " + request.getSize()); + if (request.getSize() < 0) { + request.setSize(DownloadStatus.SIZE_UNKNOWN); + } + + long freeSpace = StorageUtils.getFreeSpaceAvailable(); + if (BuildConfig.DEBUG) + Log.d(TAG, "Free space is " + freeSpace); + + if (request.getSize() != DownloadStatus.SIZE_UNKNOWN + && request.getSize() > freeSpace) { + onFail(DownloadError.ERROR_NOT_ENOUGH_SPACE, null); + return; + } + + if (BuildConfig.DEBUG) + Log.d(TAG, "Starting download"); + while (!cancelled + && (count = connection.read(buffer)) != -1) { + out.write(buffer, 0, count); + request.setSoFar(request.getSoFar() + count); + request.setProgressPercent((int) (((double) request + .getSoFar() / (double) request + .getSize()) * 100)); + } + if (cancelled) { + onCancelled(); + } else { + // check if size specified in the response header is the same as the size of the + // written file. This check cannot be made if compression was used + if (!isGzip && request.getSize() != DownloadStatus.SIZE_UNKNOWN && + request.getSoFar() != request.getSize()) { + onFail(DownloadError.ERROR_IO_ERROR, + "Download completed but size: " + + request.getSoFar() + + " does not equal expected size " + + request.getSize() + ); + return; + } + onSuccess(); + } + + } catch (IllegalArgumentException e) { + e.printStackTrace(); + onFail(DownloadError.ERROR_MALFORMED_URL, e.getMessage()); + } catch (SocketTimeoutException e) { + e.printStackTrace(); + onFail(DownloadError.ERROR_CONNECTION_ERROR, e.getMessage()); + } catch (UnknownHostException e) { + e.printStackTrace(); + onFail(DownloadError.ERROR_UNKNOWN_HOST, e.getMessage()); + } catch (IOException e) { + e.printStackTrace(); + onFail(DownloadError.ERROR_IO_ERROR, e.getMessage()); + } catch (NullPointerException e) { + // might be thrown by connection.getInputStream() + e.printStackTrace(); + onFail(DownloadError.ERROR_CONNECTION_ERROR, request.getSource()); + } finally { + IOUtils.closeQuietly(out); + AntennapodHttpClient.cleanup(); + } + } + + private void onSuccess() { + if (BuildConfig.DEBUG) + Log.d(TAG, "Download was successful"); + result.setSuccessful(); + } + + private void onFail(DownloadError reason, String reasonDetailed) { + if (BuildConfig.DEBUG) { + Log.d(TAG, "Download failed"); + } + result.setFailed(reason, reasonDetailed); + if (request.isDeleteOnFailure()) { + cleanup(); + } + } + + private void onCancelled() { + if (BuildConfig.DEBUG) + Log.d(TAG, "Download was cancelled"); + result.setCancelled(); + cleanup(); + } + + /** + * Deletes unfinished downloads. + */ + private void cleanup() { + if (request.getDestination() != null) { + File dest = new File(request.getDestination()); + if (dest.exists()) { + boolean rc = dest.delete(); + if (BuildConfig.DEBUG) + Log.d(TAG, "Deleted file " + dest.getName() + "; Result: " + + rc); + } else { + if (BuildConfig.DEBUG) + Log.d(TAG, "cleanup() didn't delete file: does not exist."); + } + } + } + +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java new file mode 100644 index 000000000..5123e40c7 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java @@ -0,0 +1,1072 @@ +package de.danoeh.antennapod.core.service.playback; + +import android.annotation.SuppressLint; +import android.app.Notification; +import android.app.PendingIntent; +import android.app.Service; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.SharedPreferences; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.media.AudioManager; +import android.media.MediaMetadataRetriever; +import android.media.MediaPlayer; +import android.media.RemoteControlClient; +import android.media.RemoteControlClient.MetadataEditor; +import android.os.AsyncTask; +import android.os.Binder; +import android.os.Build; +import android.os.IBinder; +import android.preference.PreferenceManager; +import android.support.v4.app.NotificationCompat; +import android.util.Log; +import android.util.Pair; +import android.view.KeyEvent; +import android.view.SurfaceHolder; +import android.widget.Toast; + +import org.apache.commons.lang3.StringUtils; + +import java.io.IOException; +import java.util.List; + +import de.danoeh.antennapod.core.BuildConfig; +import de.danoeh.antennapod.core.ClientConfig; +import de.danoeh.antennapod.core.R; +import de.danoeh.antennapod.core.asynctask.PicassoProvider; +import de.danoeh.antennapod.core.feed.Chapter; +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.feed.FeedMedia; +import de.danoeh.antennapod.core.feed.MediaType; +import de.danoeh.antennapod.core.preferences.PlaybackPreferences; +import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.core.receiver.MediaButtonReceiver; +import de.danoeh.antennapod.core.storage.DBTasks; +import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.core.util.QueueAccess; +import de.danoeh.antennapod.core.util.flattr.FlattrUtils; +import de.danoeh.antennapod.core.util.playback.Playable; + +/** + * Controls the MediaPlayer that plays a FeedMedia-file + */ +public class PlaybackService extends Service { + public static final String FORCE_WIDGET_UPDATE = "de.danoeh.antennapod.FORCE_WIDGET_UPDATE"; + public static final String STOP_WIDGET_UPDATE = "de.danoeh.antennapod.STOP_WIDGET_UPDATE"; + /** + * Logging tag + */ + private static final String TAG = "PlaybackService"; + + /** + * Parcelable of type Playable. + */ + public static final String EXTRA_PLAYABLE = "PlaybackService.PlayableExtra"; + /** + * True if media should be streamed. + */ + public static final String EXTRA_SHOULD_STREAM = "extra.de.danoeh.antennapod.core.service.shouldStream"; + /** + * True if playback should be started immediately after media has been + * prepared. + */ + public static final String EXTRA_START_WHEN_PREPARED = "extra.de.danoeh.antennapod.core.service.startWhenPrepared"; + + public static final String EXTRA_PREPARE_IMMEDIATELY = "extra.de.danoeh.antennapod.core.service.prepareImmediately"; + + public static final String ACTION_PLAYER_STATUS_CHANGED = "action.de.danoeh.antennapod.core.service.playerStatusChanged"; + private static final String AVRCP_ACTION_PLAYER_STATUS_CHANGED = "com.android.music.playstatechanged"; + private static final String AVRCP_ACTION_META_CHANGED = "com.android.music.metachanged"; + + public static final String ACTION_PLAYER_NOTIFICATION = "action.de.danoeh.antennapod.core.service.playerNotification"; + public static final String EXTRA_NOTIFICATION_CODE = "extra.de.danoeh.antennapod.core.service.notificationCode"; + public static final String EXTRA_NOTIFICATION_TYPE = "extra.de.danoeh.antennapod.core.service.notificationType"; + + /** + * If the PlaybackService receives this action, it will stop playback and + * try to shutdown. + */ + public static final String ACTION_SHUTDOWN_PLAYBACK_SERVICE = "action.de.danoeh.antennapod.core.service.actionShutdownPlaybackService"; + + /** + * If the PlaybackService receives this action, it will end playback of the + * current episode and load the next episode if there is one available. + */ + public static final String ACTION_SKIP_CURRENT_EPISODE = "action.de.danoeh.antennapod.core.service.skipCurrentEpisode"; + + /** + * Used in NOTIFICATION_TYPE_RELOAD. + */ + public static final int EXTRA_CODE_AUDIO = 1; + public static final int EXTRA_CODE_VIDEO = 2; + + public static final int NOTIFICATION_TYPE_ERROR = 0; + public static final int NOTIFICATION_TYPE_INFO = 1; + public static final int NOTIFICATION_TYPE_BUFFER_UPDATE = 2; + + /** + * Receivers of this intent should update their information about the curently playing media + */ + public static final int NOTIFICATION_TYPE_RELOAD = 3; + /** + * The state of the sleeptimer changed. + */ + public static final int NOTIFICATION_TYPE_SLEEPTIMER_UPDATE = 4; + public static final int NOTIFICATION_TYPE_BUFFER_START = 5; + public static final int NOTIFICATION_TYPE_BUFFER_END = 6; + /** + * No more episodes are going to be played. + */ + public static final int NOTIFICATION_TYPE_PLAYBACK_END = 7; + + /** + * Playback speed has changed + */ + public static final int NOTIFICATION_TYPE_PLAYBACK_SPEED_CHANGE = 8; + + /** + * Returned by getPositionSafe() or getDurationSafe() if the playbackService + * is in an invalid state. + */ + public static final int INVALID_TIME = -1; + + /** + * Is true if service is running. + */ + public static boolean isRunning = false; + /** + * Is true if service has received a valid start command. + */ + public static boolean started = false; + + private static final int NOTIFICATION_ID = 1; + + private RemoteControlClient remoteControlClient; + private PlaybackServiceMediaPlayer mediaPlayer; + private PlaybackServiceTaskManager taskManager; + + private static volatile MediaType currentMediaType = MediaType.UNKNOWN; + + private final IBinder mBinder = new LocalBinder(); + + public class LocalBinder extends Binder { + public PlaybackService getService() { + return PlaybackService.this; + } + } + + @Override + public boolean onUnbind(Intent intent) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Received onUnbind event"); + return super.onUnbind(intent); + } + + /** + * Returns an intent which starts an audio- or videoplayer, depending on the + * type of media that is being played. If the playbackservice is not + * running, the type of the last played media will be looked up. + */ + public static Intent getPlayerActivityIntent(Context context) { + if (isRunning) { + return ClientConfig.playbackServiceCallbacks.getPlayerActivityIntent(context, currentMediaType); + } else { + if (PlaybackPreferences.getCurrentEpisodeIsVideo()) { + return ClientConfig.playbackServiceCallbacks.getPlayerActivityIntent(context, MediaType.VIDEO); + } else { + return ClientConfig.playbackServiceCallbacks.getPlayerActivityIntent(context, MediaType.AUDIO); + } + } + } + + /** + * Same as getPlayerActivityIntent(context), but here the type of activity + * depends on the FeedMedia that is provided as an argument. + */ + public static Intent getPlayerActivityIntent(Context context, Playable media) { + MediaType mt = media.getMediaType(); + return ClientConfig.playbackServiceCallbacks.getPlayerActivityIntent(context, mt); + } + + @SuppressLint("NewApi") + @Override + public void onCreate() { + super.onCreate(); + if (BuildConfig.DEBUG) + Log.d(TAG, "Service created."); + isRunning = true; + + registerReceiver(headsetDisconnected, new IntentFilter( + Intent.ACTION_HEADSET_PLUG)); + registerReceiver(shutdownReceiver, new IntentFilter( + ACTION_SHUTDOWN_PLAYBACK_SERVICE)); + registerReceiver(audioBecomingNoisy, new IntentFilter( + AudioManager.ACTION_AUDIO_BECOMING_NOISY)); + registerReceiver(skipCurrentEpisodeReceiver, new IntentFilter( + ACTION_SKIP_CURRENT_EPISODE)); + remoteControlClient = setupRemoteControlClient(); + taskManager = new PlaybackServiceTaskManager(this, taskManagerCallback); + mediaPlayer = new PlaybackServiceMediaPlayer(this, mediaPlayerCallback); + + } + + @SuppressLint("NewApi") + @Override + public void onDestroy() { + super.onDestroy(); + if (BuildConfig.DEBUG) + Log.d(TAG, "Service is about to be destroyed"); + isRunning = false; + started = false; + currentMediaType = MediaType.UNKNOWN; + + unregisterReceiver(headsetDisconnected); + unregisterReceiver(shutdownReceiver); + unregisterReceiver(audioBecomingNoisy); + unregisterReceiver(skipCurrentEpisodeReceiver); + mediaPlayer.shutdown(); + taskManager.shutdown(); + } + + @Override + public IBinder onBind(Intent intent) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Received onBind event"); + return mBinder; + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + super.onStartCommand(intent, flags, startId); + + if (BuildConfig.DEBUG) + Log.d(TAG, "OnStartCommand called"); + final int keycode = intent.getIntExtra(MediaButtonReceiver.EXTRA_KEYCODE, -1); + final Playable playable = intent.getParcelableExtra(EXTRA_PLAYABLE); + if (keycode == -1 && playable == null) { + Log.e(TAG, "PlaybackService was started with no arguments"); + stopSelf(); + } + + if ((flags & Service.START_FLAG_REDELIVERY) != 0) { + if (BuildConfig.DEBUG) + Log.d(TAG, "onStartCommand is a redelivered intent, calling stopForeground now."); + stopForeground(true); + } else { + + if (keycode != -1) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Received media button event"); + handleKeycode(keycode); + } else { + started = true; + boolean stream = intent.getBooleanExtra(EXTRA_SHOULD_STREAM, + true); + boolean startWhenPrepared = intent.getBooleanExtra(EXTRA_START_WHEN_PREPARED, false); + boolean prepareImmediately = intent.getBooleanExtra(EXTRA_PREPARE_IMMEDIATELY, false); + sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, 0); + mediaPlayer.playMediaObject(playable, stream, startWhenPrepared, prepareImmediately); + } + } + + return Service.START_REDELIVER_INTENT; + } + + /** + * Handles media button events + */ + private void handleKeycode(int keycode) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Handling keycode: " + keycode); + + final PlaybackServiceMediaPlayer.PSMPInfo info = mediaPlayer.getPSMPInfo(); + final PlayerStatus status = info.playerStatus; + switch (keycode) { + case KeyEvent.KEYCODE_HEADSETHOOK: + case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: + if (status == PlayerStatus.PLAYING) { + 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(STOP_WIDGET_UPDATE)); + } + + private void updateWidget() { + PlaybackService.this.sendBroadcast(new Intent( + FORCE_WIDGET_UPDATE)); + } + + public boolean sleepTimerActive() { + return taskManager.isSleepTimerActive(); + } + + public long getSleepTimerTimeLeft() { + return taskManager.getSleepTimerTimeLeft(); + } + + @SuppressLint("NewApi") + private RemoteControlClient setupRemoteControlClient() { + if (Build.VERSION.SDK_INT < 14) { + return null; + } + + Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON); + mediaButtonIntent.setComponent(new ComponentName(getPackageName(), + MediaButtonReceiver.class.getName())); + PendingIntent mediaPendingIntent = PendingIntent.getBroadcast( + getApplicationContext(), 0, mediaButtonIntent, 0); + remoteControlClient = new RemoteControlClient(mediaPendingIntent); + int controlFlags; + if (android.os.Build.VERSION.SDK_INT < 16) { + controlFlags = RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE + | RemoteControlClient.FLAG_KEY_MEDIA_NEXT; + } else { + controlFlags = RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE; + } + remoteControlClient.setTransportControlFlags(controlFlags); + return remoteControlClient; + } + + /** + * Refresh player status and metadata. + */ + @SuppressLint("NewApi") + private void refreshRemoteControlClientState(PlaybackServiceMediaPlayer.PSMPInfo info) { + if (android.os.Build.VERSION.SDK_INT >= 14) { + if (remoteControlClient != null) { + switch (info.playerStatus) { + case PLAYING: + remoteControlClient + .setPlaybackState(RemoteControlClient.PLAYSTATE_PLAYING); + break; + case PAUSED: + case INITIALIZED: + remoteControlClient + .setPlaybackState(RemoteControlClient.PLAYSTATE_PAUSED); + break; + case STOPPED: + remoteControlClient + .setPlaybackState(RemoteControlClient.PLAYSTATE_STOPPED); + break; + case ERROR: + remoteControlClient + .setPlaybackState(RemoteControlClient.PLAYSTATE_ERROR); + break; + default: + remoteControlClient + .setPlaybackState(RemoteControlClient.PLAYSTATE_BUFFERING); + } + if (info.playable != null) { + MetadataEditor editor = remoteControlClient + .editMetadata(false); + editor.putString(MediaMetadataRetriever.METADATA_KEY_TITLE, + info.playable.getEpisodeTitle()); + + editor.putString(MediaMetadataRetriever.METADATA_KEY_ALBUM, + info.playable.getFeedTitle()); + + editor.apply(); + } + if (BuildConfig.DEBUG) + Log.d(TAG, "RemoteControlClient state was refreshed"); + } + } + } + + private void bluetoothNotifyChange(PlaybackServiceMediaPlayer.PSMPInfo info, String whatChanged) { + boolean isPlaying = false; + + if (info.playerStatus == PlayerStatus.PLAYING) { + isPlaying = true; + } + + if (info.playable != null) { + Intent i = new Intent(whatChanged); + i.putExtra("id", 1); + i.putExtra("artist", ""); + i.putExtra("album", info.playable.getFeedTitle()); + i.putExtra("track", info.playable.getEpisodeTitle()); + i.putExtra("playing", isPlaying); + final List 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/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceMediaPlayer.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceMediaPlayer.java new file mode 100644 index 000000000..590b67853 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceMediaPlayer.java @@ -0,0 +1,979 @@ +package de.danoeh.antennapod.core.service.playback; + +import android.content.ComponentName; +import android.content.Context; +import android.media.AudioManager; +import android.media.RemoteControlClient; +import android.net.wifi.WifiManager; +import android.os.PowerManager; +import android.telephony.TelephonyManager; +import android.util.Log; +import android.util.Pair; +import android.view.SurfaceHolder; + +import org.apache.commons.lang3.Validate; + +import java.io.IOException; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.RejectedExecutionHandler; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.locks.ReentrantLock; + +import de.danoeh.antennapod.core.BuildConfig; +import de.danoeh.antennapod.core.feed.Chapter; +import de.danoeh.antennapod.core.feed.MediaType; +import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.core.receiver.MediaButtonReceiver; +import de.danoeh.antennapod.core.util.playback.AudioPlayer; +import de.danoeh.antennapod.core.util.playback.IPlayer; +import de.danoeh.antennapod.core.util.playback.Playable; +import de.danoeh.antennapod.core.util.playback.VideoPlayer; + +/** + * Manages the MediaPlayer object of the PlaybackService. + */ +public class PlaybackServiceMediaPlayer { + public static final String TAG = "PlaybackServiceMediaPlayer"; + + /** + * Return value of some PSMP methods if the method call failed. + */ + public static final int INVALID_TIME = -1; + + private final AudioManager audioManager; + + private volatile PlayerStatus playerStatus; + private volatile PlayerStatus statusBeforeSeeking; + private volatile IPlayer mediaPlayer; + private volatile Playable media; + + private volatile boolean stream; + private volatile MediaType mediaType; + private volatile AtomicBoolean startWhenPrepared; + private volatile boolean pausedBecauseOfTransientAudiofocusLoss; + private volatile Pair 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/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceTaskManager.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceTaskManager.java new file mode 100644 index 000000000..1865afa6f --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceTaskManager.java @@ -0,0 +1,384 @@ +package de.danoeh.antennapod.core.service.playback; + +import android.content.Context; +import android.util.Log; + +import org.apache.commons.lang3.Validate; + +import de.danoeh.antennapod.core.BuildConfig; +import de.danoeh.antennapod.core.feed.EventDistributor; +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.core.util.playback.Playable; + +import java.util.List; +import java.util.concurrent.*; + +/** + * Manages the background tasks of PlaybackSerivce, i.e. + * the sleep timer, the position saver, the widget updater and + * the queue loader. + *

+ * 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/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlayerStatus.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlayerStatus.java new file mode 100644 index 000000000..1ad0c25d9 --- /dev/null +++ b/core/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/core/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java b/core/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java new file mode 100644 index 000000000..62edaae29 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java @@ -0,0 +1,908 @@ +package de.danoeh.antennapod.core.storage; + +import android.content.Context; +import android.database.Cursor; +import android.database.SQLException; +import android.util.Log; +import de.danoeh.antennapod.core.BuildConfig; +import de.danoeh.antennapod.core.feed.*; +import de.danoeh.antennapod.core.service.download.DownloadStatus; +import de.danoeh.antennapod.core.util.DownloadError; +import de.danoeh.antennapod.core.util.comparator.DownloadStatusComparator; +import de.danoeh.antennapod.core.util.comparator.FeedItemPubdateComparator; +import de.danoeh.antennapod.core.util.comparator.PlaybackCompletionDateComparator; +import de.danoeh.antennapod.core.util.flattr.FlattrStatus; +import de.danoeh.antennapod.core.util.flattr.FlattrThing; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.List; + +/** + * Provides methods for reading data from the AntennaPod database. + * In general, all database calls in DBReader-methods are executed on the caller's thread. + * This means that the caller should make sure that DBReader-methods are not executed on the GUI-thread. + * This class will use the {@link de.danoeh.antennapod.core.feed.EventDistributor} to notify listeners about changes in the database. + */ +public final class DBReader { + private static final String TAG = "DBReader"; + + /** + * Maximum size of the list returned by {@link #getPlaybackHistory(android.content.Context)}. + */ + public static final int PLAYBACK_HISTORY_SIZE = 50; + + /** + * Maximum size of the list returned by {@link #getDownloadLog(android.content.Context)}. + */ + public static final int DOWNLOAD_LOG_SIZE = 200; + + + private DBReader() { + } + + /** + * Returns a list of Feeds, sorted alphabetically by their title. + * + * @param context A context that is used for opening a database connection. + * @return A list of Feeds, sorted alphabetically by their title. A Feed-object + * of the returned list does NOT have its list of FeedItems yet. The FeedItem-list + * can be loaded separately with {@link #getFeedItemList(android.content.Context, de.danoeh.antennapod.core.feed.Feed)}. + */ + public static List 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/core/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java b/core/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java new file mode 100644 index 000000000..982959bc2 --- /dev/null +++ b/core/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.core.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/core/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java b/core/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java new file mode 100644 index 000000000..eec15acd2 --- /dev/null +++ b/core/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.core.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/core/src/main/java/de/danoeh/antennapod/core/storage/DownloadRequestException.java b/core/src/main/java/de/danoeh/antennapod/core/storage/DownloadRequestException.java new file mode 100644 index 000000000..c85559e20 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/DownloadRequestException.java @@ -0,0 +1,25 @@ +package de.danoeh.antennapod.core.storage; + +/** + * Thrown by the DownloadRequester if a download request contains invalid data + * or something went wrong while processing the request. + */ +public class DownloadRequestException extends Exception { + + public DownloadRequestException() { + super(); + } + + public DownloadRequestException(String detailMessage, Throwable throwable) { + super(detailMessage, throwable); + } + + public DownloadRequestException(String detailMessage) { + super(detailMessage); + } + + public DownloadRequestException(Throwable throwable) { + super(throwable); + } + +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/DownloadRequester.java b/core/src/main/java/de/danoeh/antennapod/core/storage/DownloadRequester.java new file mode 100644 index 000000000..2fd653d32 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/DownloadRequester.java @@ -0,0 +1,366 @@ +package de.danoeh.antennapod.core.storage; + +import android.content.Context; +import android.content.Intent; +import android.util.Log; +import android.webkit.URLUtil; +import de.danoeh.antennapod.core.BuildConfig; +import de.danoeh.antennapod.core.feed.*; +import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.core.service.download.DownloadRequest; +import de.danoeh.antennapod.core.service.download.DownloadService; +import de.danoeh.antennapod.core.util.FileNameGenerator; +import de.danoeh.antennapod.core.util.URLChecker; +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; + +import java.io.File; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + + +/** + * Sends download requests to the DownloadService. This class should always be used for starting downloads, + * otherwise they won't work correctly. + */ +public class DownloadRequester { + private static final String TAG = "DownloadRequester"; + + public static final String IMAGE_DOWNLOADPATH = "images/"; + public static final String FEED_DOWNLOADPATH = "cache/"; + public static final String MEDIA_DOWNLOADPATH = "media/"; + + private static DownloadRequester downloader; + + 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/core/src/main/java/de/danoeh/antennapod/core/storage/FeedItemStatistics.java b/core/src/main/java/de/danoeh/antennapod/core/storage/FeedItemStatistics.java new file mode 100644 index 000000000..f6a59836b --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/FeedItemStatistics.java @@ -0,0 +1,70 @@ +package de.danoeh.antennapod.core.storage; + +import java.util.Date; + +/** + * Contains information about a feed's items. + */ +public class FeedItemStatistics { + private long feedID; + private int numberOfItems; + private int numberOfNewItems; + private int numberOfInProgressItems; + private Date lastUpdate; + private static final Date UNKNOWN_DATE = new Date(0); + + + /** + * Creates new FeedItemStatistics object. + * + * @param feedID ID of the feed. + * @param numberOfItems Number of items that this feed has. + * @param numberOfNewItems Number of unread items this feed has. + * @param numberOfInProgressItems Number of items that the user has started listening to. + * @param lastUpdate pubDate of the latest episode. A lastUpdate value of 0 will be interpreted as DATE_UNKOWN if + * numberOfItems is 0. + */ + public FeedItemStatistics(long feedID, int numberOfItems, int numberOfNewItems, int numberOfInProgressItems, Date lastUpdate) { + this.feedID = feedID; + this.numberOfItems = numberOfItems; + this.numberOfNewItems = numberOfNewItems; + this.numberOfInProgressItems = numberOfInProgressItems; + if (numberOfItems > 0) { + this.lastUpdate = (lastUpdate != null) ? (Date) lastUpdate.clone() : null; + } else { + this.lastUpdate = UNKNOWN_DATE; + } + } + + public long getFeedID() { + return feedID; + } + + public int getNumberOfItems() { + return numberOfItems; + } + + public int getNumberOfNewItems() { + return numberOfNewItems; + } + + public int getNumberOfInProgressItems() { + return numberOfInProgressItems; + } + + /** + * Returns the pubDate of the latest item in the feed. Users of this method + * should check if this value is unkown or not by calling lastUpdateKnown() first. + */ + public Date getLastUpdate() { + return (lastUpdate != null) ? (Date) lastUpdate.clone() : null; + } + + /** + * Returns true if the lastUpdate value is known. The lastUpdate value is unkown if the + * feed has no items. + */ + public boolean lastUpdateKnown() { + return lastUpdate != UNKNOWN_DATE; + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/FeedSearcher.java b/core/src/main/java/de/danoeh/antennapod/core/storage/FeedSearcher.java new file mode 100644 index 000000000..3a63685ba --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/FeedSearcher.java @@ -0,0 +1,57 @@ +package de.danoeh.antennapod.core.storage; + +import android.content.Context; +import de.danoeh.antennapod.core.R; +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.feed.SearchResult; +import de.danoeh.antennapod.core.util.comparator.SearchResultValueComparator; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.FutureTask; + +/** + * Performs search on Feeds and FeedItems + */ +public class FeedSearcher { + private static final String TAG = "FeedSearcher"; + + + /** + * Performs a search in all feeds or one specific feed. + */ + public static List 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/core/src/main/java/de/danoeh/antennapod/core/storage/PodDBAdapter.java b/core/src/main/java/de/danoeh/antennapod/core/storage/PodDBAdapter.java new file mode 100644 index 000000000..1407080dc --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/PodDBAdapter.java @@ -0,0 +1,1310 @@ +package de.danoeh.antennapod.core.storage; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.DatabaseUtils; +import android.database.MergeCursor; +import android.database.SQLException; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteDatabase.CursorFactory; +import android.database.sqlite.SQLiteOpenHelper; +import android.util.Log; + +import org.apache.commons.lang3.Validate; + +import java.util.Arrays; +import java.util.List; + +import de.danoeh.antennapod.core.BuildConfig; +import de.danoeh.antennapod.core.ClientConfig; +import de.danoeh.antennapod.core.feed.Chapter; +import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.core.feed.FeedComponent; +import de.danoeh.antennapod.core.feed.FeedImage; +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.feed.FeedMedia; +import de.danoeh.antennapod.core.feed.FeedPreferences; +import de.danoeh.antennapod.core.service.download.DownloadStatus; +import de.danoeh.antennapod.core.util.flattr.FlattrStatus; + +// TODO Remove media column from feeditem table + +/** + * Implements methods for accessing the database + */ +public class PodDBAdapter { + private static final String TAG = "PodDBAdapter"; + public static final String DATABASE_NAME = "Antennapod.db"; + + /** + * Maximum number of arguments for IN-operator. + */ + public static final int IN_OPERATOR_MAXIMUM = 800; + + /** + * Maximum number of entries per search request. + */ + public static final int SEARCH_LIMIT = 30; + + // ----------- Column indices + // ----------- General indices + public static final int KEY_ID_INDEX = 0; + public static final int KEY_TITLE_INDEX = 1; + public static final int KEY_FILE_URL_INDEX = 2; + public static final int KEY_DOWNLOAD_URL_INDEX = 3; + public static final int KEY_DOWNLOADED_INDEX = 4; + public static final int KEY_LINK_INDEX = 5; + public static final int KEY_DESCRIPTION_INDEX = 6; + public static final int KEY_PAYMENT_LINK_INDEX = 7; + // ----------- Feed indices + public static final int KEY_LAST_UPDATE_INDEX = 8; + public static final int KEY_LANGUAGE_INDEX = 9; + public static final int KEY_AUTHOR_INDEX = 10; + public static final int KEY_IMAGE_INDEX = 11; + public static final int KEY_TYPE_INDEX = 12; + public static final int KEY_FEED_IDENTIFIER_INDEX = 13; + public static final int KEY_FEED_FLATTR_STATUS_INDEX = 14; + public static final int KEY_FEED_USERNAME_INDEX = 15; + public static final int KEY_FEED_PASSWORD_INDEX = 16; + // ----------- FeedItem indices + public static final int KEY_CONTENT_ENCODED_INDEX = 2; + public static final int KEY_PUBDATE_INDEX = 3; + public static final int KEY_READ_INDEX = 4; + public static final int KEY_MEDIA_INDEX = 8; + public static final int KEY_FEED_INDEX = 9; + public static final int KEY_HAS_SIMPLECHAPTERS_INDEX = 10; + public static final int KEY_ITEM_IDENTIFIER_INDEX = 11; + public static final int KEY_ITEM_FLATTR_STATUS_INDEX = 12; + // ---------- FeedMedia indices + public static final int KEY_DURATION_INDEX = 1; + public static final int KEY_POSITION_INDEX = 5; + public static final int KEY_SIZE_INDEX = 6; + public static final int KEY_MIME_TYPE_INDEX = 7; + public static final int KEY_PLAYBACK_COMPLETION_DATE_INDEX = 8; + public static final int KEY_MEDIA_FEEDITEM_INDEX = 9; + public static final int KEY_PLAYED_DURATION_INDEX = 10; + // --------- Download log indices + public static final int KEY_FEEDFILE_INDEX = 1; + public static final int KEY_FEEDFILETYPE_INDEX = 2; + public static final int KEY_REASON_INDEX = 3; + public static final int KEY_SUCCESSFUL_INDEX = 4; + public static final int KEY_COMPLETION_DATE_INDEX = 5; + public static final int KEY_REASON_DETAILED_INDEX = 6; + public static final int KEY_DOWNLOADSTATUS_TITLE_INDEX = 7; + // --------- Queue indices + public static final int KEY_FEEDITEM_INDEX = 1; + public static final int KEY_QUEUE_FEED_INDEX = 2; + // --------- Chapters indices + public static final int KEY_CHAPTER_START_INDEX = 2; + public static final int KEY_CHAPTER_FEEDITEM_INDEX = 3; + public static final int KEY_CHAPTER_LINK_INDEX = 4; + public static final int KEY_CHAPTER_TYPE_INDEX = 5; + + // Key-constants + public static final String KEY_ID = "id"; + public static final String KEY_TITLE = "title"; + public static final String KEY_NAME = "name"; + public static final String KEY_LINK = "link"; + public static final String KEY_DESCRIPTION = "description"; + public static final String KEY_FILE_URL = "file_url"; + public static final String KEY_DOWNLOAD_URL = "download_url"; + public static final String KEY_PUBDATE = "pubDate"; + public static final String KEY_READ = "read"; + public static final String KEY_DURATION = "duration"; + public static final String KEY_POSITION = "position"; + public static final String KEY_SIZE = "filesize"; + public static final String KEY_MIME_TYPE = "mime_type"; + public static final String KEY_IMAGE = "image"; + public static final String KEY_FEED = "feed"; + public static final String KEY_MEDIA = "media"; + public static final String KEY_DOWNLOADED = "downloaded"; + public static final String KEY_LASTUPDATE = "last_update"; + public static final String KEY_FEEDFILE = "feedfile"; + public static final String KEY_REASON = "reason"; + public static final String KEY_SUCCESSFUL = "successful"; + public static final String KEY_FEEDFILETYPE = "feedfile_type"; + public static final String KEY_COMPLETION_DATE = "completion_date"; + public static final String KEY_FEEDITEM = "feeditem"; + public static final String KEY_CONTENT_ENCODED = "content_encoded"; + public static final String KEY_PAYMENT_LINK = "payment_link"; + public static final String KEY_START = "start"; + public static final String KEY_LANGUAGE = "language"; + public static final String KEY_AUTHOR = "author"; + public static final String KEY_HAS_CHAPTERS = "has_simple_chapters"; + public static final String KEY_TYPE = "type"; + public static final String KEY_ITEM_IDENTIFIER = "item_identifier"; + public static final String KEY_FLATTR_STATUS = "flattr_status"; + public static final String KEY_FEED_IDENTIFIER = "feed_identifier"; + public static final String KEY_REASON_DETAILED = "reason_detailed"; + public static final String KEY_DOWNLOADSTATUS_TITLE = "title"; + public static final String KEY_CHAPTER_TYPE = "type"; + public static final String KEY_PLAYBACK_COMPLETION_DATE = "playback_completion_date"; + public static final String KEY_AUTO_DOWNLOAD = "auto_download"; + public static final String KEY_PLAYED_DURATION = "played_duration"; + public static final String KEY_USERNAME = "username"; + public static final String KEY_PASSWORD = "password"; + + // Table names + public static final String TABLE_NAME_FEEDS = "Feeds"; + public static final String TABLE_NAME_FEED_ITEMS = "FeedItems"; + public static final String TABLE_NAME_FEED_IMAGES = "FeedImages"; + public static final String TABLE_NAME_FEED_MEDIA = "FeedMedia"; + public static final String TABLE_NAME_DOWNLOAD_LOG = "DownloadLog"; + public static final String TABLE_NAME_QUEUE = "Queue"; + public static final String TABLE_NAME_SIMPLECHAPTERS = "SimpleChapters"; + + // SQL Statements for creating new tables + private static final String TABLE_PRIMARY_KEY = KEY_ID + + " INTEGER PRIMARY KEY AUTOINCREMENT ,"; + + 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, + ClientConfig.storageCallbacks.getDatabaseVersion()); + } + return dbHelperSingleton; + } + + public PodDBAdapter(Context c) { + this.context = c; + helper = getDbHelperSingleton(c.getApplicationContext()); + } + + public PodDBAdapter open() { + if (db == null || !db.isOpen() || db.isReadOnly()) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Opening DB"); + try { + db = helper.getWritableDatabase(); + } catch (SQLException ex) { + ex.printStackTrace(); + db = helper.getReadableDatabase(); + } + } + return this; + } + + public void close() { + if (BuildConfig.DEBUG) + Log.d(TAG, "Closing DB"); + //db.close(); + } + + public static boolean deleteDatabase(Context context) { + Log.w(TAG, "Deleting database"); + dbHelperSingleton.close(); + dbHelperSingleton = null; + return context.deleteDatabase(DATABASE_NAME); + } + + /** + * Inserts or updates a feed entry + * + * @return the id of the entry + */ + public long setFeed(Feed feed) { + ContentValues values = new ContentValues(); + values.put(KEY_TITLE, feed.getTitle()); + values.put(KEY_LINK, feed.getLink()); + values.put(KEY_DESCRIPTION, feed.getDescription()); + values.put(KEY_PAYMENT_LINK, feed.getPaymentLink()); + values.put(KEY_AUTHOR, feed.getAuthor()); + values.put(KEY_LANGUAGE, feed.getLanguage()); + if (feed.getImage() != null) { + if (feed.getImage().getId() == 0) { + setImage(feed.getImage()); + } + values.put(KEY_IMAGE, feed.getImage().getId()); + } + + values.put(KEY_FILE_URL, feed.getFile_url()); + values.put(KEY_DOWNLOAD_URL, feed.getDownload_url()); + values.put(KEY_DOWNLOADED, feed.isDownloaded()); + values.put(KEY_LASTUPDATE, feed.getLastUpdate().getTime()); + values.put(KEY_TYPE, feed.getType()); + values.put(KEY_FEED_IDENTIFIER, feed.getFeedIdentifier()); + + Log.d(TAG, "Setting feed with flattr status " + feed.getTitle() + ": " + feed.getFlattrStatus().toLong()); + + values.put(KEY_FLATTR_STATUS, feed.getFlattrStatus().toLong()); + if (feed.getId() == 0) { + // Create new entry + if (BuildConfig.DEBUG) + Log.d(this.toString(), "Inserting new Feed into db"); + feed.setId(db.insert(TABLE_NAME_FEEDS, null, values)); + } else { + if (BuildConfig.DEBUG) + Log.d(this.toString(), "Updating existing Feed in db"); + db.update(TABLE_NAME_FEEDS, values, KEY_ID + "=?", + new String[]{String.valueOf(feed.getId())}); + + } + return feed.getId(); + } + + public void setFeedPreferences(FeedPreferences prefs) { + if (prefs.getFeedID() == 0) { + throw new IllegalArgumentException("Feed ID of preference must not be null"); + } + ContentValues values = new ContentValues(); + values.put(KEY_AUTO_DOWNLOAD, prefs.getAutoDownload()); + values.put(KEY_USERNAME, prefs.getUsername()); + values.put(KEY_PASSWORD, prefs.getPassword()); + db.update(TABLE_NAME_FEEDS, values, KEY_ID + "=?", new String[]{String.valueOf(prefs.getFeedID())}); + } + + /** + * Inserts or updates an image entry + * + * @return the id of the entry + */ + public long setImage(FeedImage image) { + db.beginTransaction(); + ContentValues values = new ContentValues(); + values.put(KEY_TITLE, image.getTitle()); + values.put(KEY_DOWNLOAD_URL, image.getDownload_url()); + values.put(KEY_DOWNLOADED, image.isDownloaded()); + values.put(KEY_FILE_URL, image.getFile_url()); + if (image.getId() == 0) { + image.setId(db.insert(TABLE_NAME_FEED_IMAGES, null, values)); + } else { + db.update(TABLE_NAME_FEED_IMAGES, values, KEY_ID + "=?", + new String[]{String.valueOf(image.getId())}); + } + + final FeedComponent owner = image.getOwner(); + if (owner != null && owner.getId() != 0) { + values.clear(); + values.put(KEY_IMAGE, image.getId()); + if (owner instanceof Feed) { + db.update(TABLE_NAME_FEEDS, values, KEY_ID + "=?", new String[]{String.valueOf(image.getOwner().getId())}); + } + } + db.setTransactionSuccessful(); + db.endTransaction(); + return image.getId(); + } + + /** + * Inserts or updates an image entry + * + * @return the id of the entry + */ + public long setMedia(FeedMedia media) { + ContentValues values = new ContentValues(); + values.put(KEY_DURATION, media.getDuration()); + values.put(KEY_POSITION, media.getPosition()); + values.put(KEY_SIZE, media.getSize()); + values.put(KEY_MIME_TYPE, media.getMime_type()); + values.put(KEY_DOWNLOAD_URL, media.getDownload_url()); + values.put(KEY_DOWNLOADED, media.isDownloaded()); + values.put(KEY_FILE_URL, media.getFile_url()); + + if (media.getPlaybackCompletionDate() != null) { + values.put(KEY_PLAYBACK_COMPLETION_DATE, media + .getPlaybackCompletionDate().getTime()); + } else { + values.put(KEY_PLAYBACK_COMPLETION_DATE, 0); + } + if (media.getItem() != null) { + values.put(KEY_FEEDITEM, media.getItem().getId()); + } + if (media.getId() == 0) { + media.setId(db.insert(TABLE_NAME_FEED_MEDIA, null, values)); + } else { + db.update(TABLE_NAME_FEED_MEDIA, values, KEY_ID + "=?", + new String[]{String.valueOf(media.getId())}); + } + return media.getId(); + } + + public void setFeedMediaPlaybackInformation(FeedMedia media) { + if (media.getId() != 0) { + ContentValues values = new ContentValues(); + values.put(KEY_POSITION, media.getPosition()); + values.put(KEY_DURATION, media.getDuration()); + values.put(KEY_PLAYED_DURATION, media.getPlayedDuration()); + db.update(TABLE_NAME_FEED_MEDIA, values, KEY_ID + "=?", + new String[]{String.valueOf(media.getId())}); + } else { + Log.e(TAG, "setFeedMediaPlaybackInformation: ID of media was 0"); + } + } + + public void setFeedMediaPlaybackCompletionDate(FeedMedia media) { + if (media.getId() != 0) { + ContentValues values = new ContentValues(); + values.put(KEY_PLAYBACK_COMPLETION_DATE, media.getPlaybackCompletionDate().getTime()); + values.put(KEY_PLAYED_DURATION, media.getPlayedDuration()); + db.update(TABLE_NAME_FEED_MEDIA, values, KEY_ID + "=?", + new String[]{String.valueOf(media.getId())}); + } else { + Log.e(TAG, "setFeedMediaPlaybackCompletionDate: ID of media was 0"); + } + } + + /** + * Insert all FeedItems of a feed and the feed object itself in a single + * transaction + */ + public void setCompleteFeed(Feed... feeds) { + db.beginTransaction(); + for (Feed feed : feeds) { + setFeed(feed); + if (feed.getItems() != null) { + for (FeedItem item : feed.getItems()) { + setFeedItem(item, false); + } + } + if (feed.getPreferences() != null) { + setFeedPreferences(feed.getPreferences()); + } + } + db.setTransactionSuccessful(); + db.endTransaction(); + } + + /** + * Update the flattr status of a feed + */ + public void setFeedFlattrStatus(Feed feed) { + ContentValues values = new ContentValues(); + values.put(KEY_FLATTR_STATUS, feed.getFlattrStatus().toLong()); + db.update(TABLE_NAME_FEEDS, values, KEY_ID + "=?", new String[]{String.valueOf(feed.getId())}); + } + + /** + * Get all feeds in the flattr queue. + */ + public Cursor getFeedsInFlattrQueueCursor() { + return db.query(TABLE_NAME_FEEDS, FEED_SEL_STD, KEY_FLATTR_STATUS + "=?", + new String[]{String.valueOf(FlattrStatus.STATUS_QUEUE)}, null, null, null); + } + + /** + * Get all feed items in the flattr queue. + */ + public Cursor getFeedItemsInFlattrQueueCursor() { + return db.query(TABLE_NAME_FEED_ITEMS, FEEDITEM_SEL_FI_SMALL, KEY_FLATTR_STATUS + "=?", + new String[]{String.valueOf(FlattrStatus.STATUS_QUEUE)}, null, null, null); + } + + /** + * Counts feeds and feed items in the flattr queue + */ + public int getFlattrQueueSize() { + int res = 0; + Cursor c = db.rawQuery(String.format("SELECT count(*) FROM %s WHERE %s=%s", + TABLE_NAME_FEEDS, KEY_FLATTR_STATUS, String.valueOf(FlattrStatus.STATUS_QUEUE)), null); + if (c.moveToFirst()) { + res = c.getInt(0); + c.close(); + } else { + Log.e(TAG, "Unable to determine size of flattr queue: Could not count number of feeds"); + } + c = db.rawQuery(String.format("SELECT count(*) FROM %s WHERE %s=%s", + TABLE_NAME_FEED_ITEMS, KEY_FLATTR_STATUS, String.valueOf(FlattrStatus.STATUS_QUEUE)), null); + if (c.moveToFirst()) { + res += c.getInt(0); + c.close(); + } else { + Log.e(TAG, "Unable to determine size of flattr queue: Could not count number of feed items"); + } + + return res; + } + + /** + * Updates the download URL of a Feed. + */ + public void setFeedDownloadUrl(String original, String updated) { + ContentValues values = new ContentValues(); + values.put(KEY_DOWNLOAD_URL, updated); + db.update(TABLE_NAME_FEEDS, values, KEY_DOWNLOAD_URL + "=?", new String[]{original}); + } + + public void setFeedItemlist(List 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) { + ClientConfig.storageCallbacks.onUpgrade(db, oldVersion, newVersion); + } + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/FeedHandler.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/FeedHandler.java new file mode 100644 index 000000000..9efc5888f --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/FeedHandler.java @@ -0,0 +1,34 @@ +package de.danoeh.antennapod.core.syndication.handler; + +import de.danoeh.antennapod.core.feed.Feed; +import org.apache.commons.io.input.XmlStreamReader; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; + +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; +import java.io.File; +import java.io.IOException; +import java.io.Reader; + +public class FeedHandler { + + public FeedHandlerResult parseFeed(Feed feed) throws SAXException, IOException, + ParserConfigurationException, UnsupportedFeedtypeException { + TypeGetter tg = new TypeGetter(); + TypeGetter.Type type = tg.getType(feed); + SyndHandler handler = new SyndHandler(feed, type); + + SAXParserFactory factory = SAXParserFactory.newInstance(); + factory.setNamespaceAware(true); + SAXParser saxParser = factory.newSAXParser(); + File file = new File(feed.getFile_url()); + Reader inputStreamReader = new XmlStreamReader(file); + InputSource inputSource = new InputSource(inputStreamReader); + + saxParser.parse(inputSource, handler); + inputStreamReader.close(); + return new FeedHandlerResult(handler.state.feed, handler.state.alternateUrls); + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/FeedHandlerResult.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/FeedHandlerResult.java new file mode 100644 index 000000000..45d1413bf --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/FeedHandlerResult.java @@ -0,0 +1,19 @@ +package de.danoeh.antennapod.core.syndication.handler; + +import de.danoeh.antennapod.core.feed.Feed; + +import java.util.Map; + +/** + * Container for results returned by the Feed parser + */ +public class FeedHandlerResult { + + public Feed feed; + public Map alternateFeedUrls; + + public FeedHandlerResult(Feed feed, Map alternateFeedUrls) { + this.feed = feed; + this.alternateFeedUrls = alternateFeedUrls; + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/HandlerState.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/HandlerState.java new file mode 100644 index 000000000..4fe8e1aff --- /dev/null +++ b/core/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/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/SyndHandler.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/SyndHandler.java new file mode 100644 index 000000000..1dda24944 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/SyndHandler.java @@ -0,0 +1,126 @@ +package de.danoeh.antennapod.core.syndication.handler; + +import android.util.Log; +import de.danoeh.antennapod.core.BuildConfig; +import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.core.syndication.namespace.*; +import de.danoeh.antennapod.core.syndication.namespace.atom.NSAtom; +import org.xml.sax.Attributes; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.DefaultHandler; + +/** Superclass for all SAX Handlers which process Syndication formats */ +public class SyndHandler extends DefaultHandler { + private static final String TAG = "SyndHandler"; + private static final String DEFAULT_PREFIX = ""; + protected HandlerState state; + + public SyndHandler(Feed feed, TypeGetter.Type type) { + state = new HandlerState(feed); + if (type == TypeGetter.Type.RSS20 || type == TypeGetter.Type.RSS091) { + state.defaultNamespaces.push(new NSRSS20()); + } + } + + @Override + public void startElement(String uri, String localName, String qName, + Attributes attributes) throws SAXException { + state.contentBuf = new StringBuffer(); + Namespace handler = getHandlingNamespace(uri, qName); + if (handler != null) { + SyndElement element = handler.handleElementStart(localName, state, + attributes); + state.tagstack.push(element); + + } + } + + @Override + public void characters(char[] ch, int start, int length) + throws SAXException { + if (!state.tagstack.empty()) { + if (state.getTagstack().size() >= 2) { + if (state.contentBuf != null) { + state.contentBuf.append(ch, start, length); + } + } + } + } + + @Override + public void endElement(String uri, String localName, String qName) + throws SAXException { + Namespace handler = getHandlingNamespace(uri, qName); + if (handler != null) { + handler.handleElementEnd(localName, state); + state.tagstack.pop(); + + } + state.contentBuf = null; + + } + + @Override + public void endPrefixMapping(String prefix) throws SAXException { + if (state.defaultNamespaces.size() > 1 && prefix.equals(DEFAULT_PREFIX)) { + state.defaultNamespaces.pop(); + } + } + + @Override + public void startPrefixMapping(String prefix, String uri) + throws SAXException { + // Find the right namespace + if (!state.namespaces.containsKey(uri)) { + if (uri.equals(NSAtom.NSURI)) { + if (prefix.equals(DEFAULT_PREFIX)) { + state.defaultNamespaces.push(new NSAtom()); + } else if (prefix.equals(NSAtom.NSTAG)) { + state.namespaces.put(uri, new NSAtom()); + if (BuildConfig.DEBUG) + Log.d(TAG, "Recognized Atom namespace"); + } + } else if (uri.equals(NSContent.NSURI) + && prefix.equals(NSContent.NSTAG)) { + state.namespaces.put(uri, new NSContent()); + if (BuildConfig.DEBUG) + Log.d(TAG, "Recognized Content namespace"); + } else if (uri.equals(NSITunes.NSURI) + && prefix.equals(NSITunes.NSTAG)) { + state.namespaces.put(uri, new NSITunes()); + if (BuildConfig.DEBUG) + Log.d(TAG, "Recognized ITunes namespace"); + } else if (uri.equals(NSSimpleChapters.NSURI) + && prefix.matches(NSSimpleChapters.NSTAG)) { + state.namespaces.put(uri, new NSSimpleChapters()); + if (BuildConfig.DEBUG) + Log.d(TAG, "Recognized SimpleChapters namespace"); + } else if (uri.equals(NSMedia.NSURI) + && prefix.equals(NSMedia.NSTAG)) { + state.namespaces.put(uri, new NSMedia()); + if (BuildConfig.DEBUG) + Log.d(TAG, "Recognized media namespace"); + } + } + } + + private Namespace getHandlingNamespace(String uri, String qName) { + Namespace handler = state.namespaces.get(uri); + if (handler == null && !state.defaultNamespaces.empty() + && !qName.contains(":")) { + handler = state.defaultNamespaces.peek(); + } + return handler; + } + + @Override + public void endDocument() throws SAXException { + super.endDocument(); + state.getFeed().setItems(state.getItems()); + } + + public HandlerState getState() { + return state; + } + +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/TypeGetter.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/TypeGetter.java new file mode 100644 index 000000000..32cd538d5 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/TypeGetter.java @@ -0,0 +1,111 @@ +package de.danoeh.antennapod.core.syndication.handler; + +import android.util.Log; +import de.danoeh.antennapod.core.BuildConfig; +import de.danoeh.antennapod.core.feed.Feed; +import org.apache.commons.io.input.XmlStreamReader; +import org.jsoup.Jsoup; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlPullParserFactory; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.Reader; + +/** Gets the type of a specific feed by reading the root element. */ +public class TypeGetter { + private static final String TAG = "TypeGetter"; + + public enum Type { + RSS20, RSS091, ATOM, INVALID + } + + private static final String ATOM_ROOT = "feed"; + private static final String RSS_ROOT = "rss"; + + public Type getType(Feed feed) throws UnsupportedFeedtypeException { + XmlPullParserFactory factory; + if (feed.getFile_url() != null) { + try { + factory = XmlPullParserFactory.newInstance(); + factory.setNamespaceAware(true); + XmlPullParser xpp = factory.newPullParser(); + xpp.setInput(createReader(feed)); + int eventType = xpp.getEventType(); + + while (eventType != XmlPullParser.END_DOCUMENT) { + if (eventType == XmlPullParser.START_TAG) { + String tag = xpp.getName(); + if (tag.equals(ATOM_ROOT)) { + feed.setType(Feed.TYPE_ATOM1); + if (BuildConfig.DEBUG) + Log.d(TAG, "Recognized type Atom"); + return Type.ATOM; + } else if (tag.equals(RSS_ROOT)) { + String strVersion = xpp.getAttributeValue(null, + "version"); + if (strVersion != null) { + + if (strVersion.equals("2.0")) { + feed.setType(Feed.TYPE_RSS2); + if (BuildConfig.DEBUG) + Log.d(TAG, "Recognized type RSS 2.0"); + return Type.RSS20; + } else if (strVersion.equals("0.91") + || strVersion.equals("0.92")) { + if (BuildConfig.DEBUG) + Log.d(TAG, + "Recognized type RSS 0.91/0.92"); + return Type.RSS091; + } + } + throw new UnsupportedFeedtypeException(Type.INVALID); + } else { + if (BuildConfig.DEBUG) + Log.d(TAG, "Type is invalid"); + throw new UnsupportedFeedtypeException(Type.INVALID, tag); + } + } else { + eventType = xpp.next(); + } + } + + } catch (XmlPullParserException e) { + e.printStackTrace(); + // XML document might actually be a HTML document -> try to parse as HTML + String rootElement = null; + try { + if (Jsoup.parse(new File(feed.getFile_url()), null) != null) { + rootElement = "html"; + } + } catch (IOException e1) { + e1.printStackTrace(); + } finally { + throw new UnsupportedFeedtypeException(Type.INVALID, rootElement); + } + + } catch (IOException e) { + e.printStackTrace(); + } + } + if (BuildConfig.DEBUG) + Log.d(TAG, "Type is invalid"); + throw new UnsupportedFeedtypeException(Type.INVALID); + } + + private Reader createReader(Feed feed) { + Reader reader; + try { + reader = new XmlStreamReader(new File(feed.getFile_url())); + } catch (FileNotFoundException e) { + e.printStackTrace(); + return null; + } catch (IOException e) { + e.printStackTrace(); + return null; + } + return reader; + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/UnsupportedFeedtypeException.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/UnsupportedFeedtypeException.java new file mode 100644 index 000000000..3da9251d9 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/UnsupportedFeedtypeException.java @@ -0,0 +1,38 @@ +package de.danoeh.antennapod.core.syndication.handler; + +import de.danoeh.antennapod.core.syndication.handler.TypeGetter.Type; + +public class UnsupportedFeedtypeException extends Exception { + private static final long serialVersionUID = 9105878964928170669L; + private TypeGetter.Type type; + private String rootElement; + + public UnsupportedFeedtypeException(Type type) { + super(); + this.type = type; + } + + public UnsupportedFeedtypeException(Type type, String rootElement) { + this.type = type; + this.rootElement = rootElement; + } + + public TypeGetter.Type getType() { + return type; + } + + public String getRootElement() { + return rootElement; + } + + @Override + public String getMessage() { + if (type == TypeGetter.Type.INVALID) { + return "Invalid type"; + } else { + return "Type " + type + " not supported"; + } + } + + +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSContent.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSContent.java new file mode 100644 index 000000000..71bf69ffa --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSContent.java @@ -0,0 +1,25 @@ +package de.danoeh.antennapod.core.syndication.namespace; + +import de.danoeh.antennapod.core.syndication.handler.HandlerState; +import org.xml.sax.Attributes; + +public class NSContent extends Namespace { + public static final String NSTAG = "content"; + public static final String NSURI = "http://purl.org/rss/1.0/modules/content/"; + + private static final String ENCODED = "encoded"; + + @Override + public SyndElement handleElementStart(String localName, HandlerState state, + Attributes attributes) { + return new SyndElement(localName, this); + } + + @Override + public void handleElementEnd(String localName, HandlerState state) { + if (localName.equals(ENCODED)) { + state.getCurrentItem().setContentEncoded(state.getContentBuf().toString()); + } + } + +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSITunes.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSITunes.java new file mode 100644 index 000000000..fb794d7e0 --- /dev/null +++ b/core/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/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSMedia.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSMedia.java new file mode 100644 index 000000000..7f03f1139 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSMedia.java @@ -0,0 +1,68 @@ +package de.danoeh.antennapod.core.syndication.namespace; + +import android.util.Log; +import de.danoeh.antennapod.core.BuildConfig; +import de.danoeh.antennapod.core.feed.FeedMedia; +import de.danoeh.antennapod.core.syndication.handler.HandlerState; +import de.danoeh.antennapod.core.syndication.util.SyndTypeUtils; +import org.xml.sax.Attributes; + +import java.util.concurrent.TimeUnit; + +/** Processes tags from the http://search.yahoo.com/mrss/ namespace. */ +public class NSMedia extends Namespace { + private static final String TAG = "NSMedia"; + + public static final String NSTAG = "media"; + public static final String NSURI = "http://search.yahoo.com/mrss/"; + + private static final String CONTENT = "content"; + private static final String DOWNLOAD_URL = "url"; + private static final String SIZE = "fileSize"; + private static final String MIME_TYPE = "type"; + private static final String DURATION = "duration"; + + @Override + public SyndElement handleElementStart(String localName, HandlerState state, + Attributes attributes) { + if (localName.equals(CONTENT)) { + String url = attributes.getValue(DOWNLOAD_URL); + String type = attributes.getValue(MIME_TYPE); + if (state.getCurrentItem().getMedia() == null + && url != null + && (SyndTypeUtils.enclosureTypeValid(type) || ((type = SyndTypeUtils + .getValidMimeTypeFromUrl(url)) != null))) { + + long size = 0; + try { + size = Long.parseLong(attributes.getValue(SIZE)); + } catch (NumberFormatException e) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Length attribute could not be parsed."); + } + + int duration = 0; + try { + String durationStr = attributes.getValue(DURATION); + if (durationStr != null) { + duration = (int) TimeUnit.MILLISECONDS.convert( + Long.parseLong(durationStr), TimeUnit.SECONDS); + } + } catch (NumberFormatException e) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Duration attribute could not be parsed"); + } + + state.getCurrentItem().setMedia( + new FeedMedia(state.getCurrentItem(), url, size, type)); + } + } + return new SyndElement(localName, this); + } + + @Override + public void handleElementEnd(String localName, HandlerState state) { + + } + +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSRSS20.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSRSS20.java new file mode 100644 index 000000000..c29741456 --- /dev/null +++ b/core/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.core.BuildConfig; +import de.danoeh.antennapod.core.feed.FeedImage; +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.feed.FeedMedia; +import de.danoeh.antennapod.core.syndication.handler.HandlerState; +import de.danoeh.antennapod.core.syndication.util.SyndDateUtils; +import de.danoeh.antennapod.core.syndication.util.SyndTypeUtils; +import org.xml.sax.Attributes; + +/** + * SAX-Parser for reading RSS-Feeds + * + * @author daniel + * + */ +public class NSRSS20 extends Namespace { + private static final String TAG = "NSRSS20"; + public static final String NSTAG = "rss"; + public static final String NSURI = ""; + + public final static String CHANNEL = "channel"; + public final static String ITEM = "item"; + public final static String GUID = "guid"; + public final static String TITLE = "title"; + public final static String LINK = "link"; + public final static String DESCR = "description"; + public final static String PUBDATE = "pubDate"; + public final static String ENCLOSURE = "enclosure"; + public final static String IMAGE = "image"; + public final static String URL = "url"; + public final static String LANGUAGE = "language"; + + public final static String ENC_URL = "url"; + public final static String ENC_LEN = "length"; + public final static String ENC_TYPE = "type"; + + @Override + public SyndElement handleElementStart(String localName, HandlerState state, + Attributes attributes) { + if (localName.equals(ITEM)) { + state.setCurrentItem(new FeedItem()); + state.getItems().add(state.getCurrentItem()); + state.getCurrentItem().setFeed(state.getFeed()); + + } else if (localName.equals(ENCLOSURE)) { + String type = attributes.getValue(ENC_TYPE); + String url = attributes.getValue(ENC_URL); + if (state.getCurrentItem().getMedia() == null + && (SyndTypeUtils.enclosureTypeValid(type) || ((type = SyndTypeUtils + .getValidMimeTypeFromUrl(url)) != null))) { + + long size = 0; + try { + size = Long.parseLong(attributes.getValue(ENC_LEN)); + } catch (NumberFormatException e) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Length attribute could not be parsed."); + } + state.getCurrentItem().setMedia( + new FeedMedia(state.getCurrentItem(), url, size, type)); + } + + } else if (localName.equals(IMAGE)) { + if (state.getTagstack().size() >= 1) { + String parent = state.getTagstack().peek().getName(); + if (parent.equals(CHANNEL)) { + state.getFeed().setImage(new FeedImage()); + } + } + } + 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/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSSimpleChapters.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSSimpleChapters.java new file mode 100644 index 000000000..2b4a2767d --- /dev/null +++ b/core/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/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/Namespace.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/Namespace.java new file mode 100644 index 000000000..cf118d202 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/Namespace.java @@ -0,0 +1,21 @@ +package de.danoeh.antennapod.core.syndication.namespace; + +import de.danoeh.antennapod.core.syndication.handler.HandlerState; +import org.xml.sax.Attributes; + + +public abstract class Namespace { + public static final String NSTAG = null; + public static final String NSURI = null; + + /** Called by a Feedhandler when in startElement and it detects a namespace element + * @return The SyndElement to push onto the stack + * */ + public abstract SyndElement handleElementStart(String localName, HandlerState state, Attributes attributes); + + /** Called by a Feedhandler when in endElement and it detects a namespace element + * @return true if namespace handled the element, false if it ignored it + * */ + public abstract void handleElementEnd(String localName, HandlerState state); + +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/SyndElement.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/SyndElement.java new file mode 100644 index 000000000..8adcd2086 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/SyndElement.java @@ -0,0 +1,22 @@ +package de.danoeh.antennapod.core.syndication.namespace; + +/** Defines a XML Element that is pushed on the tagstack */ +public class SyndElement { + protected String name; + protected Namespace namespace; + + public SyndElement(String name, Namespace namespace) { + this.name = name; + this.namespace = namespace; + } + + public Namespace getNamespace() { + return namespace; + } + + public String getName() { + return name; + } + + +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/atom/AtomText.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/atom/AtomText.java new file mode 100644 index 000000000..43fe0edb7 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/atom/AtomText.java @@ -0,0 +1,46 @@ +package de.danoeh.antennapod.core.syndication.namespace.atom; + +import de.danoeh.antennapod.core.syndication.namespace.Namespace; +import de.danoeh.antennapod.core.syndication.namespace.SyndElement; +import org.apache.commons.lang3.StringEscapeUtils; + +/** Represents Atom Element which contains text (content, title, summary). */ +public class AtomText extends SyndElement { + public static final String TYPE_TEXT = "text"; + public static final String TYPE_HTML = "html"; + public static final String TYPE_XHTML = "xhtml"; + + private String type; + private String content; + + public AtomText(String name, Namespace namespace, String type) { + super(name, namespace); + this.type = type; + } + + /** Processes the content according to the type and returns it. */ + public String getProcessedContent() { + if (type == null) { + return content; + } else if (type.equals(TYPE_HTML)) { + return StringEscapeUtils.unescapeHtml4(content); + } else if (type.equals(TYPE_XHTML)) { + return content; + } else { // Handle as text by default + return content; + } + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + + public String getType() { + return type; + } + +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/atom/NSAtom.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/atom/NSAtom.java new file mode 100644 index 000000000..61cb9ec65 --- /dev/null +++ b/core/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.core.BuildConfig; +import de.danoeh.antennapod.core.feed.FeedImage; +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.feed.FeedMedia; +import de.danoeh.antennapod.core.syndication.handler.HandlerState; +import de.danoeh.antennapod.core.syndication.namespace.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/core/src/main/java/de/danoeh/antennapod/core/syndication/util/SyndDateUtils.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/util/SyndDateUtils.java new file mode 100644 index 000000000..977d92304 --- /dev/null +++ b/core/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/core/src/main/java/de/danoeh/antennapod/core/syndication/util/SyndTypeUtils.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/util/SyndTypeUtils.java new file mode 100644 index 000000000..8d1d8ffde --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/util/SyndTypeUtils.java @@ -0,0 +1,42 @@ +package de.danoeh.antennapod.core.syndication.util; + +import android.webkit.MimeTypeMap; +import org.apache.commons.io.FilenameUtils; + +/** Utility class for handling MIME-Types of enclosures */ +public class SyndTypeUtils { + + private final static String VALID_MIMETYPE = "audio/.*" + "|" + "video/.*" + + "|" + "application/ogg"; + + private SyndTypeUtils() { + + } + + public static boolean enclosureTypeValid(String type) { + if (type == null) { + return false; + } else { + return type.matches(VALID_MIMETYPE); + } + } + + /** + * Should be used if mime-type of enclosure tag is not supported. This + * method will check if the mime-type of the file extension is supported. If + * the type is not supported, this method will return null. + */ + public static String getValidMimeTypeFromUrl(String url) { + if (url != null) { + String extension = FilenameUtils.getExtension(url); + if (extension != null) { + String type = MimeTypeMap.getSingleton() + .getMimeTypeFromExtension(extension); + if (type != null && enclosureTypeValid(type)) { + return type; + } + } + } + return null; + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/ChapterUtils.java b/core/src/main/java/de/danoeh/antennapod/core/util/ChapterUtils.java new file mode 100644 index 000000000..759a60f43 --- /dev/null +++ b/core/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.core.BuildConfig; +import de.danoeh.antennapod.core.feed.Chapter; +import de.danoeh.antennapod.core.util.comparator.ChapterStartTimeComparator; +import de.danoeh.antennapod.core.util.id3reader.ChapterReader; +import de.danoeh.antennapod.core.util.id3reader.ID3ReaderException; +import de.danoeh.antennapod.core.util.playback.Playable; +import de.danoeh.antennapod.core.util.vorbiscommentreader.VorbisCommentChapterReader; +import de.danoeh.antennapod.core.util.vorbiscommentreader.VorbisCommentReaderException; +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/core/src/main/java/de/danoeh/antennapod/core/util/Converter.java b/core/src/main/java/de/danoeh/antennapod/core/util/Converter.java new file mode 100644 index 000000000..a0b514bd6 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/Converter.java @@ -0,0 +1,103 @@ +package de.danoeh.antennapod.core.util; + +import android.util.Log; + +/** Provides methods for converting various units. */ +public final class Converter { + /** Class shall not be instantiated. */ + private Converter() { + } + + /** Logging tag. */ + private static final String TAG = "Converter"; + + + /** Indicates that the value is in the Byte range.*/ + private static final int B_RANGE = 0; + /** Indicates that the value is in the Kilobyte range.*/ + private static final int KB_RANGE = 1; + /** Indicates that the value is in the Megabyte range.*/ + private static final int MB_RANGE = 2; + /** Indicates that the value is in the Gigabyte range.*/ + private static final int GB_RANGE = 3; + /** Determines the length of the number for best readability.*/ + private static final int NUM_LENGTH = 1024; + + + private static final int HOURS_MIL = 3600000; + private static final int MINUTES_MIL = 60000; + private static final int SECONDS_MIL = 1000; + + /** Takes a byte-value and converts it into a more readable + * String. + * @param input The value to convert + * @return The converted String with a unit + * */ + public static String byteToString(final long input) { + int i = 0; + int result = 0; + + for (i = 0; i < GB_RANGE + 1; i++) { + result = (int) (input / Math.pow(1024, i)); + if (result < NUM_LENGTH) { + break; + } + } + + switch (i) { + case B_RANGE: + return result + " B"; + case KB_RANGE: + return result + " KB"; + case MB_RANGE: + return result + " MB"; + case GB_RANGE: + return result + " GB"; + default: + Log.e(TAG, "Error happened in byteToString"); + return "ERROR"; + } + } + + /** Converts milliseconds to a string containing hours, minutes and seconds */ + public static String getDurationStringLong(int duration) { + int h = duration / HOURS_MIL; + int rest = duration - h * HOURS_MIL; + int m = rest / MINUTES_MIL; + rest -= m * MINUTES_MIL; + int s = rest / SECONDS_MIL; + + return String.format("%02d:%02d:%02d", h, m, s); + } + + /** Converts milliseconds to a string containing hours and minutes */ + public static String getDurationStringShort(int duration) { + int h = duration / HOURS_MIL; + int rest = duration - h * HOURS_MIL; + int m = rest / MINUTES_MIL; + + return String.format("%02d:%02d", h, m); + } + + /** Converts long duration string (HH:MM:SS) to milliseconds. */ + public static int durationStringLongToMs(String input) { + String[] parts = input.split(":"); + if (parts.length != 3) { + return 0; + } + return Integer.valueOf(parts[0]) * 3600 * 1000 + + Integer.valueOf(parts[1]) * 60 * 1000 + + Integer.valueOf(parts[2]) * 1000; + } + + /** Converts short duration string (HH:MM) to milliseconds. */ + public static int durationStringShortToMs(String input) { + String[] parts = input.split(":"); + if (parts.length != 2) { + return 0; + } + return Integer.valueOf(parts[0]) * 3600 * 1000 + + Integer.valueOf(parts[1]) * 1000 * 60; + } + +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/DownloadError.java b/core/src/main/java/de/danoeh/antennapod/core/util/DownloadError.java new file mode 100644 index 000000000..602c221bf --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/DownloadError.java @@ -0,0 +1,52 @@ +package de.danoeh.antennapod.core.util; + +import android.content.Context; +import de.danoeh.antennapod.core.R; + +/** Utility class for Download Errors. */ +public enum DownloadError { + SUCCESS(0, R.string.download_successful), + ERROR_PARSER_EXCEPTION(1, R.string.download_error_parser_exception), + ERROR_UNSUPPORTED_TYPE(2, R.string.download_error_unsupported_type), + ERROR_CONNECTION_ERROR(3, R.string.download_error_connection_error), + ERROR_MALFORMED_URL(4, R.string.download_error_error_unknown), + ERROR_IO_ERROR(5, R.string.download_error_io_error), + ERROR_FILE_EXISTS(6, R.string.download_error_error_unknown), + ERROR_DOWNLOAD_CANCELLED(7, R.string.download_error_error_unknown), + ERROR_DEVICE_NOT_FOUND(8, R.string.download_error_device_not_found), + ERROR_HTTP_DATA_ERROR(9, R.string.download_error_http_data_error), + ERROR_NOT_ENOUGH_SPACE(10, R.string.download_error_insufficient_space), + ERROR_UNKNOWN_HOST(11, R.string.download_error_unknown_host), + ERROR_REQUEST_ERROR(12, R.string.download_error_request_error), + ERROR_DB_ACCESS_ERROR(13, R.string.download_error_db_access), + ERROR_UNAUTHORIZED(14, R.string.download_error_unauthorized); + + private final int code; + private final int resId; + + private DownloadError(int code, int resId) { + this.code = code; + this.resId = resId; + } + + /** Return DownloadError from its associated code. */ + public static DownloadError fromCode(int code) { + for (DownloadError reason : values()) { + if (reason.getCode() == code) { + return reason; + } + } + throw new IllegalArgumentException("unknown code: " + code); + } + + /** Get machine-readable code. */ + public int getCode() { + return code; + } + + /** Get a human-readable string. */ + public String getErrorString(Context context) { + return context.getString(resId); + } + +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/DuckType.java b/core/src/main/java/de/danoeh/antennapod/core/util/DuckType.java new file mode 100644 index 000000000..f432424f8 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/DuckType.java @@ -0,0 +1,117 @@ +/* Adapted from: http://thinking-in-code.blogspot.com/2008/11/duck-typing-in-java-using-dynamic.html */ + +package de.danoeh.antennapod.core.util; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; + +import de.danoeh.antennapod.core.BuildConfig; + +/** + * Allows "duck typing" or dynamic invocation based on method signature rather + * than type hierarchy. In other words, rather than checking whether something + * IS-a duck, check whether it WALKS-like-a duck or QUACKS-like a duck. + * + * To use first use the coerce static method to indicate the object you want to + * do Duck Typing for, then specify an interface to the to method which you want + * to coerce the type to, e.g: + * + * public interface Foo { void aMethod(); } class Bar { ... public void + * aMethod() { ... } ... } Bar bar = ...; Foo foo = + * DuckType.coerce(bar).to(Foo.class); foo.aMethod(); + * + * + */ +public class DuckType { + + private final Object objectToCoerce; + + private DuckType(Object objectToCoerce) { + this.objectToCoerce = objectToCoerce; + } + + private class CoercedProxy implements InvocationHandler { + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + Method delegateMethod = findMethodBySignature(method); + assert delegateMethod != null; + return delegateMethod.invoke(DuckType.this.objectToCoerce, args); + } + } + + /** + * Specify the duck typed object to coerce. + * + * @param object + * the object to coerce + * @return + */ + public static DuckType coerce(Object object) { + return new DuckType(object); + } + + /** + * Coerce the Duck Typed object to the given interface providing it + * implements all the necessary methods. + * + * @param + * @param iface + * @return an instance of the given interface that wraps the duck typed + * class + * @throws ClassCastException + * if the object being coerced does not implement all the + * methods in the given interface. + */ + @SuppressWarnings({ "rawtypes", "unchecked" }) + public T 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/core/src/main/java/de/danoeh/antennapod/core/util/EpisodeFilter.java b/core/src/main/java/de/danoeh/antennapod/core/util/EpisodeFilter.java new file mode 100644 index 000000000..4c23b161b --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/EpisodeFilter.java @@ -0,0 +1,49 @@ +package de.danoeh.antennapod.core.util; + +import de.danoeh.antennapod.core.feed.FeedItem; + +import java.util.ArrayList; +import java.util.List; + +public class EpisodeFilter { + private EpisodeFilter() { + + } + + /** Return a copy of the itemlist without items which have no media. */ + public static ArrayList 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/core/src/main/java/de/danoeh/antennapod/core/util/FeedtitleComparator.java b/core/src/main/java/de/danoeh/antennapod/core/util/FeedtitleComparator.java new file mode 100644 index 000000000..bf14cd23e --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/FeedtitleComparator.java @@ -0,0 +1,15 @@ +package de.danoeh.antennapod.core.util; + +import de.danoeh.antennapod.core.feed.Feed; + +import java.util.Comparator; + +/** Compares the title of two feeds for sorting. */ +public class FeedtitleComparator implements Comparator { + + @Override + public int compare(Feed lhs, Feed rhs) { + return lhs.getTitle().compareToIgnoreCase(rhs.getTitle()); + } + +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/FileNameGenerator.java b/core/src/main/java/de/danoeh/antennapod/core/util/FileNameGenerator.java new file mode 100644 index 000000000..00c023b64 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/FileNameGenerator.java @@ -0,0 +1,36 @@ +package de.danoeh.antennapod.core.util; + +import java.util.Arrays; + +/** Generates valid filenames for a given string. */ +public class FileNameGenerator { + + private static final char[] ILLEGAL_CHARACTERS = { '/', '\\', '?', '%', + '*', ':', '|', '"', '<', '>' }; + static { + Arrays.sort(ILLEGAL_CHARACTERS); + } + + private FileNameGenerator() { + + } + + /** + * This method will return a new string that doesn't contain any illegal + * characters of the given string. + */ + public static String generateFileName(String string) { + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < string.length(); i++) { + char c = string.charAt(i); + if (Arrays.binarySearch(ILLEGAL_CHARACTERS, c) < 0) { + builder.append(c); + } + } + return builder.toString().replaceFirst(" *$",""); + } + + public static long generateLong(final String str) { + return str.hashCode(); + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/InvalidFeedException.java b/core/src/main/java/de/danoeh/antennapod/core/util/InvalidFeedException.java new file mode 100644 index 000000000..c98c2d82a --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/InvalidFeedException.java @@ -0,0 +1,21 @@ +package de.danoeh.antennapod.core.util; + +/** Thrown if a feed has invalid attribute values. */ +public class InvalidFeedException extends Exception { + + public InvalidFeedException() { + } + + public InvalidFeedException(String detailMessage) { + super(detailMessage); + } + + public InvalidFeedException(Throwable throwable) { + super(throwable); + } + + public InvalidFeedException(String detailMessage, Throwable throwable) { + super(detailMessage, throwable); + } + +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/LangUtils.java b/core/src/main/java/de/danoeh/antennapod/core/util/LangUtils.java new file mode 100644 index 000000000..07432d28a --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/LangUtils.java @@ -0,0 +1,120 @@ +package de.danoeh.antennapod.core.util; + +import java.nio.charset.Charset; +import java.util.HashMap; + +public class LangUtils { + public static final Charset UTF_8 = Charset.forName("UTF-8"); + + private static HashMap 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/core/src/main/java/de/danoeh/antennapod/core/util/NetworkUtils.java b/core/src/main/java/de/danoeh/antennapod/core/util/NetworkUtils.java new file mode 100644 index 000000000..b321536a3 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/NetworkUtils.java @@ -0,0 +1,69 @@ +package de.danoeh.antennapod.core.util; + +import android.content.Context; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.net.wifi.WifiInfo; +import android.net.wifi.WifiManager; +import android.util.Log; +import de.danoeh.antennapod.core.BuildConfig; +import de.danoeh.antennapod.core.preferences.UserPreferences; + +import java.util.Arrays; +import java.util.List; + +public class NetworkUtils { + private static final String TAG = "NetworkUtils"; + + private NetworkUtils() { + + } + + /** + * Returns true if the device is connected to Wi-Fi and the Wi-Fi filter for + * automatic downloads is disabled or the device is connected to a Wi-Fi + * network that is on the 'selected networks' list of the Wi-Fi filter for + * automatic downloads and false otherwise. + * */ + public static boolean autodownloadNetworkAvailable(Context context) { + ConnectivityManager cm = (ConnectivityManager) context + .getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo networkInfo = cm.getActiveNetworkInfo(); + if (networkInfo != null) { + if (networkInfo.getType() == ConnectivityManager.TYPE_WIFI) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Device is connected to Wi-Fi"); + if (networkInfo.isConnected()) { + if (!UserPreferences.isEnableAutodownloadWifiFilter()) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Auto-dl filter is disabled"); + return true; + } else { + WifiManager wm = (WifiManager) context + .getSystemService(Context.WIFI_SERVICE); + WifiInfo wifiInfo = wm.getConnectionInfo(); + List selectedNetworks = Arrays + .asList(UserPreferences + .getAutodownloadSelectedNetworks()); + if (selectedNetworks.contains(Integer.toString(wifiInfo + .getNetworkId()))) { + if (BuildConfig.DEBUG) + Log.d(TAG, + "Current network is on the selected networks list"); + return true; + } + } + } + } + } + if (BuildConfig.DEBUG) + Log.d(TAG, "Network for auto-dl is not available"); + return false; + } + + public static boolean networkAvailable(Context context) { + ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo info = cm.getActiveNetworkInfo(); + return info != null && info.isConnected(); + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/QueueAccess.java b/core/src/main/java/de/danoeh/antennapod/core/util/QueueAccess.java new file mode 100644 index 000000000..8e40ae184 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/QueueAccess.java @@ -0,0 +1,93 @@ +package de.danoeh.antennapod.core.util; + +import de.danoeh.antennapod.core.feed.FeedItem; + +import java.util.Iterator; +import java.util.List; + +/** + * Provides methods for accessing the queue. It is possible to load only a part of the information about the queue that + * is stored in the database (e.g. sometimes the user just has to test if a specific item is contained in the List. + * QueueAccess provides an interface for accessing the queue without having to care about the type of the queue + * representation. + */ +public abstract class QueueAccess { + /** + * Returns true if the item is in the queue, false otherwise. + */ + public abstract boolean contains(long id); + + /** + * Removes the item from the queue. + * + * @return true if the queue was modified by this operation. + */ + public abstract boolean remove(long id); + + private QueueAccess() { + + } + + public static QueueAccess IDListAccess(final List 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/core/src/main/java/de/danoeh/antennapod/core/util/ShareUtils.java b/core/src/main/java/de/danoeh/antennapod/core/util/ShareUtils.java new file mode 100644 index 000000000..85f32ed50 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/ShareUtils.java @@ -0,0 +1,34 @@ +package de.danoeh.antennapod.core.util; + +import android.content.Context; +import android.content.Intent; +import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.core.feed.FeedItem; + +/** Utility methods for sharing data */ +public class ShareUtils { + private static final String TAG = "ShareUtils"; + + private ShareUtils() {} + + public static void shareLink(Context context, String link) { + Intent i = new Intent(Intent.ACTION_SEND); + i.setType("text/plain"); + i.putExtra(Intent.EXTRA_SUBJECT, "Sharing URL"); + i.putExtra(Intent.EXTRA_TEXT, link); + context.startActivity(Intent.createChooser(i, "Share URL")); + } + + public static void shareFeedItemLink(Context context, FeedItem item) { + shareLink(context, item.getLink()); + } + + public static void shareFeedDownloadLink(Context context, Feed feed) { + shareLink(context, feed.getDownload_url()); + } + + public static void shareFeedlink(Context context, Feed feed) { + shareLink(context, feed.getLink()); + } + +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/ShownotesProvider.java b/core/src/main/java/de/danoeh/antennapod/core/util/ShownotesProvider.java new file mode 100644 index 000000000..7e7c6c08b --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/ShownotesProvider.java @@ -0,0 +1,16 @@ +package de.danoeh.antennapod.core.util; + +import java.util.concurrent.Callable; + +/** + * Created by daniel on 04.08.13. + */ +public interface ShownotesProvider { + /** + * Loads shownotes. If the shownotes have to be loaded from a file or from a + * database, it should be done in a separate thread. After the shownotes + * have been loaded, callback.onShownotesLoaded should be called. + */ + public Callable loadShownotes(); + +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/StorageUtils.java b/core/src/main/java/de/danoeh/antennapod/core/util/StorageUtils.java new file mode 100644 index 000000000..dea380937 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/StorageUtils.java @@ -0,0 +1,67 @@ +package de.danoeh.antennapod.core.util; + +import android.app.Activity; +import android.content.Context; +import android.os.Build; +import android.os.StatFs; +import android.util.Log; + +import java.io.File; + +import de.danoeh.antennapod.core.BuildConfig; +import de.danoeh.antennapod.core.ClientConfig; +import de.danoeh.antennapod.core.preferences.UserPreferences; + +/** + * Utility functions for handling storage errors + */ +public class StorageUtils { + private static final String TAG = "StorageUtils"; + + public static boolean storageAvailable(Context context) { + File dir = UserPreferences.getDataFolder(context, null); + if (dir != null) { + return dir.exists() && dir.canRead() && dir.canWrite(); + } else { + if (BuildConfig.DEBUG) + Log.d(TAG, "Storage not available: data folder is null"); + return false; + } + } + + /** + * Checks if external storage is available. If external storage isn't + * available, the current activity is finsished an an error activity is + * launched. + * + * @param activity the activity which would be finished if no storage is + * available + * @return true if external storage is available + */ + public static boolean checkStorageAvailability(Activity activity) { + boolean storageAvailable = storageAvailable(activity); + if (!storageAvailable) { + activity.finish(); + activity.startActivity(ClientConfig.applicationCallbacks.getStorageErrorActivity(activity)); + } + return storageAvailable; + } + + /** + * Get the number of free bytes that are available on the external storage. + */ + public static long getFreeSpaceAvailable() { + StatFs stat = new StatFs(UserPreferences.getDataFolder( + ClientConfig.applicationCallbacks.getApplicationInstance(), null).getAbsolutePath()); + long availableBlocks; + long blockSize; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { + availableBlocks = stat.getAvailableBlocksLong(); + blockSize = stat.getBlockSizeLong(); + } else { + availableBlocks = stat.getAvailableBlocks(); + blockSize = stat.getBlockSize(); + } + return availableBlocks * blockSize; + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/ThemeUtils.java b/core/src/main/java/de/danoeh/antennapod/core/util/ThemeUtils.java new file mode 100644 index 000000000..f67367643 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/ThemeUtils.java @@ -0,0 +1,23 @@ +package de.danoeh.antennapod.core.util; + +import android.util.Log; + +import de.danoeh.antennapod.core.R; +import de.danoeh.antennapod.core.preferences.UserPreferences; + +public class ThemeUtils { + private static final String TAG = "ThemeUtils"; + + public static int getSelectionBackgroundColor() { + int theme = UserPreferences.getTheme(); + if (theme == R.style.Theme_AntennaPod_Dark) { + return R.color.selection_background_color_dark; + } else if (theme == R.style.Theme_AntennaPod_Light) { + return R.color.selection_background_color_light; + } else { + Log.e(TAG, + "getSelectionBackgroundColor could not match the current theme to any color!"); + return R.color.selection_background_color_light; + } + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/URIUtil.java b/core/src/main/java/de/danoeh/antennapod/core/util/URIUtil.java new file mode 100644 index 000000000..092c06b4a --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/URIUtil.java @@ -0,0 +1,35 @@ +package de.danoeh.antennapod.core.util; + +import android.util.Log; +import de.danoeh.antennapod.core.BuildConfig; + +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; + +/** + * Utility methods for dealing with URL encoding. + */ +public class URIUtil { + private static final String TAG = "URIUtil"; + + private URIUtil() {} + + public static URI getURIFromRequestUrl(String source) { + // try without encoding the URI + try { + return new URI(source); + } catch (URISyntaxException e) { + if (BuildConfig.DEBUG) Log.d(TAG, "Source is not encoded, encoding now"); + } + try { + URL url = new URL(source); + return new URI(url.getProtocol(), url.getUserInfo(), url.getHost(), url.getPort(), url.getPath(), url.getQuery(), url.getRef()); + } catch (MalformedURLException e) { + throw new IllegalArgumentException(e); + } catch (URISyntaxException e) { + throw new IllegalArgumentException(e); + } + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/URLChecker.java b/core/src/main/java/de/danoeh/antennapod/core/util/URLChecker.java new file mode 100644 index 000000000..ca49427c0 --- /dev/null +++ b/core/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.core.BuildConfig; + +/** + * Provides methods for checking and editing a URL. + */ +public final class URLChecker { + + /** + * Class shall not be instantiated. + */ + private URLChecker() { + } + + /** + * Logging tag. + */ + private static final String TAG = "URLChecker"; + + /** + * 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/core/src/main/java/de/danoeh/antennapod/core/util/UndoBarController.java b/core/src/main/java/de/danoeh/antennapod/core/util/UndoBarController.java new file mode 100644 index 000000000..5843c5f8f --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/UndoBarController.java @@ -0,0 +1,137 @@ +/* + * Copyright 2012 Roman Nurik + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package de.danoeh.antennapod.core.util; + +import android.os.Bundle; +import android.os.Handler; +import android.os.Parcelable; +import android.text.TextUtils; +import android.view.View; +import android.widget.TextView; +import com.nineoldandroids.animation.Animator; +import com.nineoldandroids.animation.AnimatorListenerAdapter; +import com.nineoldandroids.view.ViewHelper; +import com.nineoldandroids.view.ViewPropertyAnimator; +import de.danoeh.antennapod.core.R; + +import static com.nineoldandroids.view.ViewPropertyAnimator.animate; + +public class UndoBarController { + private View mBarView; + private TextView mMessageView; + private ViewPropertyAnimator mBarAnimator; + private Handler mHideHandler = new Handler(); + + private UndoListener mUndoListener; + + // State objects + private Parcelable mUndoToken; + private CharSequence mUndoMessage; + + public interface UndoListener { + void onUndo(Parcelable token); + } + + public UndoBarController(View undoBarView, UndoListener undoListener) { + mBarView = undoBarView; + mBarAnimator = animate(mBarView); + mUndoListener = undoListener; + + mMessageView = (TextView) mBarView.findViewById(R.id.undobar_message); + mBarView.findViewById(R.id.undobar_button) + .setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + hideUndoBar(false); + mUndoListener.onUndo(mUndoToken); + } + }); + + hideUndoBar(true); + } + + public void showUndoBar(boolean immediate, CharSequence message, Parcelable undoToken) { + mUndoToken = undoToken; + mUndoMessage = message; + mMessageView.setText(mUndoMessage); + + mHideHandler.removeCallbacks(mHideRunnable); + mHideHandler.postDelayed(mHideRunnable, + mBarView.getResources().getInteger(R.integer.undobar_hide_delay)); + + mBarView.setVisibility(View.VISIBLE); + if (immediate) { + ViewHelper.setAlpha(mBarView, 1); + } else { + mBarAnimator.cancel(); + mBarAnimator + .alpha(1) + .setDuration( + mBarView.getResources() + .getInteger(android.R.integer.config_shortAnimTime)) + .setListener(null); + } + } + + public void hideUndoBar(boolean immediate) { + mHideHandler.removeCallbacks(mHideRunnable); + if (immediate) { + mBarView.setVisibility(View.GONE); + ViewHelper.setAlpha(mBarView, 0); + mUndoMessage = null; + mUndoToken = null; + + } else { + mBarAnimator.cancel(); + mBarAnimator + .alpha(0) + .setDuration(mBarView.getResources() + .getInteger(android.R.integer.config_shortAnimTime)) + .setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mBarView.setVisibility(View.GONE); + mUndoMessage = null; + mUndoToken = null; + } + }); + } + } + + public void onSaveInstanceState(Bundle outState) { + outState.putCharSequence("undo_message", mUndoMessage); + outState.putParcelable("undo_token", mUndoToken); + } + + public void onRestoreInstanceState(Bundle savedInstanceState) { + if (savedInstanceState != null) { + mUndoMessage = savedInstanceState.getCharSequence("undo_message"); + mUndoToken = savedInstanceState.getParcelable("undo_token"); + + if (mUndoToken != null || !TextUtils.isEmpty(mUndoMessage)) { + showUndoBar(true, mUndoMessage, mUndoToken); + } + } + } + + private Runnable mHideRunnable = new Runnable() { + @Override + public void run() { + hideUndoBar(false); + } + }; +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/comparator/ChapterStartTimeComparator.java b/core/src/main/java/de/danoeh/antennapod/core/util/comparator/ChapterStartTimeComparator.java new file mode 100644 index 000000000..5274ffc9e --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/comparator/ChapterStartTimeComparator.java @@ -0,0 +1,20 @@ +package de.danoeh.antennapod.core.util.comparator; + +import de.danoeh.antennapod.core.feed.Chapter; + +import java.util.Comparator; + +public class ChapterStartTimeComparator implements Comparator { + + @Override + public int compare(Chapter lhs, Chapter rhs) { + if (lhs.getStart() == rhs.getStart()) { + return 0; + } else if (lhs.getStart() < rhs.getStart()) { + return -1; + } else { + return 1; + } + } + +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/comparator/DownloadStatusComparator.java b/core/src/main/java/de/danoeh/antennapod/core/util/comparator/DownloadStatusComparator.java new file mode 100644 index 000000000..ebdbfe2a5 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/comparator/DownloadStatusComparator.java @@ -0,0 +1,15 @@ +package de.danoeh.antennapod.core.util.comparator; + +import de.danoeh.antennapod.core.service.download.DownloadStatus; + +import java.util.Comparator; + +/** Compares the completion date of two Downloadstatus objects. */ +public class DownloadStatusComparator implements Comparator { + + @Override + public int compare(DownloadStatus lhs, DownloadStatus rhs) { + return rhs.getCompletionDate().compareTo(lhs.getCompletionDate()); + } + +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/comparator/FeedItemPubdateComparator.java b/core/src/main/java/de/danoeh/antennapod/core/util/comparator/FeedItemPubdateComparator.java new file mode 100644 index 000000000..a1f3ec699 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/comparator/FeedItemPubdateComparator.java @@ -0,0 +1,19 @@ +package de.danoeh.antennapod.core.util.comparator; + +import de.danoeh.antennapod.core.feed.FeedItem; + +import java.util.Comparator; + +/** Compares the pubDate of two FeedItems for sorting*/ +public class FeedItemPubdateComparator implements Comparator { + + /** Returns a new instance of this comparator in reverse order. + public static FeedItemPubdateComparator newInstance() { + FeedItemPubdateComparator + }*/ + @Override + public int compare(FeedItem lhs, FeedItem rhs) { + return rhs.getPubDate().compareTo(lhs.getPubDate()); + } + +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/comparator/PlaybackCompletionDateComparator.java b/core/src/main/java/de/danoeh/antennapod/core/util/comparator/PlaybackCompletionDateComparator.java new file mode 100644 index 000000000..84d244660 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/comparator/PlaybackCompletionDateComparator.java @@ -0,0 +1,19 @@ +package de.danoeh.antennapod.core.util.comparator; + +import de.danoeh.antennapod.core.feed.FeedItem; + +import java.util.Comparator; + +public class PlaybackCompletionDateComparator implements Comparator { + + public int compare(FeedItem lhs, FeedItem rhs) { + if (lhs.getMedia() != null + && lhs.getMedia().getPlaybackCompletionDate() != null + && rhs.getMedia() != null + && rhs.getMedia().getPlaybackCompletionDate() != null) { + return rhs.getMedia().getPlaybackCompletionDate() + .compareTo(lhs.getMedia().getPlaybackCompletionDate()); + } + return 0; + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/comparator/SearchResultValueComparator.java b/core/src/main/java/de/danoeh/antennapod/core/util/comparator/SearchResultValueComparator.java new file mode 100644 index 000000000..b16e0949d --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/comparator/SearchResultValueComparator.java @@ -0,0 +1,14 @@ +package de.danoeh.antennapod.core.util.comparator; + +import de.danoeh.antennapod.core.feed.SearchResult; + +import java.util.Comparator; + +public class SearchResultValueComparator implements Comparator { + + @Override + public int compare(SearchResult lhs, SearchResult rhs) { + return rhs.getValue() - lhs.getValue(); + } + +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/exception/MediaFileNotFoundException.java b/core/src/main/java/de/danoeh/antennapod/core/util/exception/MediaFileNotFoundException.java new file mode 100644 index 000000000..287fe1100 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/exception/MediaFileNotFoundException.java @@ -0,0 +1,23 @@ +package de.danoeh.antennapod.core.util.exception; + +import de.danoeh.antennapod.core.feed.FeedMedia; + +public class MediaFileNotFoundException extends Exception { + private static final long serialVersionUID = 1L; + + private FeedMedia media; + + public MediaFileNotFoundException(String msg, FeedMedia media) { + super(msg); + this.media = media; + } + + public MediaFileNotFoundException(FeedMedia media) { + super(); + this.media = media; + } + + public FeedMedia getMedia() { + return media; + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/flattr/FlattrServiceCreator.java b/core/src/main/java/de/danoeh/antennapod/core/util/flattr/FlattrServiceCreator.java new file mode 100644 index 000000000..e4818214e --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/flattr/FlattrServiceCreator.java @@ -0,0 +1,25 @@ +package de.danoeh.antennapod.core.util.flattr; + +import android.util.Log; +import de.danoeh.antennapod.core.BuildConfig; +import org.shredzone.flattr4j.FlattrFactory; +import org.shredzone.flattr4j.FlattrService; +import org.shredzone.flattr4j.oauth.AccessToken; + +/** Ensures that only one instance of the FlattrService class exists at a time */ + +public class FlattrServiceCreator { + public static final String TAG = "FlattrServiceCreator"; + + private static volatile FlattrService flattrService; + + public static FlattrService getService(AccessToken token) { + return FlattrFactory.getInstance().createFlattrService(token); + } + + public static void deleteFlattrService() { + if (BuildConfig.DEBUG) Log.d(TAG, "Deleting service instance"); + flattrService = null; + } +} + diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/flattr/FlattrStatus.java b/core/src/main/java/de/danoeh/antennapod/core/util/flattr/FlattrStatus.java new file mode 100644 index 000000000..d82171d1a --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/flattr/FlattrStatus.java @@ -0,0 +1,68 @@ +package de.danoeh.antennapod.core.util.flattr; + +import java.util.Calendar; + +public class FlattrStatus { + public static final int STATUS_UNFLATTERED = 0; + public static final int STATUS_QUEUE = 1; + public static final int STATUS_FLATTRED = 2; + + private int status = STATUS_UNFLATTERED; + private Calendar lastFlattred; + + public FlattrStatus() { + status = STATUS_UNFLATTERED; + lastFlattred = Calendar.getInstance(); + } + + public FlattrStatus(long status) { + lastFlattred = Calendar.getInstance(); + fromLong(status); + } + + public void setFlattred() { + status = STATUS_FLATTRED; + lastFlattred = Calendar.getInstance(); + } + + public void setUnflattred() { + status = STATUS_UNFLATTERED; + } + + public boolean getUnflattred() { + return status == STATUS_UNFLATTERED; + } + + public void setFlattrQueue() { + if (flattrable()) + status = STATUS_QUEUE; + } + + public void fromLong(long status) { + if (status == STATUS_UNFLATTERED || status == STATUS_QUEUE) + this.status = (int) status; + else { + this.status = STATUS_FLATTRED; + lastFlattred.setTimeInMillis(status); + } + } + + public long toLong() { + if (status == STATUS_UNFLATTERED || status == STATUS_QUEUE) + return status; + else { + return lastFlattred.getTimeInMillis(); + } + } + + public boolean flattrable() { + Calendar firstOfMonth = Calendar.getInstance(); + firstOfMonth.set(Calendar.DAY_OF_MONTH, Calendar.getInstance().getActualMinimum(Calendar.DAY_OF_MONTH)); + + return (status == STATUS_UNFLATTERED) || (status == STATUS_FLATTRED && firstOfMonth.after(lastFlattred) ); + } + + public boolean getFlattrQueue() { + return status == STATUS_QUEUE; + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/flattr/FlattrThing.java b/core/src/main/java/de/danoeh/antennapod/core/util/flattr/FlattrThing.java new file mode 100644 index 000000000..515028ab6 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/flattr/FlattrThing.java @@ -0,0 +1,7 @@ +package de.danoeh.antennapod.core.util.flattr; + +public interface FlattrThing { + public String getTitle(); + public String getPaymentLink(); + public FlattrStatus getFlattrStatus(); +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/flattr/FlattrUtils.java b/core/src/main/java/de/danoeh/antennapod/core/util/flattr/FlattrUtils.java new file mode 100644 index 000000000..42eeeadce --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/flattr/FlattrUtils.java @@ -0,0 +1,304 @@ +package de.danoeh.antennapod.core.util.flattr; + +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; +import android.content.Intent; +import android.content.SharedPreferences; +import android.net.Uri; +import android.preference.PreferenceManager; +import android.util.Log; + +import org.apache.commons.lang3.StringUtils; +import org.shredzone.flattr4j.FlattrService; +import org.shredzone.flattr4j.exception.FlattrException; +import org.shredzone.flattr4j.model.Flattr; +import org.shredzone.flattr4j.model.Thing; +import org.shredzone.flattr4j.oauth.AccessToken; +import org.shredzone.flattr4j.oauth.AndroidAuthenticator; +import org.shredzone.flattr4j.oauth.Scope; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.EnumSet; +import java.util.List; +import java.util.TimeZone; + +import de.danoeh.antennapod.core.BuildConfig; +import de.danoeh.antennapod.core.ClientConfig; +import de.danoeh.antennapod.core.R; +import de.danoeh.antennapod.core.asynctask.FlattrTokenFetcher; +import de.danoeh.antennapod.core.storage.DBWriter; + +/** + * Utility methods for doing something with flattr. + */ + +public class FlattrUtils { + private static final String TAG = "FlattrUtils"; + + private static final String HOST_NAME = "de.danoeh.antennapod"; + + private static final String PREF_ACCESS_TOKEN = "de.danoeh.antennapod.preference.flattrAccessToken"; + + // Flattr URL for this app. + public static final String APP_URL = "http://antennapod.com"; + // Human-readable flattr-page. + public static final String APP_LINK = "https://flattr.com/thing/745609/"; + public static final String APP_THING_ID = "745609"; + + private static volatile AccessToken cachedToken; + + private static AndroidAuthenticator createAuthenticator() { + return new AndroidAuthenticator(HOST_NAME, ClientConfig.flattrCallbacks.getFlattrAppKey(), + ClientConfig.flattrCallbacks.getFlattrAppSecret()); + } + + public static void startAuthProcess(Context context) throws FlattrException { + AndroidAuthenticator auth = createAuthenticator(); + auth.setScope(EnumSet.of(Scope.FLATTR)); + Intent intent = auth.createAuthenticateIntent(); + context.startActivity(intent); + } + + private static AccessToken retrieveToken() { + if (cachedToken == null) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Retrieving access token"); + String token = PreferenceManager.getDefaultSharedPreferences( + ClientConfig.applicationCallbacks.getApplicationInstance()) + .getString(PREF_ACCESS_TOKEN, null); + if (token != null) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Found access token. Caching."); + cachedToken = new AccessToken(token); + } else { + if (BuildConfig.DEBUG) + Log.d(TAG, "No access token found"); + return null; + } + } + return cachedToken; + + } + + /** + * Returns true if FLATTR_APP_KEY and FLATTR_APP_SECRET in BuildConfig are not null and not empty + */ + public static boolean hasAPICredentials() { + return StringUtils.isNotEmpty(ClientConfig.flattrCallbacks.getFlattrAppKey()) + && StringUtils.isNotEmpty(ClientConfig.flattrCallbacks.getFlattrAppSecret()); + } + + public static boolean hasToken() { + return retrieveToken() != null; + } + + public static void storeToken(AccessToken token) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Storing token"); + SharedPreferences.Editor editor = PreferenceManager + .getDefaultSharedPreferences(ClientConfig.applicationCallbacks.getApplicationInstance()).edit(); + if (token != null) { + editor.putString(PREF_ACCESS_TOKEN, token.getToken()); + } else { + editor.putString(PREF_ACCESS_TOKEN, null); + } + editor.commit(); + cachedToken = token; + } + + public static void deleteToken() { + if (BuildConfig.DEBUG) + Log.d(TAG, "Deleting flattr token"); + storeToken(null); + } + + public static Thing getAppThing(Context context) { + FlattrService fs = FlattrServiceCreator.getService(retrieveToken()); + try { + Thing thing = fs.getThing(Thing.withId(APP_THING_ID)); + return thing; + } catch (FlattrException e) { + e.printStackTrace(); + showErrorDialog(context, e.getMessage()); + return null; + } + } + + public static void clickUrl(Context context, String url) + throws FlattrException { + if (hasToken()) { + FlattrService fs = FlattrServiceCreator.getService(retrieveToken()); + fs.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( + ClientConfig.flattrCallbacks.getFlattrAuthenticationActivityIntent(context)); + } + + } + ); + + builder.setNegativeButton(R.string.visit_website_label, + new OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + Uri uri = Uri.parse(url); + context.startActivity(new Intent(Intent.ACTION_VIEW, + uri)); + } + + } + ); + builder.create().show(); + } else { + Uri uri = Uri.parse(url); + context.startActivity(new Intent(Intent.ACTION_VIEW, uri)); + } + } + + public static void showForbiddenDialog(final Context context, + final String url) { + AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setTitle(R.string.action_forbidden_title); + builder.setMessage(R.string.action_forbidden_msg); + builder.setPositiveButton(R.string.authenticate_now_label, + new OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + context.startActivity( + ClientConfig.flattrCallbacks.getFlattrAuthenticationActivityIntent(context)); + } + + } + ); + builder.setNegativeButton(R.string.visit_website_label, + new OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + Uri uri = Uri.parse(url); + context.startActivity(new Intent(Intent.ACTION_VIEW, + uri)); + } + + } + ); + builder.create().show(); + } + + public static void showErrorDialog(final Context context, final String msg) { + AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setTitle(R.string.error_label); + builder.setMessage(msg); + builder.setNeutralButton(android.R.string.ok, new OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.cancel(); + } + }); + builder.create().show(); + } + +} \ No newline at end of file diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/flattr/SimpleFlattrThing.java b/core/src/main/java/de/danoeh/antennapod/core/util/flattr/SimpleFlattrThing.java new file mode 100644 index 000000000..2c178496e --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/flattr/SimpleFlattrThing.java @@ -0,0 +1,30 @@ +package de.danoeh.antennapod.core.util.flattr; + +/* SimpleFlattrThing is a trivial implementation of the FlattrThing interface */ +public class SimpleFlattrThing implements FlattrThing { + public SimpleFlattrThing(String title, String url, FlattrStatus status) + { + this.title = title; + this.url = url; + this.status = status; + } + + public String getTitle() + { + return this.title; + } + + public String getPaymentLink() + { + return this.url; + } + + public FlattrStatus getFlattrStatus() + { + return this.status; + } + + private String title; + private String url; + private FlattrStatus status; +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/gui/FeedItemUndoToken.java b/core/src/main/java/de/danoeh/antennapod/core/util/gui/FeedItemUndoToken.java new file mode 100644 index 000000000..17581d3e9 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/gui/FeedItemUndoToken.java @@ -0,0 +1,55 @@ +package de.danoeh.antennapod.core.util.gui; + +import android.os.Parcel; +import android.os.Parcelable; +import de.danoeh.antennapod.core.feed.FeedItem; + +/** + * Used by an UndoBarController for saving a removed FeedItem + */ +public class FeedItemUndoToken implements Parcelable { + private long itemId; + private long feedId; + private int position; + + public FeedItemUndoToken(FeedItem item, int position) { + this.itemId = item.getId(); + this.feedId = item.getFeed().getId(); + this.position = position; + } + + private FeedItemUndoToken(Parcel in) { + itemId = in.readLong(); + feedId = in.readLong(); + position = in.readInt(); + } + + public static final Parcelable.Creator 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/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/ChapterReader.java b/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/ChapterReader.java new file mode 100644 index 000000000..9f3c4c6d5 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/ChapterReader.java @@ -0,0 +1,118 @@ +package de.danoeh.antennapod.core.util.id3reader; + +import android.util.Log; +import de.danoeh.antennapod.core.BuildConfig; +import de.danoeh.antennapod.core.feed.Chapter; +import de.danoeh.antennapod.core.feed.ID3Chapter; +import de.danoeh.antennapod.core.util.id3reader.model.FrameHeader; +import de.danoeh.antennapod.core.util.id3reader.model.TagHeader; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URLDecoder; +import java.util.ArrayList; +import java.util.List; + +public class ChapterReader extends ID3Reader { + private static final String TAG = "ID3ChapterReader"; + + private static final String FRAME_ID_CHAPTER = "CHAP"; + private static final String FRAME_ID_TITLE = "TIT2"; + private static final String FRAME_ID_LINK = "WXXX"; + + private List 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/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/ID3Reader.java b/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/ID3Reader.java new file mode 100644 index 000000000..a238c11e9 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/ID3Reader.java @@ -0,0 +1,250 @@ +package de.danoeh.antennapod.core.util.id3reader; + +import de.danoeh.antennapod.core.util.id3reader.model.FrameHeader; +import de.danoeh.antennapod.core.util.id3reader.model.TagHeader; +import org.apache.commons.io.IOUtils; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; + +/** + * Reads the ID3 Tag of a given file. In order to use this class, you should + * create a subclass of it and overwrite the onStart* - or onEnd* - methods. + */ +public class ID3Reader { + private static final int HEADER_LENGTH = 10; + private static final int ID3_LENGTH = 3; + private static final int FRAME_ID_LENGTH = 4; + + protected static final int ACTION_SKIP = 1; + protected static final int ACTION_DONT_SKIP = 2; + + protected int readerPosition; + + private static final byte ENCODING_UTF16_WITH_BOM = 1; + private static final byte ENCODING_UTF16_WITHOUT_BOM = 2; + private static final byte ENCODING_UTF8 = 3; + + private TagHeader tagHeader; + + public ID3Reader() { + } + + public final void readInputStream(InputStream input) throws IOException, + ID3ReaderException { + int rc; + readerPosition = 0; + char[] tagHeaderSource = readBytes(input, HEADER_LENGTH); + tagHeader = createTagHeader(tagHeaderSource); + if (tagHeader == null) { + onNoTagHeaderFound(); + } else { + rc = onStartTagHeader(tagHeader); + if (rc == ACTION_SKIP) { + onEndTag(); + } else { + while (readerPosition < tagHeader.getSize()) { + FrameHeader frameHeader = createFrameHeader(readBytes( + input, HEADER_LENGTH)); + if (checkForNullString(frameHeader.getId())) { + break; + } else { + rc = onStartFrameHeader(frameHeader, input); + if (rc == ACTION_SKIP) { + + if (frameHeader.getSize() + readerPosition > tagHeader + .getSize()) { + break; + } else { + skipBytes(input, frameHeader.getSize()); + } + } + } + } + onEndTag(); + } + } + } + + /** Returns true if string only contains null-bytes. */ + private boolean checkForNullString(String s) { + if (!s.isEmpty()) { + int i = 0; + if (s.charAt(i) == 0) { + for (i = 1; i < s.length(); i++) { + if (s.charAt(i) != 0) { + return false; + } + } + return true; + } + return false; + } else { + return true; + } + + } + + /** + * Read a certain number of bytes from the given input stream. This method + * changes the readerPosition-attribute. + */ + protected char[] readBytes(InputStream input, int number) + throws IOException, ID3ReaderException { + char[] header = new char[number]; + for (int i = 0; i < number; i++) { + int b = input.read(); + readerPosition++; + if (b != -1) { + header[i] = (char) b; + } else { + throw new ID3ReaderException("Unexpected end of stream"); + } + } + return header; + } + + /** + * Skip a certain number of bytes on the given input stream. This method + * changes the readerPosition-attribute. + */ + protected void skipBytes(InputStream input, int number) throws IOException { + if (number <= 0) { + number = 1; + } + IOUtils.skipFully(input, number); + + readerPosition += number; + } + + private TagHeader createTagHeader(char[] source) throws ID3ReaderException { + boolean hasTag = (source[0] == 0x49) && (source[1] == 0x44) + && (source[2] == 0x33); + if (source.length != HEADER_LENGTH) { + throw new ID3ReaderException("Length of header must be " + + HEADER_LENGTH); + } + if (hasTag) { + String id = new String(source, 0, ID3_LENGTH); + char version = (char) ((source[3] << 8) | source[4]); + byte flags = (byte) source[5]; + int size = (source[6] << 24) | (source[7] << 16) | (source[8] << 8) + | source[9]; + size = unsynchsafe(size); + return new TagHeader(id, size, version, flags); + } else { + return null; + } + } + + private FrameHeader createFrameHeader(char[] source) + throws ID3ReaderException { + if (source.length != HEADER_LENGTH) { + throw new ID3ReaderException("Length of header must be " + + HEADER_LENGTH); + } + String id = new String(source, 0, FRAME_ID_LENGTH); + + int size = (((int) source[4]) << 24) | (((int) source[5]) << 16) + | (((int) source[6]) << 8) | source[7]; + if (tagHeader != null && tagHeader.getVersion() >= 0x0400) { + size = unsynchsafe(size); + } + char flags = (char) ((source[8] << 8) | source[9]); + return new FrameHeader(id, size, flags); + } + + private int unsynchsafe(int in) { + int out = 0; + int mask = 0x7F000000; + + while (mask != 0) { + out >>= 1; + out |= in & mask; + mask >>= 8; + } + + return out; + } + + protected int readString(StringBuffer buffer, InputStream input, int max) throws IOException, + ID3ReaderException { + if (max > 0) { + char[] encoding = readBytes(input, 1); + max--; + + if (encoding[0] == ENCODING_UTF16_WITH_BOM || encoding[0] == ENCODING_UTF16_WITHOUT_BOM) { + return readUnicodeString(buffer, input, max, Charset.forName("UTF-16")) + 1; // take encoding byte into account + } else if (encoding[0] == ENCODING_UTF8) { + return readUnicodeString(buffer, input, max, Charset.forName("UTF-8")) + 1; // take encoding byte into account + } else { + return readISOString(buffer, input, max) + 1; // take encoding byte into account + } + } else { + if (buffer != null) { + buffer.append(""); + } + return 0; + } + } + + protected int readISOString(StringBuffer buffer, InputStream input, int max) + throws IOException, ID3ReaderException { + + int bytesRead = 0; + char c; + while (++bytesRead <= max && (c = (char) input.read()) > 0) { + if (buffer != null) { + buffer.append(c); + } + } + return bytesRead; + } + + private int readUnicodeString(StringBuffer strBuffer, InputStream input, int max, Charset charset) + throws IOException, ID3ReaderException { + byte[] buffer = new byte[max]; + int c, cZero = -1; + int i = 0; + for (; i < max; i++) { + c = input.read(); + if (c == -1) { + break; + } else if (c == 0) { + if (cZero == 0) { + // termination character found + break; + } else { + cZero = 0; + } + } else { + buffer[i] = (byte) c; + cZero = -1; + } + } + if (strBuffer != null) { + strBuffer.append(charset.newDecoder().decode(ByteBuffer.wrap(buffer)).toString()); + } + return i; + } + + public int onStartTagHeader(TagHeader header) { + return ACTION_SKIP; + } + + public int onStartFrameHeader(FrameHeader header, InputStream input) + throws IOException, ID3ReaderException { + return ACTION_SKIP; + } + + public void onEndTag() { + + } + + public void onNoTagHeaderFound() { + + } + +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/ID3ReaderException.java b/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/ID3ReaderException.java new file mode 100644 index 000000000..0c746d7e5 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/ID3ReaderException.java @@ -0,0 +1,20 @@ +package de.danoeh.antennapod.core.util.id3reader; + +public class ID3ReaderException extends Exception { + + public ID3ReaderException() { + } + + public ID3ReaderException(String arg0) { + super(arg0); + } + + public ID3ReaderException(Throwable arg0) { + super(arg0); + } + + public ID3ReaderException(String arg0, Throwable arg1) { + super(arg0, arg1); + } + +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/model/FrameHeader.java b/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/model/FrameHeader.java new file mode 100644 index 000000000..89eab1398 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/model/FrameHeader.java @@ -0,0 +1,17 @@ +package de.danoeh.antennapod.core.util.id3reader.model; + +public class FrameHeader extends Header { + + protected char flags; + + public FrameHeader(String id, int size, char flags) { + super(id, size); + this.flags = flags; + } + + @Override + public String toString() { + return String.format("FrameHeader [flags=%s, id=%s, size=%s]", Integer.toBinaryString(flags), id, Integer.toBinaryString(size)); + } + +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/model/Header.java b/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/model/Header.java new file mode 100644 index 000000000..346e2893f --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/model/Header.java @@ -0,0 +1,29 @@ +package de.danoeh.antennapod.core.util.id3reader.model; + +public abstract class Header { + + protected String id; + protected int size; + + public Header(String id, int size) { + super(); + this.id = id; + this.size = size; + } + + public String getId() { + return id; + } + + public int getSize() { + return size; + } + + @Override + public String toString() { + return "Header [id=" + id + ", size=" + size + "]"; + } + + + +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/model/TagHeader.java b/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/model/TagHeader.java new file mode 100644 index 000000000..0a6b8357f --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/model/TagHeader.java @@ -0,0 +1,26 @@ +package de.danoeh.antennapod.core.util.id3reader.model; + +public class TagHeader extends Header { + + protected char version; + protected byte flags; + + public TagHeader(String id, int size, char version, byte flags) { + super(id, size); + this.version = version; + this.flags = flags; + } + + @Override + public String toString() { + return "TagHeader [version=" + version + ", flags=" + flags + ", id=" + + id + ", size=" + size + "]"; + } + + public char getVersion() { + return version; + } + + + +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/AudioPlayer.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/AudioPlayer.java new file mode 100644 index 000000000..aafcea307 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/AudioPlayer.java @@ -0,0 +1,34 @@ +package de.danoeh.antennapod.core.util.playback; + +import android.content.Context; +import android.util.Log; +import android.view.SurfaceHolder; +import com.aocate.media.MediaPlayer; + +public class AudioPlayer extends MediaPlayer implements IPlayer { + private static final String TAG = "AudioPlayer"; + + public AudioPlayer(Context context) { + super(context); + } + + @Override + public void setScreenOnWhilePlaying(boolean screenOn) { + Log.e(TAG, "Setting screen on while playing not supported in Audio Player"); + throw new UnsupportedOperationException("Setting screen on while playing not supported in Audio Player"); + + } + + @Override + public void setDisplay(SurfaceHolder sh) { + if (sh != null) { + Log.e(TAG, "Setting display not supported in Audio Player"); + throw new UnsupportedOperationException("Setting display not supported in Audio Player"); + } + } + + @Override + public void setVideoScalingMode(int mode) { + throw new UnsupportedOperationException("Setting scaling mode is not supported in Audio Player"); + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/ExternalMedia.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/ExternalMedia.java new file mode 100644 index 000000000..49769f4f0 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/ExternalMedia.java @@ -0,0 +1,235 @@ +package de.danoeh.antennapod.core.util.playback; + +import android.content.SharedPreferences; +import android.content.SharedPreferences.Editor; +import android.media.MediaMetadataRetriever; +import android.net.Uri; +import android.os.Parcel; +import android.os.Parcelable; +import de.danoeh.antennapod.core.feed.Chapter; +import de.danoeh.antennapod.core.feed.MediaType; +import de.danoeh.antennapod.core.util.ChapterUtils; + +import java.util.List; +import java.util.concurrent.Callable; + +/** Represents a media file that is stored on the local storage device. */ +public class ExternalMedia implements Playable { + + public static final int PLAYABLE_TYPE_EXTERNAL_MEDIA = 2; + public static final String PREF_SOURCE_URL = "ExternalMedia.PrefSourceUrl"; + public static final String PREF_POSITION = "ExternalMedia.PrefPosition"; + public static final String PREF_MEDIA_TYPE = "ExternalMedia.PrefMediaType"; + + private String source; + + private String episodeTitle; + private String feedTitle; + private MediaType mediaType = MediaType.AUDIO; + private List 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/core/src/main/java/de/danoeh/antennapod/core/util/playback/IPlayer.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/IPlayer.java new file mode 100644 index 000000000..147c7848d --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/IPlayer.java @@ -0,0 +1,69 @@ +package de.danoeh.antennapod.core.util.playback; + +import android.content.Context; +import android.view.SurfaceHolder; + +import java.io.IOException; + +public interface IPlayer { + boolean canSetPitch(); + + boolean canSetSpeed(); + + float getCurrentPitchStepsAdjustment(); + + int getCurrentPosition(); + + float getCurrentSpeedMultiplier(); + + int getDuration(); + + float getMaxSpeedMultiplier(); + + float getMinSpeedMultiplier(); + + boolean isLooping(); + + boolean isPlaying(); + + void pause(); + + void prepare() throws IllegalStateException, IOException; + + void prepareAsync(); + + void release(); + + void reset(); + + void seekTo(int msec); + + void setAudioStreamType(int streamtype); + + void setScreenOnWhilePlaying(boolean screenOn); + + void setDataSource(String path) throws IllegalStateException, IOException, + IllegalArgumentException, SecurityException; + + void setDisplay(SurfaceHolder sh); + + void setEnableSpeedAdjustment(boolean enableSpeedAdjustment); + + void setLooping(boolean looping); + + void setPitchStepsAdjustment(float pitchSteps); + + void setPlaybackPitch(float f); + + void setPlaybackSpeed(float f); + + void setVolume(float left, float right); + + void start(); + + void stop(); + + public void setVideoScalingMode(int mode); + + public void setWakeMode(Context context, int mode); +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/MediaPlayerError.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/MediaPlayerError.java new file mode 100644 index 000000000..0650225f0 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/MediaPlayerError.java @@ -0,0 +1,23 @@ +package de.danoeh.antennapod.core.util.playback; + +import android.content.Context; +import android.media.MediaPlayer; +import de.danoeh.antennapod.core.R; + +/** Utility class for MediaPlayer errors. */ +public class MediaPlayerError { + + /** Get a human-readable string for a specific error code. */ + public static String getErrorString(Context context, int code) { + int resId; + switch(code) { + case MediaPlayer.MEDIA_ERROR_SERVER_DIED: + resId = R.string.playback_error_server_died; + break; + default: + resId = R.string.playback_error_unknown; + break; + } + return context.getString(resId); + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/Playable.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/Playable.java new file mode 100644 index 000000000..7ebd580f7 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/Playable.java @@ -0,0 +1,207 @@ +package de.danoeh.antennapod.core.util.playback; + +import android.content.Context; +import android.content.SharedPreferences; +import android.os.Parcelable; +import android.util.Log; + +import java.util.List; + +import de.danoeh.antennapod.core.asynctask.PicassoImageResource; +import de.danoeh.antennapod.core.feed.Chapter; +import de.danoeh.antennapod.core.feed.FeedMedia; +import de.danoeh.antennapod.core.feed.MediaType; +import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.core.util.ShownotesProvider; + +/** + * Interface for objects that can be played by the PlaybackService. + */ +public interface Playable extends Parcelable, + ShownotesProvider, PicassoImageResource { + + /** + * Save information about the playable in a preference so that it can be + * restored later via PlayableUtils.createInstanceFromPreferences. + * Implementations must NOT call commit() after they have written the values + * to the preferences file. + */ + public void writeToPreferences(SharedPreferences.Editor prefEditor); + + /** + * This method is called from a separate thread by the PlaybackService. + * Playable objects should load their metadata in this method. This method + * should execute as quickly as possible and NOT load chapter marks if no + * local file is available. + */ + public void loadMetadata() throws PlayableException; + + /** + * This method is called from a separate thread by the PlaybackService. + * Playable objects should load their chapter marks in this method if no + * local file was available when loadMetadata() was called. + */ + public void loadChapterMarks(); + + /** + * Returns the title of the episode that this playable represents + */ + public String getEpisodeTitle(); + + /** + * Returns a list of chapter marks or null if this Playable has no chapters. + */ + public List 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/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java new file mode 100644 index 000000000..5118d92ae --- /dev/null +++ b/core/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.core.BuildConfig; +import de.danoeh.antennapod.core.R; +import de.danoeh.antennapod.core.feed.Chapter; +import de.danoeh.antennapod.core.feed.FeedMedia; +import de.danoeh.antennapod.core.feed.MediaType; +import de.danoeh.antennapod.core.preferences.PlaybackPreferences; +import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.core.service.playback.PlaybackService; +import de.danoeh.antennapod.core.service.playback.PlaybackServiceMediaPlayer; +import de.danoeh.antennapod.core.service.playback.PlayerStatus; +import de.danoeh.antennapod.core.storage.DBTasks; +import de.danoeh.antennapod.core.util.Converter; +import de.danoeh.antennapod.core.util.playback.Playable.PlayableUtils; + +import java.util.concurrent.*; + +/** + * Communicates with the playback service. GUI classes should use this class to + * control playback instead of communicating with the PlaybackService directly. + */ +public abstract class PlaybackController { + private static final String TAG = "PlaybackController"; + + public static final int INVALID_TIME = -1; + + private final Activity activity; + + private PlaybackService playbackService; + private Playable media; + private PlayerStatus status; + + private ScheduledThreadPoolExecutor schedExecutor; + private static final int SCHED_EX_POOLSIZE = 1; + + protected MediaPositionObserver positionObserver; + protected ScheduledFuture positionObserverFuture; + + private boolean mediaInfoLoaded = false; + private boolean released = false; + + /** + * True if controller should reinit playback service if 'pause' button is + * pressed. + */ + private boolean reinitOnPause; + + public PlaybackController(Activity activity, boolean reinitOnPause) { + Validate.notNull(activity); + + this.activity = activity; + this.reinitOnPause = reinitOnPause; + schedExecutor = new ScheduledThreadPoolExecutor(SCHED_EX_POOLSIZE, + new ThreadFactory() { + + @Override + public Thread newThread(Runnable r) { + Thread t = new Thread(r); + t.setPriority(Thread.MIN_PRIORITY); + return t; + } + }, new RejectedExecutionHandler() { + + @Override + public void rejectedExecution(Runnable r, + ThreadPoolExecutor executor) { + Log.w(TAG, + "Rejected execution of runnable in schedExecutor"); + } + } + ); + } + + /** + * Creates a new connection to the playbackService. Should be called in the + * activity's onResume() method. + */ + public void init() { + activity.registerReceiver(statusUpdate, new IntentFilter( + PlaybackService.ACTION_PLAYER_STATUS_CHANGED)); + + activity.registerReceiver(notificationReceiver, new IntentFilter( + PlaybackService.ACTION_PLAYER_NOTIFICATION)); + + activity.registerReceiver(shutdownReceiver, new IntentFilter( + PlaybackService.ACTION_SHUTDOWN_PLAYBACK_SERVICE)); + + if (!released) { + bindToService(); + } else { + throw new IllegalStateException( + "Can't call init() after release() has been called"); + } + } + + /** + * Should be called if the PlaybackController is no longer needed, for + * example in the activity's onStop() method. + */ + public void release() { + if (BuildConfig.DEBUG) + Log.d(TAG, "Releasing PlaybackController"); + + try { + activity.unregisterReceiver(statusUpdate); + } catch (IllegalArgumentException e) { + // ignore + } + + try { + activity.unregisterReceiver(notificationReceiver); + } catch (IllegalArgumentException e) { + // ignore + } + + try { + activity.unbindService(mConnection); + } catch (IllegalArgumentException e) { + // ignore + } + + try { + activity.unregisterReceiver(shutdownReceiver); + } catch (IllegalArgumentException e) { + // ignore + } + cancelPositionObserver(); + schedExecutor.shutdownNow(); + media = null; + released = true; + + } + + /** + * Should be called in the activity's onPause() method. + */ + public void pause() { + mediaInfoLoaded = false; + } + + /** + * Tries to establish a connection to the PlaybackService. If it isn't + * running, the PlaybackService will be started with the last played media + * as the arguments of the launch intent. + */ + private void bindToService() { + if (BuildConfig.DEBUG) + Log.d(TAG, "Trying to connect to service"); + AsyncTask 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/core/src/main/java/de/danoeh/antennapod/core/util/playback/Timeline.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/Timeline.java new file mode 100644 index 000000000..443ff0ad1 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/Timeline.java @@ -0,0 +1,161 @@ +package de.danoeh.antennapod.core.util.playback; + +import android.content.Context; +import android.content.res.TypedArray; +import android.util.Log; +import android.util.TypedValue; + +import org.apache.commons.lang3.Validate; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.jsoup.select.Elements; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import de.danoeh.antennapod.core.BuildConfig; +import de.danoeh.antennapod.core.util.Converter; +import de.danoeh.antennapod.core.util.ShownotesProvider; + +/** + * Connects chapter information and shownotes of a shownotesProvider, for example by making it possible to use the + * shownotes to navigate to another position in the podcast or by highlighting certain parts of the shownotesProvider's + * shownotes. + *

+ * 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/core/src/main/java/de/danoeh/antennapod/core/util/playback/VideoPlayer.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/VideoPlayer.java new file mode 100644 index 000000000..dc5270d8f --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/VideoPlayer.java @@ -0,0 +1,67 @@ +package de.danoeh.antennapod.core.util.playback; + +import android.media.MediaPlayer; +import android.util.Log; + +public class VideoPlayer extends MediaPlayer implements IPlayer { + private static final String TAG = "VideoPlayer"; + + @Override + public boolean canSetPitch() { + return false; + } + + @Override + public boolean canSetSpeed() { + return false; + } + + @Override + public float getCurrentPitchStepsAdjustment() { + return 1; + } + + @Override + public float getCurrentSpeedMultiplier() { + return 1; + } + + @Override + public float getMaxSpeedMultiplier() { + return 1; + } + + @Override + public float getMinSpeedMultiplier() { + return 1; + } + + @Override + public void setEnableSpeedAdjustment(boolean enableSpeedAdjustment) throws UnsupportedOperationException { + Log.e(TAG, "Setting enable speed adjustment unsupported in video player"); + throw new UnsupportedOperationException("Setting enable speed adjustment unsupported in video player"); + } + + @Override + public void setPitchStepsAdjustment(float pitchSteps) { + Log.e(TAG, "Setting pitch steps adjustment unsupported in video player"); + throw new UnsupportedOperationException("Setting pitch steps adjustment unsupported in video player"); + } + + @Override + public void setPlaybackPitch(float f) { + Log.e(TAG, "Setting playback pitch unsupported in video player"); + throw new UnsupportedOperationException("Setting playback pitch unsupported in video player"); + } + + @Override + public void setPlaybackSpeed(float f) { + Log.e(TAG, "Setting playback speed unsupported in video player"); + throw new UnsupportedOperationException("Setting playback speed unsupported in video player"); + } + + @Override + public void setVideoScalingMode(int mode) { + super.setVideoScalingMode(mode); + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/syndication/FeedDiscoverer.java b/core/src/main/java/de/danoeh/antennapod/core/util/syndication/FeedDiscoverer.java new file mode 100644 index 000000000..9588265b8 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/syndication/FeedDiscoverer.java @@ -0,0 +1,78 @@ +package de.danoeh.antennapod.core.util.syndication; + +import android.net.Uri; +import org.apache.commons.lang3.StringUtils; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.jsoup.select.Elements; + +import java.io.File; +import java.io.IOException; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Finds RSS/Atom URLs in a HTML document using the auto-discovery techniques described here: + *

+ * 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/core/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/OggInputStream.java b/core/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/OggInputStream.java new file mode 100644 index 000000000..4799d3881 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/OggInputStream.java @@ -0,0 +1,81 @@ +package de.danoeh.antennapod.core.util.vorbiscommentreader; + +import org.apache.commons.io.IOUtils; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; + +public class OggInputStream extends InputStream { + private InputStream input; + + /** True if OggInputStream is currently inside an Ogg page. */ + private boolean isInPage; + private long bytesLeft; + + public OggInputStream(InputStream input) { + super(); + isInPage = false; + this.input = input; + } + + @Override + public int read() throws IOException { + if (!isInPage) { + readOggPage(); + } + + if (isInPage && bytesLeft > 0) { + int result = input.read(); + bytesLeft -= 1; + if (bytesLeft == 0) { + isInPage = false; + } + return result; + } + return -1; + } + + private void readOggPage() throws IOException { + // find OggS + int[] buffer = new int[4]; + int c = 0; + boolean isInOggS = false; + while ((c = input.read()) != -1) { + switch (c) { + case 'O': + isInOggS = true; + buffer[0] = c; + break; + case 'g': + if (buffer[1] != c) { + buffer[1] = c; + } else { + buffer[2] = c; + } + break; + case 'S': + buffer[3] = c; + break; + default: + if (isInOggS) { + Arrays.fill(buffer, 0); + isInOggS = false; + } + } + if (buffer[0] == 'O' && buffer[1] == 'g' && buffer[2] == 'g' + && buffer[3] == 'S') { + break; + } + } + // read segments + IOUtils.skipFully(input, 22); + bytesLeft = 0; + int numSegments = input.read(); + for (int i = 0; i < numSegments; i++) { + bytesLeft += input.read(); + } + isInPage = true; + } + +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/VorbisCommentChapterReader.java b/core/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/VorbisCommentChapterReader.java new file mode 100644 index 000000000..c4961a3ab --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/VorbisCommentChapterReader.java @@ -0,0 +1,101 @@ +package de.danoeh.antennapod.core.util.vorbiscommentreader; + +import android.util.Log; +import de.danoeh.antennapod.core.BuildConfig; +import de.danoeh.antennapod.core.feed.Chapter; +import de.danoeh.antennapod.core.feed.VorbisCommentChapter; + +import java.util.ArrayList; +import java.util.List; + +public class VorbisCommentChapterReader extends VorbisCommentReader { + private static final String TAG = "VorbisCommentChapterReader"; + + private static final String CHAPTER_KEY = "chapter\\d\\d\\d.*"; + private static final String CHAPTER_ATTRIBUTE_TITLE = "name"; + private static final String CHAPTER_ATTRIBUTE_LINK = "url"; + + private List 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/core/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/VorbisCommentHeader.java b/core/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/VorbisCommentHeader.java new file mode 100644 index 000000000..5f9dd0faf --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/VorbisCommentHeader.java @@ -0,0 +1,26 @@ +package de.danoeh.antennapod.core.util.vorbiscommentreader; +public class VorbisCommentHeader { + private String vendorString; + private long userCommentLength; + + public VorbisCommentHeader(String vendorString, long userCommentLength) { + super(); + this.vendorString = vendorString; + this.userCommentLength = userCommentLength; + } + + @Override + public String toString() { + return "VorbisCommentHeader [vendorString=" + vendorString + + ", userCommentLength=" + userCommentLength + "]"; + } + + public String getVendorString() { + return vendorString; + } + + public long getUserCommentLength() { + return userCommentLength; + } + +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/VorbisCommentReader.java b/core/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/VorbisCommentReader.java new file mode 100644 index 000000000..9639b9c42 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/VorbisCommentReader.java @@ -0,0 +1,194 @@ +package de.danoeh.antennapod.core.util.vorbiscommentreader; + +import org.apache.commons.io.EndianUtils; +import org.apache.commons.io.IOUtils; + +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.util.Arrays; + + +public abstract class VorbisCommentReader { + /** Length of first page in an ogg file in bytes. */ + private static final int FIRST_PAGE_LENGTH = 58; + private static final int SECOND_PAGE_MAX_LENGTH = 64 * 1024 * 1024; + private static final int PACKET_TYPE_IDENTIFICATION = 1; + private static final int PACKET_TYPE_COMMENT = 3; + + /** Called when Reader finds identification header. */ + public abstract void onVorbisCommentFound(); + + public abstract void onVorbisCommentHeaderFound(VorbisCommentHeader header); + + /** + * Is called every time the Reader finds a content vector. The handler + * should return true if it wants to handle the content vector. + */ + public abstract boolean onContentVectorKey(String content); + + /** + * Is called if onContentVectorKey returned true for the key. + * + * @throws VorbisCommentReaderException + */ + public abstract void onContentVectorValue(String key, String value) + throws VorbisCommentReaderException; + + public abstract void onNoVorbisCommentFound(); + + public abstract void onEndOfComment(); + + public abstract void onError(VorbisCommentReaderException exception); + + public void readInputStream(InputStream input) + throws VorbisCommentReaderException { + try { + // look for identification header + if (findIdentificationHeader(input)) { + + onVorbisCommentFound(); + input = new OggInputStream(input); + if (findCommentHeader(input)) { + VorbisCommentHeader commentHeader = readCommentHeader(input); + if (commentHeader != null) { + onVorbisCommentHeaderFound(commentHeader); + for (int i = 0; i < commentHeader + .getUserCommentLength(); i++) { + try { + long vectorLength = EndianUtils + .readSwappedUnsignedInteger(input); + String key = readContentVectorKey(input, + vectorLength).toLowerCase(); + boolean readValue = onContentVectorKey(key); + if (readValue) { + String value = readUTF8String( + input, + (int) (vectorLength - key.length() - 1)); + onContentVectorValue(key, value); + } else { + IOUtils.skipFully(input, + vectorLength - key.length() - 1); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + onEndOfComment(); + } + + } else { + onError(new VorbisCommentReaderException( + "No comment header found")); + } + } else { + onNoVorbisCommentFound(); + } + } catch (IOException e) { + onError(new VorbisCommentReaderException(e)); + } + } + + private String readUTF8String(InputStream input, long length) + throws IOException { + byte[] buffer = new byte[(int) length]; + + IOUtils.readFully(input, buffer); + Charset charset = Charset.forName("UTF-8"); + return charset.newDecoder().decode(ByteBuffer.wrap(buffer)).toString(); + } + + /** + * Looks for an identification header in the first page of the file. If an + * identification header is found, it will be skipped completely and the + * method will return true, otherwise false. + * + * @throws IOException + */ + private boolean findIdentificationHeader(InputStream input) + throws IOException { + byte[] buffer = new byte[FIRST_PAGE_LENGTH]; + IOUtils.readFully(input, buffer); + int i; + for (i = 6; i < buffer.length; i++) { + if (buffer[i - 5] == 'v' && buffer[i - 4] == 'o' + && buffer[i - 3] == 'r' && buffer[i - 2] == 'b' + && buffer[i - 1] == 'i' && buffer[i] == 's' + && buffer[i - 6] == PACKET_TYPE_IDENTIFICATION) { + return true; + } + } + return false; + } + + private boolean findCommentHeader(InputStream input) throws IOException { + char[] buffer = new char["vorbis".length() + 1]; + for (int bytesRead = 0; bytesRead < SECOND_PAGE_MAX_LENGTH; bytesRead++) { + char c = (char) input.read(); + int dest = -1; + switch (c) { + case PACKET_TYPE_COMMENT: + dest = 0; + break; + case 'v': + dest = 1; + break; + case 'o': + dest = 2; + break; + case 'r': + dest = 3; + break; + case 'b': + dest = 4; + break; + case 'i': + dest = 5; + break; + case 's': + dest = 6; + break; + } + if (dest >= 0) { + buffer[dest] = c; + if (buffer[1] == 'v' && buffer[2] == 'o' && buffer[3] == 'r' + && buffer[4] == 'b' && buffer[5] == 'i' + && buffer[6] == 's' && buffer[0] == PACKET_TYPE_COMMENT) { + return true; + } + } else { + Arrays.fill(buffer, (char) 0); + } + } + return false; + } + + private VorbisCommentHeader readCommentHeader(InputStream input) + throws IOException, VorbisCommentReaderException { + try { + long vendorLength = EndianUtils.readSwappedUnsignedInteger(input); + String vendorName = readUTF8String(input, vendorLength); + long userCommentLength = EndianUtils + .readSwappedUnsignedInteger(input); + return new VorbisCommentHeader(vendorName, userCommentLength); + } catch (UnsupportedEncodingException e) { + throw new VorbisCommentReaderException(e); + } + } + + private String readContentVectorKey(InputStream input, long vectorLength) + throws IOException { + StringBuffer buffer = new StringBuffer(); + for (int i = 0; i < vectorLength; i++) { + char c = (char) input.read(); + if (c == '=') { + return buffer.toString(); + } else { + buffer.append(c); + } + } + return null; // no key found + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/VorbisCommentReaderException.java b/core/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/VorbisCommentReaderException.java new file mode 100644 index 000000000..89ab20db0 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/VorbisCommentReaderException.java @@ -0,0 +1,24 @@ +package de.danoeh.antennapod.core.util.vorbiscommentreader; +public class VorbisCommentReaderException extends Exception { + + public VorbisCommentReaderException() { + super(); + // TODO Auto-generated constructor stub + } + + public VorbisCommentReaderException(String arg0, Throwable arg1) { + super(arg0, arg1); + // TODO Auto-generated constructor stub + } + + public VorbisCommentReaderException(String arg0) { + super(arg0); + // TODO Auto-generated constructor stub + } + + public VorbisCommentReaderException(Throwable arg0) { + super(arg0); + // TODO Auto-generated constructor stub + } + +} diff --git a/core/src/main/res/drawable-hdpi-v11/ic_stat_antenna.png b/core/src/main/res/drawable-hdpi-v11/ic_stat_antenna.png new file mode 100644 index 000000000..37d73c734 Binary files /dev/null and b/core/src/main/res/drawable-hdpi-v11/ic_stat_antenna.png differ diff --git a/core/src/main/res/drawable-hdpi-v11/ic_stat_authentication.png b/core/src/main/res/drawable-hdpi-v11/ic_stat_authentication.png new file mode 100755 index 000000000..ad148cc6b Binary files /dev/null and b/core/src/main/res/drawable-hdpi-v11/ic_stat_authentication.png differ diff --git a/core/src/main/res/drawable-hdpi-v11/stat_notify_sync.png b/core/src/main/res/drawable-hdpi-v11/stat_notify_sync.png new file mode 100644 index 000000000..90b39c958 Binary files /dev/null and b/core/src/main/res/drawable-hdpi-v11/stat_notify_sync.png differ diff --git a/core/src/main/res/drawable-hdpi-v11/stat_notify_sync_error.png b/core/src/main/res/drawable-hdpi-v11/stat_notify_sync_error.png new file mode 100644 index 000000000..074cdee27 Binary files /dev/null and b/core/src/main/res/drawable-hdpi-v11/stat_notify_sync_error.png differ diff --git a/core/src/main/res/drawable-hdpi/action_about.png b/core/src/main/res/drawable-hdpi/action_about.png new file mode 100644 index 000000000..8f39c428a Binary files /dev/null and b/core/src/main/res/drawable-hdpi/action_about.png differ diff --git a/core/src/main/res/drawable-hdpi/action_about_dark.png b/core/src/main/res/drawable-hdpi/action_about_dark.png new file mode 100755 index 000000000..6eaf08aec Binary files /dev/null and b/core/src/main/res/drawable-hdpi/action_about_dark.png differ diff --git a/core/src/main/res/drawable-hdpi/action_search.png b/core/src/main/res/drawable-hdpi/action_search.png new file mode 100644 index 000000000..e6b704518 Binary files /dev/null and b/core/src/main/res/drawable-hdpi/action_search.png differ diff --git a/core/src/main/res/drawable-hdpi/action_search_dark.png b/core/src/main/res/drawable-hdpi/action_search_dark.png new file mode 100755 index 000000000..f12e005eb Binary files /dev/null and b/core/src/main/res/drawable-hdpi/action_search_dark.png differ diff --git a/core/src/main/res/drawable-hdpi/action_settings.png b/core/src/main/res/drawable-hdpi/action_settings.png new file mode 100644 index 000000000..cc32e2d1d Binary files /dev/null and b/core/src/main/res/drawable-hdpi/action_settings.png differ diff --git a/core/src/main/res/drawable-hdpi/action_settings_dark.png b/core/src/main/res/drawable-hdpi/action_settings_dark.png new file mode 100755 index 000000000..3e4580e05 Binary files /dev/null and b/core/src/main/res/drawable-hdpi/action_settings_dark.png differ diff --git a/core/src/main/res/drawable-hdpi/action_stream.png b/core/src/main/res/drawable-hdpi/action_stream.png new file mode 100644 index 000000000..8fc7a7b1e Binary files /dev/null and b/core/src/main/res/drawable-hdpi/action_stream.png differ diff --git a/core/src/main/res/drawable-hdpi/action_stream_dark.png b/core/src/main/res/drawable-hdpi/action_stream_dark.png new file mode 100644 index 000000000..97b752cea Binary files /dev/null and b/core/src/main/res/drawable-hdpi/action_stream_dark.png differ diff --git a/core/src/main/res/drawable-hdpi/av_download.png b/core/src/main/res/drawable-hdpi/av_download.png new file mode 100644 index 000000000..5bceafb1e Binary files /dev/null and b/core/src/main/res/drawable-hdpi/av_download.png differ diff --git a/core/src/main/res/drawable-hdpi/av_download_dark.png b/core/src/main/res/drawable-hdpi/av_download_dark.png new file mode 100755 index 000000000..d5bfa457c Binary files /dev/null and b/core/src/main/res/drawable-hdpi/av_download_dark.png differ diff --git a/core/src/main/res/drawable-hdpi/av_fast_forward.png b/core/src/main/res/drawable-hdpi/av_fast_forward.png new file mode 100644 index 000000000..58ee5c26c Binary files /dev/null and b/core/src/main/res/drawable-hdpi/av_fast_forward.png differ diff --git a/core/src/main/res/drawable-hdpi/av_fast_forward_dark.png b/core/src/main/res/drawable-hdpi/av_fast_forward_dark.png new file mode 100755 index 000000000..237c4f846 Binary files /dev/null and b/core/src/main/res/drawable-hdpi/av_fast_forward_dark.png differ diff --git a/core/src/main/res/drawable-hdpi/av_pause.png b/core/src/main/res/drawable-hdpi/av_pause.png new file mode 100644 index 000000000..9661cfbb0 Binary files /dev/null and b/core/src/main/res/drawable-hdpi/av_pause.png differ diff --git a/core/src/main/res/drawable-hdpi/av_pause_dark.png b/core/src/main/res/drawable-hdpi/av_pause_dark.png new file mode 100755 index 000000000..6b435bb0f Binary files /dev/null and b/core/src/main/res/drawable-hdpi/av_pause_dark.png differ diff --git a/core/src/main/res/drawable-hdpi/av_play.png b/core/src/main/res/drawable-hdpi/av_play.png new file mode 100644 index 000000000..e70f0413e Binary files /dev/null and b/core/src/main/res/drawable-hdpi/av_play.png differ diff --git a/core/src/main/res/drawable-hdpi/av_play_dark.png b/core/src/main/res/drawable-hdpi/av_play_dark.png new file mode 100755 index 000000000..df8a2ca28 Binary files /dev/null and b/core/src/main/res/drawable-hdpi/av_play_dark.png differ diff --git a/core/src/main/res/drawable-hdpi/av_rewind.png b/core/src/main/res/drawable-hdpi/av_rewind.png new file mode 100644 index 000000000..e2f843ce2 Binary files /dev/null and b/core/src/main/res/drawable-hdpi/av_rewind.png differ diff --git a/core/src/main/res/drawable-hdpi/av_rewind_dark.png b/core/src/main/res/drawable-hdpi/av_rewind_dark.png new file mode 100755 index 000000000..caf517498 Binary files /dev/null and b/core/src/main/res/drawable-hdpi/av_rewind_dark.png differ diff --git a/core/src/main/res/drawable-hdpi/content_discard.png b/core/src/main/res/drawable-hdpi/content_discard.png new file mode 100644 index 000000000..e9ce89e04 Binary files /dev/null and b/core/src/main/res/drawable-hdpi/content_discard.png differ diff --git a/core/src/main/res/drawable-hdpi/content_discard_dark.png b/core/src/main/res/drawable-hdpi/content_discard_dark.png new file mode 100755 index 000000000..ffd19d9e8 Binary files /dev/null and b/core/src/main/res/drawable-hdpi/content_discard_dark.png differ diff --git a/core/src/main/res/drawable-hdpi/content_new.png b/core/src/main/res/drawable-hdpi/content_new.png new file mode 100644 index 000000000..5741995cb Binary files /dev/null and b/core/src/main/res/drawable-hdpi/content_new.png differ diff --git a/core/src/main/res/drawable-hdpi/content_new_dark.png b/core/src/main/res/drawable-hdpi/content_new_dark.png new file mode 100755 index 000000000..ad8ada6bd Binary files /dev/null and b/core/src/main/res/drawable-hdpi/content_new_dark.png differ diff --git a/core/src/main/res/drawable-hdpi/default_cover.png b/core/src/main/res/drawable-hdpi/default_cover.png new file mode 100644 index 000000000..a6e67e2ca Binary files /dev/null and b/core/src/main/res/drawable-hdpi/default_cover.png differ diff --git a/core/src/main/res/drawable-hdpi/default_cover_dark.png b/core/src/main/res/drawable-hdpi/default_cover_dark.png new file mode 100755 index 000000000..0f650ee25 Binary files /dev/null and b/core/src/main/res/drawable-hdpi/default_cover_dark.png differ diff --git a/core/src/main/res/drawable-hdpi/device_access_time.png b/core/src/main/res/drawable-hdpi/device_access_time.png new file mode 100644 index 000000000..001549f38 Binary files /dev/null and b/core/src/main/res/drawable-hdpi/device_access_time.png differ diff --git a/core/src/main/res/drawable-hdpi/device_access_time_dark.png b/core/src/main/res/drawable-hdpi/device_access_time_dark.png new file mode 100755 index 000000000..314ec9319 Binary files /dev/null and b/core/src/main/res/drawable-hdpi/device_access_time_dark.png differ diff --git a/core/src/main/res/drawable-hdpi/ic_action_overflow.png b/core/src/main/res/drawable-hdpi/ic_action_overflow.png new file mode 100644 index 000000000..002fc4bfb Binary files /dev/null and b/core/src/main/res/drawable-hdpi/ic_action_overflow.png differ diff --git a/core/src/main/res/drawable-hdpi/ic_action_overflow_dark.png b/core/src/main/res/drawable-hdpi/ic_action_overflow_dark.png new file mode 100644 index 000000000..c8792cbe2 Binary files /dev/null and b/core/src/main/res/drawable-hdpi/ic_action_overflow_dark.png differ diff --git a/core/src/main/res/drawable-hdpi/ic_action_pause_over_video.png b/core/src/main/res/drawable-hdpi/ic_action_pause_over_video.png new file mode 100755 index 000000000..64b07728f Binary files /dev/null and b/core/src/main/res/drawable-hdpi/ic_action_pause_over_video.png differ diff --git a/core/src/main/res/drawable-hdpi/ic_action_play_over_video.png b/core/src/main/res/drawable-hdpi/ic_action_play_over_video.png new file mode 100755 index 000000000..a364ca7c2 Binary files /dev/null and b/core/src/main/res/drawable-hdpi/ic_action_play_over_video.png differ diff --git a/core/src/main/res/drawable-hdpi/ic_drag_handle.png b/core/src/main/res/drawable-hdpi/ic_drag_handle.png new file mode 100755 index 000000000..38ec201de Binary files /dev/null and b/core/src/main/res/drawable-hdpi/ic_drag_handle.png differ diff --git a/core/src/main/res/drawable-hdpi/ic_drag_handle_dark.png b/core/src/main/res/drawable-hdpi/ic_drag_handle_dark.png new file mode 100755 index 000000000..e96d23252 Binary files /dev/null and b/core/src/main/res/drawable-hdpi/ic_drag_handle_dark.png differ diff --git a/core/src/main/res/drawable-hdpi/ic_drawer.png b/core/src/main/res/drawable-hdpi/ic_drawer.png new file mode 100644 index 000000000..c59f601ca Binary files /dev/null and b/core/src/main/res/drawable-hdpi/ic_drawer.png differ diff --git a/core/src/main/res/drawable-hdpi/ic_drawer_dark.png b/core/src/main/res/drawable-hdpi/ic_drawer_dark.png new file mode 100644 index 000000000..6614ea4f4 Binary files /dev/null and b/core/src/main/res/drawable-hdpi/ic_drawer_dark.png differ diff --git a/core/src/main/res/drawable-hdpi/ic_launcher.png b/core/src/main/res/drawable-hdpi/ic_launcher.png new file mode 100644 index 000000000..994b763cc Binary files /dev/null and b/core/src/main/res/drawable-hdpi/ic_launcher.png differ diff --git a/core/src/main/res/drawable-hdpi/ic_new.png b/core/src/main/res/drawable-hdpi/ic_new.png new file mode 100755 index 000000000..8ff519052 Binary files /dev/null and b/core/src/main/res/drawable-hdpi/ic_new.png differ diff --git a/core/src/main/res/drawable-hdpi/ic_new_dark.png b/core/src/main/res/drawable-hdpi/ic_new_dark.png new file mode 100755 index 000000000..c8581e01c Binary files /dev/null and b/core/src/main/res/drawable-hdpi/ic_new_dark.png differ diff --git a/core/src/main/res/drawable-hdpi/ic_stat_antenna.png b/core/src/main/res/drawable-hdpi/ic_stat_antenna.png new file mode 100644 index 000000000..36d502492 Binary files /dev/null and b/core/src/main/res/drawable-hdpi/ic_stat_antenna.png differ diff --git a/core/src/main/res/drawable-hdpi/ic_stat_authentication.png b/core/src/main/res/drawable-hdpi/ic_stat_authentication.png new file mode 100755 index 000000000..c6b5efd33 Binary files /dev/null and b/core/src/main/res/drawable-hdpi/ic_stat_authentication.png differ diff --git a/core/src/main/res/drawable-hdpi/location_web_site.png b/core/src/main/res/drawable-hdpi/location_web_site.png new file mode 100644 index 000000000..6a2bc8857 Binary files /dev/null and b/core/src/main/res/drawable-hdpi/location_web_site.png differ diff --git a/core/src/main/res/drawable-hdpi/location_web_site_dark.png b/core/src/main/res/drawable-hdpi/location_web_site_dark.png new file mode 100755 index 000000000..e154afdbc Binary files /dev/null and b/core/src/main/res/drawable-hdpi/location_web_site_dark.png differ diff --git a/core/src/main/res/drawable-hdpi/navigation_accept.png b/core/src/main/res/drawable-hdpi/navigation_accept.png new file mode 100644 index 000000000..58bf97217 Binary files /dev/null and b/core/src/main/res/drawable-hdpi/navigation_accept.png differ diff --git a/core/src/main/res/drawable-hdpi/navigation_accept_dark.png b/core/src/main/res/drawable-hdpi/navigation_accept_dark.png new file mode 100755 index 000000000..53cf6877e Binary files /dev/null and b/core/src/main/res/drawable-hdpi/navigation_accept_dark.png differ diff --git a/core/src/main/res/drawable-hdpi/navigation_cancel.png b/core/src/main/res/drawable-hdpi/navigation_cancel.png new file mode 100644 index 000000000..cde36e1fa Binary files /dev/null and b/core/src/main/res/drawable-hdpi/navigation_cancel.png differ diff --git a/core/src/main/res/drawable-hdpi/navigation_cancel_dark.png b/core/src/main/res/drawable-hdpi/navigation_cancel_dark.png new file mode 100755 index 000000000..094eea589 Binary files /dev/null and b/core/src/main/res/drawable-hdpi/navigation_cancel_dark.png differ diff --git a/core/src/main/res/drawable-hdpi/navigation_chapters.png b/core/src/main/res/drawable-hdpi/navigation_chapters.png new file mode 100755 index 000000000..b034459bc Binary files /dev/null and b/core/src/main/res/drawable-hdpi/navigation_chapters.png differ diff --git a/core/src/main/res/drawable-hdpi/navigation_chapters_dark.png b/core/src/main/res/drawable-hdpi/navigation_chapters_dark.png new file mode 100755 index 000000000..7b0d4889c Binary files /dev/null and b/core/src/main/res/drawable-hdpi/navigation_chapters_dark.png differ diff --git a/core/src/main/res/drawable-hdpi/navigation_collapse.png b/core/src/main/res/drawable-hdpi/navigation_collapse.png new file mode 100755 index 000000000..bd405bada Binary files /dev/null and b/core/src/main/res/drawable-hdpi/navigation_collapse.png differ diff --git a/core/src/main/res/drawable-hdpi/navigation_collapse_dark.png b/core/src/main/res/drawable-hdpi/navigation_collapse_dark.png new file mode 100755 index 000000000..ca78f2ec0 Binary files /dev/null and b/core/src/main/res/drawable-hdpi/navigation_collapse_dark.png differ diff --git a/core/src/main/res/drawable-hdpi/navigation_expand.png b/core/src/main/res/drawable-hdpi/navigation_expand.png new file mode 100644 index 000000000..8225e74b7 Binary files /dev/null and b/core/src/main/res/drawable-hdpi/navigation_expand.png differ diff --git a/core/src/main/res/drawable-hdpi/navigation_expand_dark.png b/core/src/main/res/drawable-hdpi/navigation_expand_dark.png new file mode 100755 index 000000000..1676b104b Binary files /dev/null and b/core/src/main/res/drawable-hdpi/navigation_expand_dark.png differ diff --git a/core/src/main/res/drawable-hdpi/navigation_refresh.png b/core/src/main/res/drawable-hdpi/navigation_refresh.png new file mode 100644 index 000000000..479aca465 Binary files /dev/null and b/core/src/main/res/drawable-hdpi/navigation_refresh.png differ diff --git a/core/src/main/res/drawable-hdpi/navigation_refresh_dark.png b/core/src/main/res/drawable-hdpi/navigation_refresh_dark.png new file mode 100755 index 000000000..bb9d855f7 Binary files /dev/null and b/core/src/main/res/drawable-hdpi/navigation_refresh_dark.png differ diff --git a/core/src/main/res/drawable-hdpi/navigation_shownotes.png b/core/src/main/res/drawable-hdpi/navigation_shownotes.png new file mode 100755 index 000000000..c5f6c97b2 Binary files /dev/null and b/core/src/main/res/drawable-hdpi/navigation_shownotes.png differ diff --git a/core/src/main/res/drawable-hdpi/navigation_shownotes_dark.png b/core/src/main/res/drawable-hdpi/navigation_shownotes_dark.png new file mode 100755 index 000000000..e45ea1fd9 Binary files /dev/null and b/core/src/main/res/drawable-hdpi/navigation_shownotes_dark.png differ diff --git a/core/src/main/res/drawable-hdpi/navigation_up.png b/core/src/main/res/drawable-hdpi/navigation_up.png new file mode 100755 index 000000000..a2cf2ba52 Binary files /dev/null and b/core/src/main/res/drawable-hdpi/navigation_up.png differ diff --git a/core/src/main/res/drawable-hdpi/navigation_up_dark.png b/core/src/main/res/drawable-hdpi/navigation_up_dark.png new file mode 100755 index 000000000..f2374a323 Binary files /dev/null and b/core/src/main/res/drawable-hdpi/navigation_up_dark.png differ diff --git a/core/src/main/res/drawable-hdpi/social_share.png b/core/src/main/res/drawable-hdpi/social_share.png new file mode 100644 index 000000000..47ae18674 Binary files /dev/null and b/core/src/main/res/drawable-hdpi/social_share.png differ diff --git a/core/src/main/res/drawable-hdpi/social_share_dark.png b/core/src/main/res/drawable-hdpi/social_share_dark.png new file mode 100755 index 000000000..c329f58da Binary files /dev/null and b/core/src/main/res/drawable-hdpi/social_share_dark.png differ diff --git a/core/src/main/res/drawable-hdpi/spinner_button.9.png b/core/src/main/res/drawable-hdpi/spinner_button.9.png new file mode 100644 index 000000000..fa68a137f Binary files /dev/null and b/core/src/main/res/drawable-hdpi/spinner_button.9.png differ diff --git a/core/src/main/res/drawable-hdpi/spinner_button_dark.9.png b/core/src/main/res/drawable-hdpi/spinner_button_dark.9.png new file mode 100644 index 000000000..88f8765cd Binary files /dev/null and b/core/src/main/res/drawable-hdpi/spinner_button_dark.9.png differ diff --git a/core/src/main/res/drawable-hdpi/stat_notify_sync.png b/core/src/main/res/drawable-hdpi/stat_notify_sync.png new file mode 100644 index 000000000..bfb8110fe Binary files /dev/null and b/core/src/main/res/drawable-hdpi/stat_notify_sync.png differ diff --git a/core/src/main/res/drawable-hdpi/stat_notify_sync_error.png b/core/src/main/res/drawable-hdpi/stat_notify_sync_error.png new file mode 100644 index 000000000..b340a313e Binary files /dev/null and b/core/src/main/res/drawable-hdpi/stat_notify_sync_error.png differ diff --git a/core/src/main/res/drawable-hdpi/stat_playlist.png b/core/src/main/res/drawable-hdpi/stat_playlist.png new file mode 100644 index 000000000..93c3f02b8 Binary files /dev/null and b/core/src/main/res/drawable-hdpi/stat_playlist.png differ diff --git a/core/src/main/res/drawable-hdpi/stat_playlist_dark.png b/core/src/main/res/drawable-hdpi/stat_playlist_dark.png new file mode 100644 index 000000000..972ce98b3 Binary files /dev/null and b/core/src/main/res/drawable-hdpi/stat_playlist_dark.png differ diff --git a/core/src/main/res/drawable-hdpi/type_audio.png b/core/src/main/res/drawable-hdpi/type_audio.png new file mode 100644 index 000000000..d43e8a33c Binary files /dev/null and b/core/src/main/res/drawable-hdpi/type_audio.png differ diff --git a/core/src/main/res/drawable-hdpi/type_audio_dark.png b/core/src/main/res/drawable-hdpi/type_audio_dark.png new file mode 100755 index 000000000..7b69ea56b Binary files /dev/null and b/core/src/main/res/drawable-hdpi/type_audio_dark.png differ diff --git a/core/src/main/res/drawable-hdpi/type_video.png b/core/src/main/res/drawable-hdpi/type_video.png new file mode 100644 index 000000000..f9467146c Binary files /dev/null and b/core/src/main/res/drawable-hdpi/type_video.png differ diff --git a/core/src/main/res/drawable-hdpi/type_video_dark.png b/core/src/main/res/drawable-hdpi/type_video_dark.png new file mode 100755 index 000000000..37f3a93a2 Binary files /dev/null and b/core/src/main/res/drawable-hdpi/type_video_dark.png differ diff --git a/core/src/main/res/drawable-ldpi-v11/ic_stat_antenna.png b/core/src/main/res/drawable-ldpi-v11/ic_stat_antenna.png new file mode 100644 index 000000000..e44f42510 Binary files /dev/null and b/core/src/main/res/drawable-ldpi-v11/ic_stat_antenna.png differ diff --git a/core/src/main/res/drawable-ldpi/action_stream.png b/core/src/main/res/drawable-ldpi/action_stream.png new file mode 100644 index 000000000..5ae4f3d34 Binary files /dev/null and b/core/src/main/res/drawable-ldpi/action_stream.png differ diff --git a/core/src/main/res/drawable-ldpi/action_stream_dark.png b/core/src/main/res/drawable-ldpi/action_stream_dark.png new file mode 100644 index 000000000..f3c81fff8 Binary files /dev/null and b/core/src/main/res/drawable-ldpi/action_stream_dark.png differ diff --git a/core/src/main/res/drawable-ldpi/ic_launcher.png b/core/src/main/res/drawable-ldpi/ic_launcher.png new file mode 100644 index 000000000..546090dd2 Binary files /dev/null and b/core/src/main/res/drawable-ldpi/ic_launcher.png differ diff --git a/core/src/main/res/drawable-ldpi/ic_stat_antenna.png b/core/src/main/res/drawable-ldpi/ic_stat_antenna.png new file mode 100644 index 000000000..63d72970d Binary files /dev/null and b/core/src/main/res/drawable-ldpi/ic_stat_antenna.png differ diff --git a/core/src/main/res/drawable-ldpi/stat_playlist.png b/core/src/main/res/drawable-ldpi/stat_playlist.png new file mode 100644 index 000000000..3a702ef2f Binary files /dev/null and b/core/src/main/res/drawable-ldpi/stat_playlist.png differ diff --git a/core/src/main/res/drawable-ldpi/stat_playlist_dark.png b/core/src/main/res/drawable-ldpi/stat_playlist_dark.png new file mode 100644 index 000000000..b82b06f67 Binary files /dev/null and b/core/src/main/res/drawable-ldpi/stat_playlist_dark.png differ diff --git a/core/src/main/res/drawable-mdpi-v11/ic_stat_antenna.png b/core/src/main/res/drawable-mdpi-v11/ic_stat_antenna.png new file mode 100644 index 000000000..8808dedc7 Binary files /dev/null and b/core/src/main/res/drawable-mdpi-v11/ic_stat_antenna.png differ diff --git a/core/src/main/res/drawable-mdpi-v11/ic_stat_authentication.png b/core/src/main/res/drawable-mdpi-v11/ic_stat_authentication.png new file mode 100755 index 000000000..de69b17c0 Binary files /dev/null and b/core/src/main/res/drawable-mdpi-v11/ic_stat_authentication.png differ diff --git a/core/src/main/res/drawable-mdpi-v11/stat_notify_sync.png b/core/src/main/res/drawable-mdpi-v11/stat_notify_sync.png new file mode 100644 index 000000000..1be8677f1 Binary files /dev/null and b/core/src/main/res/drawable-mdpi-v11/stat_notify_sync.png differ diff --git a/core/src/main/res/drawable-mdpi-v11/stat_notify_sync_error.png b/core/src/main/res/drawable-mdpi-v11/stat_notify_sync_error.png new file mode 100644 index 000000000..30658c583 Binary files /dev/null and b/core/src/main/res/drawable-mdpi-v11/stat_notify_sync_error.png differ diff --git a/core/src/main/res/drawable-mdpi/action_about.png b/core/src/main/res/drawable-mdpi/action_about.png new file mode 100644 index 000000000..7c57436fc Binary files /dev/null and b/core/src/main/res/drawable-mdpi/action_about.png differ diff --git a/core/src/main/res/drawable-mdpi/action_about_dark.png b/core/src/main/res/drawable-mdpi/action_about_dark.png new file mode 100755 index 000000000..d7b7e6986 Binary files /dev/null and b/core/src/main/res/drawable-mdpi/action_about_dark.png differ diff --git a/core/src/main/res/drawable-mdpi/action_search.png b/core/src/main/res/drawable-mdpi/action_search.png new file mode 100644 index 000000000..3aa644048 Binary files /dev/null and b/core/src/main/res/drawable-mdpi/action_search.png differ diff --git a/core/src/main/res/drawable-mdpi/action_search_dark.png b/core/src/main/res/drawable-mdpi/action_search_dark.png new file mode 100755 index 000000000..587d9e0bf Binary files /dev/null and b/core/src/main/res/drawable-mdpi/action_search_dark.png differ diff --git a/core/src/main/res/drawable-mdpi/action_settings.png b/core/src/main/res/drawable-mdpi/action_settings.png new file mode 100644 index 000000000..dc66d914e Binary files /dev/null and b/core/src/main/res/drawable-mdpi/action_settings.png differ diff --git a/core/src/main/res/drawable-mdpi/action_settings_dark.png b/core/src/main/res/drawable-mdpi/action_settings_dark.png new file mode 100755 index 000000000..d3e42edcb Binary files /dev/null and b/core/src/main/res/drawable-mdpi/action_settings_dark.png differ diff --git a/core/src/main/res/drawable-mdpi/action_stream.png b/core/src/main/res/drawable-mdpi/action_stream.png new file mode 100644 index 000000000..4bc7d8379 Binary files /dev/null and b/core/src/main/res/drawable-mdpi/action_stream.png differ diff --git a/core/src/main/res/drawable-mdpi/action_stream_dark.png b/core/src/main/res/drawable-mdpi/action_stream_dark.png new file mode 100644 index 000000000..1f4fdd186 Binary files /dev/null and b/core/src/main/res/drawable-mdpi/action_stream_dark.png differ diff --git a/core/src/main/res/drawable-mdpi/av_download.png b/core/src/main/res/drawable-mdpi/av_download.png new file mode 100644 index 000000000..678ecfad4 Binary files /dev/null and b/core/src/main/res/drawable-mdpi/av_download.png differ diff --git a/core/src/main/res/drawable-mdpi/av_download_dark.png b/core/src/main/res/drawable-mdpi/av_download_dark.png new file mode 100755 index 000000000..cc4d9576b Binary files /dev/null and b/core/src/main/res/drawable-mdpi/av_download_dark.png differ diff --git a/core/src/main/res/drawable-mdpi/av_fast_forward.png b/core/src/main/res/drawable-mdpi/av_fast_forward.png new file mode 100644 index 000000000..43f15a245 Binary files /dev/null and b/core/src/main/res/drawable-mdpi/av_fast_forward.png differ diff --git a/core/src/main/res/drawable-mdpi/av_fast_forward_dark.png b/core/src/main/res/drawable-mdpi/av_fast_forward_dark.png new file mode 100755 index 000000000..fc8074cea Binary files /dev/null and b/core/src/main/res/drawable-mdpi/av_fast_forward_dark.png differ diff --git a/core/src/main/res/drawable-mdpi/av_pause.png b/core/src/main/res/drawable-mdpi/av_pause.png new file mode 100644 index 000000000..01858e34d Binary files /dev/null and b/core/src/main/res/drawable-mdpi/av_pause.png differ diff --git a/core/src/main/res/drawable-mdpi/av_pause_dark.png b/core/src/main/res/drawable-mdpi/av_pause_dark.png new file mode 100755 index 000000000..a5aee6f2c Binary files /dev/null and b/core/src/main/res/drawable-mdpi/av_pause_dark.png differ diff --git a/core/src/main/res/drawable-mdpi/av_play.png b/core/src/main/res/drawable-mdpi/av_play.png new file mode 100644 index 000000000..1e3bc97af Binary files /dev/null and b/core/src/main/res/drawable-mdpi/av_play.png differ diff --git a/core/src/main/res/drawable-mdpi/av_play_dark.png b/core/src/main/res/drawable-mdpi/av_play_dark.png new file mode 100755 index 000000000..6a40cd5f7 Binary files /dev/null and b/core/src/main/res/drawable-mdpi/av_play_dark.png differ diff --git a/core/src/main/res/drawable-mdpi/av_rewind.png b/core/src/main/res/drawable-mdpi/av_rewind.png new file mode 100644 index 000000000..a2f7f5895 Binary files /dev/null and b/core/src/main/res/drawable-mdpi/av_rewind.png differ diff --git a/core/src/main/res/drawable-mdpi/av_rewind_dark.png b/core/src/main/res/drawable-mdpi/av_rewind_dark.png new file mode 100755 index 000000000..e555a2046 Binary files /dev/null and b/core/src/main/res/drawable-mdpi/av_rewind_dark.png differ diff --git a/core/src/main/res/drawable-mdpi/content_discard.png b/core/src/main/res/drawable-mdpi/content_discard.png new file mode 100644 index 000000000..cedb1085b Binary files /dev/null and b/core/src/main/res/drawable-mdpi/content_discard.png differ diff --git a/core/src/main/res/drawable-mdpi/content_discard_dark.png b/core/src/main/res/drawable-mdpi/content_discard_dark.png new file mode 100755 index 000000000..a8ee5f253 Binary files /dev/null and b/core/src/main/res/drawable-mdpi/content_discard_dark.png differ diff --git a/core/src/main/res/drawable-mdpi/content_new.png b/core/src/main/res/drawable-mdpi/content_new.png new file mode 100644 index 000000000..884c9d270 Binary files /dev/null and b/core/src/main/res/drawable-mdpi/content_new.png differ diff --git a/core/src/main/res/drawable-mdpi/content_new_dark.png b/core/src/main/res/drawable-mdpi/content_new_dark.png new file mode 100755 index 000000000..4d5d484b3 Binary files /dev/null and b/core/src/main/res/drawable-mdpi/content_new_dark.png differ diff --git a/core/src/main/res/drawable-mdpi/default_cover.png b/core/src/main/res/drawable-mdpi/default_cover.png new file mode 100644 index 000000000..62adf32ab Binary files /dev/null and b/core/src/main/res/drawable-mdpi/default_cover.png differ diff --git a/core/src/main/res/drawable-mdpi/default_cover_dark.png b/core/src/main/res/drawable-mdpi/default_cover_dark.png new file mode 100755 index 000000000..d6235554b Binary files /dev/null and b/core/src/main/res/drawable-mdpi/default_cover_dark.png differ diff --git a/core/src/main/res/drawable-mdpi/device_access_time.png b/core/src/main/res/drawable-mdpi/device_access_time.png new file mode 100644 index 000000000..de9b4fb2a Binary files /dev/null and b/core/src/main/res/drawable-mdpi/device_access_time.png differ diff --git a/core/src/main/res/drawable-mdpi/device_access_time_dark.png b/core/src/main/res/drawable-mdpi/device_access_time_dark.png new file mode 100755 index 000000000..a09df2b99 Binary files /dev/null and b/core/src/main/res/drawable-mdpi/device_access_time_dark.png differ diff --git a/core/src/main/res/drawable-mdpi/ic_action_overflow.png b/core/src/main/res/drawable-mdpi/ic_action_overflow.png new file mode 100644 index 000000000..6f0fb23f4 Binary files /dev/null and b/core/src/main/res/drawable-mdpi/ic_action_overflow.png differ diff --git a/core/src/main/res/drawable-mdpi/ic_action_overflow_dark.png b/core/src/main/res/drawable-mdpi/ic_action_overflow_dark.png new file mode 100644 index 000000000..b4a4a221f Binary files /dev/null and b/core/src/main/res/drawable-mdpi/ic_action_overflow_dark.png differ diff --git a/core/src/main/res/drawable-mdpi/ic_action_pause_over_video.png b/core/src/main/res/drawable-mdpi/ic_action_pause_over_video.png new file mode 100755 index 000000000..f478ac321 Binary files /dev/null and b/core/src/main/res/drawable-mdpi/ic_action_pause_over_video.png differ diff --git a/core/src/main/res/drawable-mdpi/ic_action_play_over_video.png b/core/src/main/res/drawable-mdpi/ic_action_play_over_video.png new file mode 100755 index 000000000..835ff3636 Binary files /dev/null and b/core/src/main/res/drawable-mdpi/ic_action_play_over_video.png differ diff --git a/core/src/main/res/drawable-mdpi/ic_drag_handle.png b/core/src/main/res/drawable-mdpi/ic_drag_handle.png new file mode 100755 index 000000000..4afbdc67d Binary files /dev/null and b/core/src/main/res/drawable-mdpi/ic_drag_handle.png differ diff --git a/core/src/main/res/drawable-mdpi/ic_drag_handle_dark.png b/core/src/main/res/drawable-mdpi/ic_drag_handle_dark.png new file mode 100755 index 000000000..2b25c4101 Binary files /dev/null and b/core/src/main/res/drawable-mdpi/ic_drag_handle_dark.png differ diff --git a/core/src/main/res/drawable-mdpi/ic_drawer.png b/core/src/main/res/drawable-mdpi/ic_drawer.png new file mode 100644 index 000000000..1ed2c56ee Binary files /dev/null and b/core/src/main/res/drawable-mdpi/ic_drawer.png differ diff --git a/core/src/main/res/drawable-mdpi/ic_drawer_dark.png b/core/src/main/res/drawable-mdpi/ic_drawer_dark.png new file mode 100644 index 000000000..b05c026c1 Binary files /dev/null and b/core/src/main/res/drawable-mdpi/ic_drawer_dark.png differ diff --git a/core/src/main/res/drawable-mdpi/ic_launcher.png b/core/src/main/res/drawable-mdpi/ic_launcher.png new file mode 100644 index 000000000..403dfabc4 Binary files /dev/null and b/core/src/main/res/drawable-mdpi/ic_launcher.png differ diff --git a/core/src/main/res/drawable-mdpi/ic_new.png b/core/src/main/res/drawable-mdpi/ic_new.png new file mode 100755 index 000000000..84994bd10 Binary files /dev/null and b/core/src/main/res/drawable-mdpi/ic_new.png differ diff --git a/core/src/main/res/drawable-mdpi/ic_new_dark.png b/core/src/main/res/drawable-mdpi/ic_new_dark.png new file mode 100755 index 000000000..b723618b4 Binary files /dev/null and b/core/src/main/res/drawable-mdpi/ic_new_dark.png differ diff --git a/core/src/main/res/drawable-mdpi/ic_stat_antenna.png b/core/src/main/res/drawable-mdpi/ic_stat_antenna.png new file mode 100644 index 000000000..8b1206b51 Binary files /dev/null and b/core/src/main/res/drawable-mdpi/ic_stat_antenna.png differ diff --git a/core/src/main/res/drawable-mdpi/ic_stat_authentication.png b/core/src/main/res/drawable-mdpi/ic_stat_authentication.png new file mode 100755 index 000000000..cadfb9643 Binary files /dev/null and b/core/src/main/res/drawable-mdpi/ic_stat_authentication.png differ diff --git a/core/src/main/res/drawable-mdpi/location_web_site.png b/core/src/main/res/drawable-mdpi/location_web_site.png new file mode 100644 index 000000000..f146cf997 Binary files /dev/null and b/core/src/main/res/drawable-mdpi/location_web_site.png differ diff --git a/core/src/main/res/drawable-mdpi/location_web_site_dark.png b/core/src/main/res/drawable-mdpi/location_web_site_dark.png new file mode 100755 index 000000000..41b56ec92 Binary files /dev/null and b/core/src/main/res/drawable-mdpi/location_web_site_dark.png differ diff --git a/core/src/main/res/drawable-mdpi/navigation_accept.png b/core/src/main/res/drawable-mdpi/navigation_accept.png new file mode 100644 index 000000000..cf5fab3ad Binary files /dev/null and b/core/src/main/res/drawable-mdpi/navigation_accept.png differ diff --git a/core/src/main/res/drawable-mdpi/navigation_accept_dark.png b/core/src/main/res/drawable-mdpi/navigation_accept_dark.png new file mode 100755 index 000000000..35cda8e11 Binary files /dev/null and b/core/src/main/res/drawable-mdpi/navigation_accept_dark.png differ diff --git a/core/src/main/res/drawable-mdpi/navigation_cancel.png b/core/src/main/res/drawable-mdpi/navigation_cancel.png new file mode 100644 index 000000000..9f4c3d6a2 Binary files /dev/null and b/core/src/main/res/drawable-mdpi/navigation_cancel.png differ diff --git a/core/src/main/res/drawable-mdpi/navigation_cancel_dark.png b/core/src/main/res/drawable-mdpi/navigation_cancel_dark.png new file mode 100755 index 000000000..3336760d5 Binary files /dev/null and b/core/src/main/res/drawable-mdpi/navigation_cancel_dark.png differ diff --git a/core/src/main/res/drawable-mdpi/navigation_chapters.png b/core/src/main/res/drawable-mdpi/navigation_chapters.png new file mode 100755 index 000000000..b1884726c Binary files /dev/null and b/core/src/main/res/drawable-mdpi/navigation_chapters.png differ diff --git a/core/src/main/res/drawable-mdpi/navigation_chapters_dark.png b/core/src/main/res/drawable-mdpi/navigation_chapters_dark.png new file mode 100755 index 000000000..1042294e4 Binary files /dev/null and b/core/src/main/res/drawable-mdpi/navigation_chapters_dark.png differ diff --git a/core/src/main/res/drawable-mdpi/navigation_collapse.png b/core/src/main/res/drawable-mdpi/navigation_collapse.png new file mode 100755 index 000000000..6673c7aea Binary files /dev/null and b/core/src/main/res/drawable-mdpi/navigation_collapse.png differ diff --git a/core/src/main/res/drawable-mdpi/navigation_collapse_dark.png b/core/src/main/res/drawable-mdpi/navigation_collapse_dark.png new file mode 100755 index 000000000..01d6511ee Binary files /dev/null and b/core/src/main/res/drawable-mdpi/navigation_collapse_dark.png differ diff --git a/core/src/main/res/drawable-mdpi/navigation_expand.png b/core/src/main/res/drawable-mdpi/navigation_expand.png new file mode 100644 index 000000000..78107862c Binary files /dev/null and b/core/src/main/res/drawable-mdpi/navigation_expand.png differ diff --git a/core/src/main/res/drawable-mdpi/navigation_expand_dark.png b/core/src/main/res/drawable-mdpi/navigation_expand_dark.png new file mode 100755 index 000000000..aa2b40ca0 Binary files /dev/null and b/core/src/main/res/drawable-mdpi/navigation_expand_dark.png differ diff --git a/core/src/main/res/drawable-mdpi/navigation_refresh.png b/core/src/main/res/drawable-mdpi/navigation_refresh.png new file mode 100644 index 000000000..63e70e178 Binary files /dev/null and b/core/src/main/res/drawable-mdpi/navigation_refresh.png differ diff --git a/core/src/main/res/drawable-mdpi/navigation_refresh_dark.png b/core/src/main/res/drawable-mdpi/navigation_refresh_dark.png new file mode 100755 index 000000000..bd611e8e2 Binary files /dev/null and b/core/src/main/res/drawable-mdpi/navigation_refresh_dark.png differ diff --git a/core/src/main/res/drawable-mdpi/navigation_shownotes.png b/core/src/main/res/drawable-mdpi/navigation_shownotes.png new file mode 100755 index 000000000..ec6a2bf8f Binary files /dev/null and b/core/src/main/res/drawable-mdpi/navigation_shownotes.png differ diff --git a/core/src/main/res/drawable-mdpi/navigation_shownotes_dark.png b/core/src/main/res/drawable-mdpi/navigation_shownotes_dark.png new file mode 100755 index 000000000..9c748b0b5 Binary files /dev/null and b/core/src/main/res/drawable-mdpi/navigation_shownotes_dark.png differ diff --git a/core/src/main/res/drawable-mdpi/navigation_up.png b/core/src/main/res/drawable-mdpi/navigation_up.png new file mode 100755 index 000000000..1ee248a79 Binary files /dev/null and b/core/src/main/res/drawable-mdpi/navigation_up.png differ diff --git a/core/src/main/res/drawable-mdpi/navigation_up_dark.png b/core/src/main/res/drawable-mdpi/navigation_up_dark.png new file mode 100755 index 000000000..8ef44cbac Binary files /dev/null and b/core/src/main/res/drawable-mdpi/navigation_up_dark.png differ diff --git a/core/src/main/res/drawable-mdpi/social_share.png b/core/src/main/res/drawable-mdpi/social_share.png new file mode 100644 index 000000000..8aa52bc7d Binary files /dev/null and b/core/src/main/res/drawable-mdpi/social_share.png differ diff --git a/core/src/main/res/drawable-mdpi/social_share_dark.png b/core/src/main/res/drawable-mdpi/social_share_dark.png new file mode 100755 index 000000000..056deb57b Binary files /dev/null and b/core/src/main/res/drawable-mdpi/social_share_dark.png differ diff --git a/core/src/main/res/drawable-mdpi/spinner_button.9.png b/core/src/main/res/drawable-mdpi/spinner_button.9.png new file mode 100644 index 000000000..716560bb1 Binary files /dev/null and b/core/src/main/res/drawable-mdpi/spinner_button.9.png differ diff --git a/core/src/main/res/drawable-mdpi/spinner_button_dark.9.png b/core/src/main/res/drawable-mdpi/spinner_button_dark.9.png new file mode 100644 index 000000000..8d7594685 Binary files /dev/null and b/core/src/main/res/drawable-mdpi/spinner_button_dark.9.png differ diff --git a/core/src/main/res/drawable-mdpi/stat_notify_sync.png b/core/src/main/res/drawable-mdpi/stat_notify_sync.png new file mode 100644 index 000000000..03ce57a47 Binary files /dev/null and b/core/src/main/res/drawable-mdpi/stat_notify_sync.png differ diff --git a/core/src/main/res/drawable-mdpi/stat_notify_sync_error.png b/core/src/main/res/drawable-mdpi/stat_notify_sync_error.png new file mode 100644 index 000000000..f849b5040 Binary files /dev/null and b/core/src/main/res/drawable-mdpi/stat_notify_sync_error.png differ diff --git a/core/src/main/res/drawable-mdpi/stat_playlist.png b/core/src/main/res/drawable-mdpi/stat_playlist.png new file mode 100644 index 000000000..136a7a265 Binary files /dev/null and b/core/src/main/res/drawable-mdpi/stat_playlist.png differ diff --git a/core/src/main/res/drawable-mdpi/stat_playlist_dark.png b/core/src/main/res/drawable-mdpi/stat_playlist_dark.png new file mode 100644 index 000000000..7ed94b13c Binary files /dev/null and b/core/src/main/res/drawable-mdpi/stat_playlist_dark.png differ diff --git a/core/src/main/res/drawable-mdpi/type_audio.png b/core/src/main/res/drawable-mdpi/type_audio.png new file mode 100644 index 000000000..4ec9efd97 Binary files /dev/null and b/core/src/main/res/drawable-mdpi/type_audio.png differ diff --git a/core/src/main/res/drawable-mdpi/type_audio_dark.png b/core/src/main/res/drawable-mdpi/type_audio_dark.png new file mode 100755 index 000000000..f8dd8469c Binary files /dev/null and b/core/src/main/res/drawable-mdpi/type_audio_dark.png differ diff --git a/core/src/main/res/drawable-mdpi/type_video.png b/core/src/main/res/drawable-mdpi/type_video.png new file mode 100644 index 000000000..a2722b812 Binary files /dev/null and b/core/src/main/res/drawable-mdpi/type_video.png differ diff --git a/core/src/main/res/drawable-mdpi/type_video_dark.png b/core/src/main/res/drawable-mdpi/type_video_dark.png new file mode 100755 index 000000000..aa0c320dc Binary files /dev/null and b/core/src/main/res/drawable-mdpi/type_video_dark.png differ diff --git a/core/src/main/res/drawable-xhdpi-v11/ic_stat_antenna.png b/core/src/main/res/drawable-xhdpi-v11/ic_stat_antenna.png new file mode 100644 index 000000000..59de64c87 Binary files /dev/null and b/core/src/main/res/drawable-xhdpi-v11/ic_stat_antenna.png differ diff --git a/core/src/main/res/drawable-xhdpi-v11/ic_stat_authentication.png b/core/src/main/res/drawable-xhdpi-v11/ic_stat_authentication.png new file mode 100755 index 000000000..f58fb21df Binary files /dev/null and b/core/src/main/res/drawable-xhdpi-v11/ic_stat_authentication.png differ diff --git a/core/src/main/res/drawable-xhdpi-v11/stat_notify_sync.png b/core/src/main/res/drawable-xhdpi-v11/stat_notify_sync.png new file mode 100644 index 000000000..b3bf21ffe Binary files /dev/null and b/core/src/main/res/drawable-xhdpi-v11/stat_notify_sync.png differ diff --git a/core/src/main/res/drawable-xhdpi-v11/stat_notify_sync_error.png b/core/src/main/res/drawable-xhdpi-v11/stat_notify_sync_error.png new file mode 100644 index 000000000..33582ef10 Binary files /dev/null and b/core/src/main/res/drawable-xhdpi-v11/stat_notify_sync_error.png differ diff --git a/core/src/main/res/drawable-xhdpi/action_about.png b/core/src/main/res/drawable-xhdpi/action_about.png new file mode 100644 index 000000000..2641f142a Binary files /dev/null and b/core/src/main/res/drawable-xhdpi/action_about.png differ diff --git a/core/src/main/res/drawable-xhdpi/action_about_dark.png b/core/src/main/res/drawable-xhdpi/action_about_dark.png new file mode 100755 index 000000000..4ee903f07 Binary files /dev/null and b/core/src/main/res/drawable-xhdpi/action_about_dark.png differ diff --git a/core/src/main/res/drawable-xhdpi/action_search.png b/core/src/main/res/drawable-xhdpi/action_search.png new file mode 100644 index 000000000..804420aee Binary files /dev/null and b/core/src/main/res/drawable-xhdpi/action_search.png differ diff --git a/core/src/main/res/drawable-xhdpi/action_search_dark.png b/core/src/main/res/drawable-xhdpi/action_search_dark.png new file mode 100755 index 000000000..3549f84dd Binary files /dev/null and b/core/src/main/res/drawable-xhdpi/action_search_dark.png differ diff --git a/core/src/main/res/drawable-xhdpi/action_settings.png b/core/src/main/res/drawable-xhdpi/action_settings.png new file mode 100644 index 000000000..04b65dc34 Binary files /dev/null and b/core/src/main/res/drawable-xhdpi/action_settings.png differ diff --git a/core/src/main/res/drawable-xhdpi/action_settings_dark.png b/core/src/main/res/drawable-xhdpi/action_settings_dark.png new file mode 100755 index 000000000..09b014834 Binary files /dev/null and b/core/src/main/res/drawable-xhdpi/action_settings_dark.png differ diff --git a/core/src/main/res/drawable-xhdpi/action_stream.png b/core/src/main/res/drawable-xhdpi/action_stream.png new file mode 100644 index 000000000..f87f2da5e Binary files /dev/null and b/core/src/main/res/drawable-xhdpi/action_stream.png differ diff --git a/core/src/main/res/drawable-xhdpi/action_stream_dark.png b/core/src/main/res/drawable-xhdpi/action_stream_dark.png new file mode 100644 index 000000000..d3721318c Binary files /dev/null and b/core/src/main/res/drawable-xhdpi/action_stream_dark.png differ diff --git a/core/src/main/res/drawable-xhdpi/av_download.png b/core/src/main/res/drawable-xhdpi/av_download.png new file mode 100644 index 000000000..dfe81e064 Binary files /dev/null and b/core/src/main/res/drawable-xhdpi/av_download.png differ diff --git a/core/src/main/res/drawable-xhdpi/av_download_dark.png b/core/src/main/res/drawable-xhdpi/av_download_dark.png new file mode 100755 index 000000000..bc0ced50f Binary files /dev/null and b/core/src/main/res/drawable-xhdpi/av_download_dark.png differ diff --git a/core/src/main/res/drawable-xhdpi/av_fast_forward.png b/core/src/main/res/drawable-xhdpi/av_fast_forward.png new file mode 100644 index 000000000..026c3b779 Binary files /dev/null and b/core/src/main/res/drawable-xhdpi/av_fast_forward.png differ diff --git a/core/src/main/res/drawable-xhdpi/av_fast_forward_dark.png b/core/src/main/res/drawable-xhdpi/av_fast_forward_dark.png new file mode 100755 index 000000000..896334d47 Binary files /dev/null and b/core/src/main/res/drawable-xhdpi/av_fast_forward_dark.png differ diff --git a/core/src/main/res/drawable-xhdpi/av_pause.png b/core/src/main/res/drawable-xhdpi/av_pause.png new file mode 100644 index 000000000..97d6f91ac Binary files /dev/null and b/core/src/main/res/drawable-xhdpi/av_pause.png differ diff --git a/core/src/main/res/drawable-xhdpi/av_pause_dark.png b/core/src/main/res/drawable-xhdpi/av_pause_dark.png new file mode 100755 index 000000000..333c1b24d Binary files /dev/null and b/core/src/main/res/drawable-xhdpi/av_pause_dark.png differ diff --git a/core/src/main/res/drawable-xhdpi/av_play.png b/core/src/main/res/drawable-xhdpi/av_play.png new file mode 100644 index 000000000..2d67d31e7 Binary files /dev/null and b/core/src/main/res/drawable-xhdpi/av_play.png differ diff --git a/core/src/main/res/drawable-xhdpi/av_play_dark.png b/core/src/main/res/drawable-xhdpi/av_play_dark.png new file mode 100755 index 000000000..51124993d Binary files /dev/null and b/core/src/main/res/drawable-xhdpi/av_play_dark.png differ diff --git a/core/src/main/res/drawable-xhdpi/av_rewind.png b/core/src/main/res/drawable-xhdpi/av_rewind.png new file mode 100644 index 000000000..57b41744d Binary files /dev/null and b/core/src/main/res/drawable-xhdpi/av_rewind.png differ diff --git a/core/src/main/res/drawable-xhdpi/av_rewind_dark.png b/core/src/main/res/drawable-xhdpi/av_rewind_dark.png new file mode 100755 index 000000000..69dda127c Binary files /dev/null and b/core/src/main/res/drawable-xhdpi/av_rewind_dark.png differ diff --git a/core/src/main/res/drawable-xhdpi/content_discard.png b/core/src/main/res/drawable-xhdpi/content_discard.png new file mode 100644 index 000000000..98c73da1f Binary files /dev/null and b/core/src/main/res/drawable-xhdpi/content_discard.png differ diff --git a/core/src/main/res/drawable-xhdpi/content_discard_dark.png b/core/src/main/res/drawable-xhdpi/content_discard_dark.png new file mode 100755 index 000000000..412b33354 Binary files /dev/null and b/core/src/main/res/drawable-xhdpi/content_discard_dark.png differ diff --git a/core/src/main/res/drawable-xhdpi/content_new.png b/core/src/main/res/drawable-xhdpi/content_new.png new file mode 100644 index 000000000..9b48a63da Binary files /dev/null and b/core/src/main/res/drawable-xhdpi/content_new.png differ diff --git a/core/src/main/res/drawable-xhdpi/content_new_dark.png b/core/src/main/res/drawable-xhdpi/content_new_dark.png new file mode 100755 index 000000000..23b9a1c18 Binary files /dev/null and b/core/src/main/res/drawable-xhdpi/content_new_dark.png differ diff --git a/core/src/main/res/drawable-xhdpi/content_remove.png b/core/src/main/res/drawable-xhdpi/content_remove.png new file mode 100644 index 000000000..ca7d159fd Binary files /dev/null and b/core/src/main/res/drawable-xhdpi/content_remove.png differ diff --git a/core/src/main/res/drawable-xhdpi/content_remove_dark.png b/core/src/main/res/drawable-xhdpi/content_remove_dark.png new file mode 100755 index 000000000..f391760ef Binary files /dev/null and b/core/src/main/res/drawable-xhdpi/content_remove_dark.png differ diff --git a/core/src/main/res/drawable-xhdpi/default_cover.png b/core/src/main/res/drawable-xhdpi/default_cover.png new file mode 100644 index 000000000..c2f4578f9 Binary files /dev/null and b/core/src/main/res/drawable-xhdpi/default_cover.png differ diff --git a/core/src/main/res/drawable-xhdpi/default_cover_dark.png b/core/src/main/res/drawable-xhdpi/default_cover_dark.png new file mode 100755 index 000000000..3f93e4f65 Binary files /dev/null and b/core/src/main/res/drawable-xhdpi/default_cover_dark.png differ diff --git a/core/src/main/res/drawable-xhdpi/device_access_time.png b/core/src/main/res/drawable-xhdpi/device_access_time.png new file mode 100644 index 000000000..2beae08c3 Binary files /dev/null and b/core/src/main/res/drawable-xhdpi/device_access_time.png differ diff --git a/core/src/main/res/drawable-xhdpi/device_access_time_dark.png b/core/src/main/res/drawable-xhdpi/device_access_time_dark.png new file mode 100755 index 000000000..c8771db97 Binary files /dev/null and b/core/src/main/res/drawable-xhdpi/device_access_time_dark.png differ diff --git a/core/src/main/res/drawable-xhdpi/ic_action_overflow.png b/core/src/main/res/drawable-xhdpi/ic_action_overflow.png new file mode 100644 index 000000000..7ba4e10ea Binary files /dev/null and b/core/src/main/res/drawable-xhdpi/ic_action_overflow.png differ diff --git a/core/src/main/res/drawable-xhdpi/ic_action_overflow_dark.png b/core/src/main/res/drawable-xhdpi/ic_action_overflow_dark.png new file mode 100644 index 000000000..5d8af5d63 Binary files /dev/null and b/core/src/main/res/drawable-xhdpi/ic_action_overflow_dark.png differ diff --git a/core/src/main/res/drawable-xhdpi/ic_action_pause_over_video.png b/core/src/main/res/drawable-xhdpi/ic_action_pause_over_video.png new file mode 100755 index 000000000..b0777a023 Binary files /dev/null and b/core/src/main/res/drawable-xhdpi/ic_action_pause_over_video.png differ diff --git a/core/src/main/res/drawable-xhdpi/ic_action_play_over_video.png b/core/src/main/res/drawable-xhdpi/ic_action_play_over_video.png new file mode 100755 index 000000000..24331a48c Binary files /dev/null and b/core/src/main/res/drawable-xhdpi/ic_action_play_over_video.png differ diff --git a/core/src/main/res/drawable-xhdpi/ic_drag_handle.png b/core/src/main/res/drawable-xhdpi/ic_drag_handle.png new file mode 100755 index 000000000..5bdcac342 Binary files /dev/null and b/core/src/main/res/drawable-xhdpi/ic_drag_handle.png differ diff --git a/core/src/main/res/drawable-xhdpi/ic_drag_handle_dark.png b/core/src/main/res/drawable-xhdpi/ic_drag_handle_dark.png new file mode 100755 index 000000000..d341c7c82 Binary files /dev/null and b/core/src/main/res/drawable-xhdpi/ic_drag_handle_dark.png differ diff --git a/core/src/main/res/drawable-xhdpi/ic_drawer.png b/core/src/main/res/drawable-xhdpi/ic_drawer.png new file mode 100644 index 000000000..a5fa74def Binary files /dev/null and b/core/src/main/res/drawable-xhdpi/ic_drawer.png differ diff --git a/core/src/main/res/drawable-xhdpi/ic_drawer_dark.png b/core/src/main/res/drawable-xhdpi/ic_drawer_dark.png new file mode 100644 index 000000000..bcf49dd73 Binary files /dev/null and b/core/src/main/res/drawable-xhdpi/ic_drawer_dark.png differ diff --git a/core/src/main/res/drawable-xhdpi/ic_launcher.png b/core/src/main/res/drawable-xhdpi/ic_launcher.png new file mode 100644 index 000000000..857a1b12e Binary files /dev/null and b/core/src/main/res/drawable-xhdpi/ic_launcher.png differ diff --git a/core/src/main/res/drawable-xhdpi/ic_new.png b/core/src/main/res/drawable-xhdpi/ic_new.png new file mode 100755 index 000000000..447a9398b Binary files /dev/null and b/core/src/main/res/drawable-xhdpi/ic_new.png differ diff --git a/core/src/main/res/drawable-xhdpi/ic_new_dark.png b/core/src/main/res/drawable-xhdpi/ic_new_dark.png new file mode 100755 index 000000000..4a23d309c Binary files /dev/null and b/core/src/main/res/drawable-xhdpi/ic_new_dark.png differ diff --git a/core/src/main/res/drawable-xhdpi/ic_stat_antenna.png b/core/src/main/res/drawable-xhdpi/ic_stat_antenna.png new file mode 100644 index 000000000..50d73271d Binary files /dev/null and b/core/src/main/res/drawable-xhdpi/ic_stat_antenna.png differ diff --git a/core/src/main/res/drawable-xhdpi/ic_stat_authentication.png b/core/src/main/res/drawable-xhdpi/ic_stat_authentication.png new file mode 100755 index 000000000..4adfb636c Binary files /dev/null and b/core/src/main/res/drawable-xhdpi/ic_stat_authentication.png differ diff --git a/core/src/main/res/drawable-xhdpi/ic_undobar_undo.png b/core/src/main/res/drawable-xhdpi/ic_undobar_undo.png new file mode 100644 index 000000000..91c8429ad Binary files /dev/null and b/core/src/main/res/drawable-xhdpi/ic_undobar_undo.png differ diff --git a/core/src/main/res/drawable-xhdpi/location_web_site.png b/core/src/main/res/drawable-xhdpi/location_web_site.png new file mode 100644 index 000000000..bd6b8682a Binary files /dev/null and b/core/src/main/res/drawable-xhdpi/location_web_site.png differ diff --git a/core/src/main/res/drawable-xhdpi/location_web_site_dark.png b/core/src/main/res/drawable-xhdpi/location_web_site_dark.png new file mode 100755 index 000000000..9b77be967 Binary files /dev/null and b/core/src/main/res/drawable-xhdpi/location_web_site_dark.png differ diff --git a/core/src/main/res/drawable-xhdpi/navigation_accept.png b/core/src/main/res/drawable-xhdpi/navigation_accept.png new file mode 100644 index 000000000..b8915716e Binary files /dev/null and b/core/src/main/res/drawable-xhdpi/navigation_accept.png differ diff --git a/core/src/main/res/drawable-xhdpi/navigation_accept_dark.png b/core/src/main/res/drawable-xhdpi/navigation_accept_dark.png new file mode 100755 index 000000000..b52dc3701 Binary files /dev/null and b/core/src/main/res/drawable-xhdpi/navigation_accept_dark.png differ diff --git a/core/src/main/res/drawable-xhdpi/navigation_cancel.png b/core/src/main/res/drawable-xhdpi/navigation_cancel.png new file mode 100644 index 000000000..ca7d159fd Binary files /dev/null and b/core/src/main/res/drawable-xhdpi/navigation_cancel.png differ diff --git a/core/src/main/res/drawable-xhdpi/navigation_cancel_dark.png b/core/src/main/res/drawable-xhdpi/navigation_cancel_dark.png new file mode 100755 index 000000000..f391760ef Binary files /dev/null and b/core/src/main/res/drawable-xhdpi/navigation_cancel_dark.png differ diff --git a/core/src/main/res/drawable-xhdpi/navigation_chapters.png b/core/src/main/res/drawable-xhdpi/navigation_chapters.png new file mode 100755 index 000000000..d527454c6 Binary files /dev/null and b/core/src/main/res/drawable-xhdpi/navigation_chapters.png differ diff --git a/core/src/main/res/drawable-xhdpi/navigation_chapters_dark.png b/core/src/main/res/drawable-xhdpi/navigation_chapters_dark.png new file mode 100755 index 000000000..e53d5eb16 Binary files /dev/null and b/core/src/main/res/drawable-xhdpi/navigation_chapters_dark.png differ diff --git a/core/src/main/res/drawable-xhdpi/navigation_collapse.png b/core/src/main/res/drawable-xhdpi/navigation_collapse.png new file mode 100755 index 000000000..be6a7688c Binary files /dev/null and b/core/src/main/res/drawable-xhdpi/navigation_collapse.png differ diff --git a/core/src/main/res/drawable-xhdpi/navigation_collapse_dark.png b/core/src/main/res/drawable-xhdpi/navigation_collapse_dark.png new file mode 100755 index 000000000..2ed325108 Binary files /dev/null and b/core/src/main/res/drawable-xhdpi/navigation_collapse_dark.png differ diff --git a/core/src/main/res/drawable-xhdpi/navigation_expand.png b/core/src/main/res/drawable-xhdpi/navigation_expand.png new file mode 100644 index 000000000..53c013b09 Binary files /dev/null and b/core/src/main/res/drawable-xhdpi/navigation_expand.png differ diff --git a/core/src/main/res/drawable-xhdpi/navigation_expand_dark.png b/core/src/main/res/drawable-xhdpi/navigation_expand_dark.png new file mode 100755 index 000000000..38c7b20d7 Binary files /dev/null and b/core/src/main/res/drawable-xhdpi/navigation_expand_dark.png differ diff --git a/core/src/main/res/drawable-xhdpi/navigation_refresh.png b/core/src/main/res/drawable-xhdpi/navigation_refresh.png new file mode 100644 index 000000000..e6212cf67 Binary files /dev/null and b/core/src/main/res/drawable-xhdpi/navigation_refresh.png differ diff --git a/core/src/main/res/drawable-xhdpi/navigation_refresh_dark.png b/core/src/main/res/drawable-xhdpi/navigation_refresh_dark.png new file mode 100755 index 000000000..a7fdc0dfc Binary files /dev/null and b/core/src/main/res/drawable-xhdpi/navigation_refresh_dark.png differ diff --git a/core/src/main/res/drawable-xhdpi/navigation_shownotes.png b/core/src/main/res/drawable-xhdpi/navigation_shownotes.png new file mode 100755 index 000000000..a0a156a94 Binary files /dev/null and b/core/src/main/res/drawable-xhdpi/navigation_shownotes.png differ diff --git a/core/src/main/res/drawable-xhdpi/navigation_shownotes_dark.png b/core/src/main/res/drawable-xhdpi/navigation_shownotes_dark.png new file mode 100755 index 000000000..95708234a Binary files /dev/null and b/core/src/main/res/drawable-xhdpi/navigation_shownotes_dark.png differ diff --git a/core/src/main/res/drawable-xhdpi/navigation_up.png b/core/src/main/res/drawable-xhdpi/navigation_up.png new file mode 100755 index 000000000..f8c3e6f75 Binary files /dev/null and b/core/src/main/res/drawable-xhdpi/navigation_up.png differ diff --git a/core/src/main/res/drawable-xhdpi/navigation_up_dark.png b/core/src/main/res/drawable-xhdpi/navigation_up_dark.png new file mode 100755 index 000000000..6964e069b Binary files /dev/null and b/core/src/main/res/drawable-xhdpi/navigation_up_dark.png differ diff --git a/core/src/main/res/drawable-xhdpi/social_share.png b/core/src/main/res/drawable-xhdpi/social_share.png new file mode 100644 index 000000000..cdafd8abc Binary files /dev/null and b/core/src/main/res/drawable-xhdpi/social_share.png differ diff --git a/core/src/main/res/drawable-xhdpi/social_share_dark.png b/core/src/main/res/drawable-xhdpi/social_share_dark.png new file mode 100755 index 000000000..15549b04e Binary files /dev/null and b/core/src/main/res/drawable-xhdpi/social_share_dark.png differ diff --git a/core/src/main/res/drawable-xhdpi/spinner_button.9.png b/core/src/main/res/drawable-xhdpi/spinner_button.9.png new file mode 100644 index 000000000..3dc481e54 Binary files /dev/null and b/core/src/main/res/drawable-xhdpi/spinner_button.9.png differ diff --git a/core/src/main/res/drawable-xhdpi/spinner_button_dark.9.png b/core/src/main/res/drawable-xhdpi/spinner_button_dark.9.png new file mode 100644 index 000000000..c43293d5c Binary files /dev/null and b/core/src/main/res/drawable-xhdpi/spinner_button_dark.9.png differ diff --git a/core/src/main/res/drawable-xhdpi/stat_playlist.png b/core/src/main/res/drawable-xhdpi/stat_playlist.png new file mode 100644 index 000000000..7977e6f2a Binary files /dev/null and b/core/src/main/res/drawable-xhdpi/stat_playlist.png differ diff --git a/core/src/main/res/drawable-xhdpi/stat_playlist_dark.png b/core/src/main/res/drawable-xhdpi/stat_playlist_dark.png new file mode 100644 index 000000000..f32dd3780 Binary files /dev/null and b/core/src/main/res/drawable-xhdpi/stat_playlist_dark.png differ diff --git a/core/src/main/res/drawable-xhdpi/type_audio.png b/core/src/main/res/drawable-xhdpi/type_audio.png new file mode 100644 index 000000000..777fab84e Binary files /dev/null and b/core/src/main/res/drawable-xhdpi/type_audio.png differ diff --git a/core/src/main/res/drawable-xhdpi/type_audio_dark.png b/core/src/main/res/drawable-xhdpi/type_audio_dark.png new file mode 100755 index 000000000..dfd2b33c7 Binary files /dev/null and b/core/src/main/res/drawable-xhdpi/type_audio_dark.png differ diff --git a/core/src/main/res/drawable-xhdpi/type_video.png b/core/src/main/res/drawable-xhdpi/type_video.png new file mode 100644 index 000000000..bbd1f112f Binary files /dev/null and b/core/src/main/res/drawable-xhdpi/type_video.png differ diff --git a/core/src/main/res/drawable-xhdpi/type_video_dark.png b/core/src/main/res/drawable-xhdpi/type_video_dark.png new file mode 100755 index 000000000..a74947459 Binary files /dev/null and b/core/src/main/res/drawable-xhdpi/type_video_dark.png differ diff --git a/core/src/main/res/drawable-xhdpi/undobar.9.png b/core/src/main/res/drawable-xhdpi/undobar.9.png new file mode 100644 index 000000000..22fa2205b Binary files /dev/null and b/core/src/main/res/drawable-xhdpi/undobar.9.png differ diff --git a/core/src/main/res/drawable-xhdpi/undobar_button_focused.9.png b/core/src/main/res/drawable-xhdpi/undobar_button_focused.9.png new file mode 100644 index 000000000..d284ca7cb Binary files /dev/null and b/core/src/main/res/drawable-xhdpi/undobar_button_focused.9.png differ diff --git a/core/src/main/res/drawable-xhdpi/undobar_button_pressed.9.png b/core/src/main/res/drawable-xhdpi/undobar_button_pressed.9.png new file mode 100644 index 000000000..e990659f0 Binary files /dev/null and b/core/src/main/res/drawable-xhdpi/undobar_button_pressed.9.png differ diff --git a/core/src/main/res/drawable-xhdpi/undobar_divider.9.png b/core/src/main/res/drawable-xhdpi/undobar_divider.9.png new file mode 100644 index 000000000..1b067d4e7 Binary files /dev/null and b/core/src/main/res/drawable-xhdpi/undobar_divider.9.png differ diff --git a/core/src/main/res/drawable-xxhdpi/ic_action_overflow.png b/core/src/main/res/drawable-xxhdpi/ic_action_overflow.png new file mode 100644 index 000000000..5a603b6bc Binary files /dev/null and b/core/src/main/res/drawable-xxhdpi/ic_action_overflow.png differ diff --git a/core/src/main/res/drawable-xxhdpi/ic_action_overflow_dark.png b/core/src/main/res/drawable-xxhdpi/ic_action_overflow_dark.png new file mode 100644 index 000000000..e22049b1e Binary files /dev/null and b/core/src/main/res/drawable-xxhdpi/ic_action_overflow_dark.png differ diff --git a/core/src/main/res/drawable-xxhdpi/ic_action_pause_over_video.png b/core/src/main/res/drawable-xxhdpi/ic_action_pause_over_video.png new file mode 100755 index 000000000..fa85601cf Binary files /dev/null and b/core/src/main/res/drawable-xxhdpi/ic_action_pause_over_video.png differ diff --git a/core/src/main/res/drawable-xxhdpi/ic_action_play_over_video.png b/core/src/main/res/drawable-xxhdpi/ic_action_play_over_video.png new file mode 100755 index 000000000..121be211e Binary files /dev/null and b/core/src/main/res/drawable-xxhdpi/ic_action_play_over_video.png differ diff --git a/core/src/main/res/drawable-xxhdpi/ic_drag_handle.png b/core/src/main/res/drawable-xxhdpi/ic_drag_handle.png new file mode 100755 index 000000000..f834699c6 Binary files /dev/null and b/core/src/main/res/drawable-xxhdpi/ic_drag_handle.png differ diff --git a/core/src/main/res/drawable-xxhdpi/ic_drag_handle_dark.png b/core/src/main/res/drawable-xxhdpi/ic_drag_handle_dark.png new file mode 100755 index 000000000..a9408bc9d Binary files /dev/null and b/core/src/main/res/drawable-xxhdpi/ic_drag_handle_dark.png differ diff --git a/core/src/main/res/drawable-xxhdpi/ic_drawer.png b/core/src/main/res/drawable-xxhdpi/ic_drawer.png new file mode 100644 index 000000000..9c4685d6e Binary files /dev/null and b/core/src/main/res/drawable-xxhdpi/ic_drawer.png differ diff --git a/core/src/main/res/drawable-xxhdpi/ic_drawer_dark.png b/core/src/main/res/drawable-xxhdpi/ic_drawer_dark.png new file mode 100644 index 000000000..f7e3b3079 Binary files /dev/null and b/core/src/main/res/drawable-xxhdpi/ic_drawer_dark.png differ diff --git a/core/src/main/res/drawable-xxhdpi/ic_launcher.png b/core/src/main/res/drawable-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..2bef52ec7 Binary files /dev/null and b/core/src/main/res/drawable-xxhdpi/ic_launcher.png differ diff --git a/core/src/main/res/drawable-xxhdpi/ic_new.png b/core/src/main/res/drawable-xxhdpi/ic_new.png new file mode 100755 index 000000000..5e836eae4 Binary files /dev/null and b/core/src/main/res/drawable-xxhdpi/ic_new.png differ diff --git a/core/src/main/res/drawable-xxhdpi/ic_new_dark.png b/core/src/main/res/drawable-xxhdpi/ic_new_dark.png new file mode 100755 index 000000000..bca96b751 Binary files /dev/null and b/core/src/main/res/drawable-xxhdpi/ic_new_dark.png differ diff --git a/core/src/main/res/drawable-xxhdpi/ic_stat_authentication.png b/core/src/main/res/drawable-xxhdpi/ic_stat_authentication.png new file mode 100755 index 000000000..b274bb60f Binary files /dev/null and b/core/src/main/res/drawable-xxhdpi/ic_stat_authentication.png differ diff --git a/core/src/main/res/drawable/badge.xml b/core/src/main/res/drawable/badge.xml new file mode 100644 index 000000000..f98384cb9 --- /dev/null +++ b/core/src/main/res/drawable/badge.xml @@ -0,0 +1,13 @@ + + + + + + + + \ No newline at end of file diff --git a/core/src/main/res/drawable/borderless_button.xml b/core/src/main/res/drawable/borderless_button.xml new file mode 100644 index 000000000..27d723eed --- /dev/null +++ b/core/src/main/res/drawable/borderless_button.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/core/src/main/res/drawable/borderless_button_dark.xml b/core/src/main/res/drawable/borderless_button_dark.xml new file mode 100644 index 000000000..6d263938d --- /dev/null +++ b/core/src/main/res/drawable/borderless_button_dark.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/core/src/main/res/drawable/horizontal_divider.9.png b/core/src/main/res/drawable/horizontal_divider.9.png new file mode 100644 index 000000000..7db0549da Binary files /dev/null and b/core/src/main/res/drawable/horizontal_divider.9.png differ diff --git a/core/src/main/res/drawable/overlay_button_circle_background.xml b/core/src/main/res/drawable/overlay_button_circle_background.xml new file mode 100644 index 000000000..90c51472c --- /dev/null +++ b/core/src/main/res/drawable/overlay_button_circle_background.xml @@ -0,0 +1,10 @@ + + + + + \ No newline at end of file diff --git a/core/src/main/res/drawable/overlay_drawable.xml b/core/src/main/res/drawable/overlay_drawable.xml new file mode 100644 index 000000000..185ffefc1 --- /dev/null +++ b/core/src/main/res/drawable/overlay_drawable.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/core/src/main/res/drawable/overlay_drawable_dark.xml b/core/src/main/res/drawable/overlay_drawable_dark.xml new file mode 100644 index 000000000..fb78f5633 --- /dev/null +++ b/core/src/main/res/drawable/overlay_drawable_dark.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/core/src/main/res/drawable/type_audio.png b/core/src/main/res/drawable/type_audio.png new file mode 100644 index 000000000..4ec9efd97 Binary files /dev/null and b/core/src/main/res/drawable/type_audio.png differ diff --git a/core/src/main/res/drawable/type_video.png b/core/src/main/res/drawable/type_video.png new file mode 100644 index 000000000..a2722b812 Binary files /dev/null and b/core/src/main/res/drawable/type_video.png differ diff --git a/core/src/main/res/drawable/undobar_button.xml b/core/src/main/res/drawable/undobar_button.xml new file mode 100644 index 000000000..a4de91b49 --- /dev/null +++ b/core/src/main/res/drawable/undobar_button.xml @@ -0,0 +1,22 @@ + + + + + + diff --git a/core/src/main/res/drawable/vertical_divider.9.png b/core/src/main/res/drawable/vertical_divider.9.png new file mode 100644 index 000000000..6a0edafb3 Binary files /dev/null and b/core/src/main/res/drawable/vertical_divider.9.png differ diff --git a/core/src/main/res/drawable/white_circle.xml b/core/src/main/res/drawable/white_circle.xml new file mode 100644 index 000000000..597b70a2d --- /dev/null +++ b/core/src/main/res/drawable/white_circle.xml @@ -0,0 +1,11 @@ + + + + + + + + \ No newline at end of file diff --git a/core/src/main/res/values-az/strings.xml b/core/src/main/res/values-az/strings.xml new file mode 100644 index 000000000..adb983e9e --- /dev/null +++ b/core/src/main/res/values-az/strings.xml @@ -0,0 +1,217 @@ + + + + AntennaPod + Kanallar + PODKASTLAR + EPIZODLAR + Yeni + Gözləmədə + Parametrlər + Yükləmələr + Yükləməyi ləğv et + Oynatma tarixiçəsi + gpodder.net + + + + Brauzerdə aç + URLı kopiyala + URLı paylaş + URL buferə köçürüldü + + Tarixiçəni sildir + + Oldu + Ləğv et + Müəlif + Dil + Parametrlər + Xəta + Xəta baş verdi: + Təzələ + Heç bir yaddaş cihazı tapılmadı. + Fəsillər + Təsvir + Ən yeni epizod:\u0020 + \u0020epizod + Müddət:\u0020 + Ölçü:\u0020 + Hazırlaşma + Yükləmə... + Bağla + + Kanalın URLı + + Hamısını oxunmuş kimi işarələ + Məlumatı göstər + Web-səhifəyi paylaş + Kanalı paylaş + Bütün kanallar və epizodlar silinəçək. + + Yüklə + Oynat + Pauza + İnternetən yayimla + Sil + Oxumuş kimi işarələ + Oxunmamış kimi işarələ + Növbəyə əlavə et + Növbədən sil + Web-səhifəsini aç + Flattrla + Hamsını növbəyə əlavə et + Hamısını yüklə + Epizodu burax + + Yükləmə gözlənir + Yükləmə gedir + Yaddaş cihazı tapılmadı + Yaddaş çatmır + Fayl xətası + HTTP protokolnun xətası + Naməlum xəta + Parserin xətası + Naməlum kanal növü + Əlaqə xətasi + Naməlum xost + Yükləmələrin hamısını ləğv et + Yükləmə ləğv olundu + Yükləmə başa çatdı + Yanlış URL + IO xətasi + Tələbin xətası + \u0020yükləmə galdı + Podkast məlumatların yüklənişi + %1$d yükləmə uğurludur, %2$d uğursuzdur + Naməlum başliğ + Kanal + Mediya fayl + Şəkil + Fayl yükləmə xətası:\u0020 + + Xəta! + Heç nə oynadılmır + Hazırlanır + Hazır + Axtarış + Server iştəmir + Naməlum xəta + Heç nə oynadılmır + 00:00:00 + Buferləşmə + Podkast oynadılır + + Növbəyi sil + Qaytar + Element silindi + + Flattra gir + Girmə prosesini başlamaq üçün düyməyi basın. Flattrın giriş səhifəsinə aparılacağsınız. + Gir + Baş ekrana dön + Giriş uğurludur! İndi tətbiqlədən Flattrla istifadə edə bilərsiniz. + Heç bir Flattr tokeni tapılmadı + Olsun ki sizin Flattr hesabınız AntennaPod\'a qoşulmadı. Yenə Flattra girin ya da podkastın səhifəsinə keçin. + Gir + Əməliyyat qadağan olundu + Bu əməliyyat üçün AntennaPod\'un icazəsi yoxdur. AntennaPod\'un keçid tokenin ləğv olunması bunun səbəbi ola bilər. Yenə Flattra girin ya da podkastın səhifəsinə keçin. + Keçid ləğv olundu + AntennaPod\'un keçid tokeni uğurlu ləğv olundu. + + + Plagin yüklə + Plagin yüklü deyil + + Siyahıda heç nə yoxdur + Hələ heç bir kanala yazilmadınız + + Başqa + Proqram haqqinda + Növbə + Flattr + Qulaqliqı ayiranda oynatma dayanacağ + Oynatma başa çatanda növbədə irəlidəki epizodu oynat + Oynatma + Şəbəkə + Təzələmə intervalı + Kanalın avtomatik təzələməsinin intervalını seç ya da keçir onu + Təkçə Wi-Fi vasitəsiilə yüklə + Fasiləsiz oynatma + Wi-Fi vasitəsiilə yükləmə + Qulaqliqı ayır + Mobil şəbəbkə vasitəsiilə təzələmə + Mobil şəbəbkə vasitəsiilə təzələməyə icazə vermək + Təzələmə + Flattr parametrləri + Flattra gir + Flattr\'la istifadə etmək üçün, öz Flattr hesabınıza girin + Bu proqramı flattrla + Flattr vasitəsiilə AntennaPodun inkişafını dəstək edin. Sağolun! + Keçidi ləğv ət + Flattr hesabına keçidi ləğv et + İnterfeys + Görüşü seç + AntennaPod\'un görüşünü dəyişdir + Avtomatik yükləmə + Epizodların avtomatik yüklənişinin konfiqurasiyanı dəyiş + Wi-Fi filtr + Seçilən Wi-Fi səbəkələr vasitəsiilə avtomatik yükləməyi icazə ver + Epizod keşi + + Qara + Hədsiz + saat + saat + Əl ilə + + + Kanalları və ya epizodları axtar + Təsvirlərdə tapıldı + Fəsillərdə tapıldı + Heç nə tapılmadı + Axtar + Başlığda tapıldı + + OPML faylın idxalı üçün onu aşağıdakı qovluqa yerləşdirin və idxal prosesini başlamaq üçün düyməyi basın. + İdxalı başla + OPML idxalı + XƏTA! + OPML faylın oxunması + OPML faylını oxuyanda xəta baş verdi: + İdxal qovliqu boşdur. + Hamısını seç + Seçimi ləğv et + İdxal üçün fayl seç + OPML ixraçı + İxrac... + İxracın xətası + OPML ixracı uğurlu keçdi + OPML fayl:\u0020 yazılıb + + Yuxu taymerini qoy + Yuxu taymerini keçir + Vaxtı yaz + Yuxu taymeri + Vaxt galdı:\u0020 + Yanlış yazi. Vaxt təkçə rəqmlərlə yazılır + + TOP PODKASTLAR + + Seçilən qovluq: + Qovluqu yarat + Məlumat qovluqunu seç + \"%1$s\" adlı qovluq yaradılsınmı? + Yeni qovluq yaradıldı + Bu qovluqa yazıla bilinmer + Qovluq artiq var + Qovluq yaradılmadı + Qovluq boş deyil + Seçilən qovluq boş deyil. Mediya yükləmələr və başka fayllar bu qovluqa yazılacaqlar. Necə olsa davam olsunmu? + Başlanğıc qovluqu seç + + Yükləmə... + + + + diff --git a/core/src/main/res/values-ca/strings.xml b/core/src/main/res/values-ca/strings.xml new file mode 100644 index 000000000..ae2addb05 --- /dev/null +++ b/core/src/main/res/values-ca/strings.xml @@ -0,0 +1,341 @@ + + + + AntennaPod + Canals + Afegeix podcast + PODCASTS + EPISODIS + Episodis nous + Tots els episodis + Nous + Llista d\'espera + Configuració + Afegeix podcast + Baixades + En execució + Completat + Registre + Cancel·la la baixada + Historial de reproducció + gpodder.net + Inici de sessió a gpodder.net + + Publicats recentment + Mostra només els episodis nous + + Obre menú + Tanca menú + + Obre en un navegador + Copia l\'enllaç + Comparteix l\'enllaç + S\'ha copiat l\'enllaç al porta-retalls. + Vés a aquesta posició + + Esborra l\'historial + + D\'acord + Cancel·la + Autor + Llengua + Configuració + Imatge + Error + S\'ha produït un error: + Actualitza + L\'emmagatzemament extern no està disponible. Assegureu-vos que està muntat per què l\'aplicació funcioni correctament. + Capítols + Notes del programa + Descripció + Episodi més recent: \u0020 + \u0020episodis + Durada:\u0020 + Mida:\u0020 + S\'està processant + S\'està carregant... + Desa nom d\'usuari i contrasenya + Tanca + Reintenta + Inclou a baixades automàtiques + + Enllaç del canal + URL, canal o lloc web + Afegeix podcast amb l\'URL + Cerca podcast al directori + Podeu cercar nous podcasts al directori de gpodder.net mitjançant el seu nom, categoria o popularitat. + Navega gpodder.net + + Marca-ho tot com a llegit + S\'han marcat tots els episodis com a llegits + Mostra informació + Esborra podcast + Comparteix l\'enllaç de la plana + Comparteix l\'enllaç del canal + Confirmeu que, efectivament, voleu suprimir aquest canal i tots els episodis que us n\'heu baixat. + S\'està esborrant el canal + + Baixa + Reprodueix + Pausa + Reprodueix sense baixar + Suprimeix + Esborra episodi + Marca com a llegit + Marca com a pendent + Afegeix a la cua + Suprimeix de la cua + Visita el lloc web + Comparteix amb Flattr + Posa-ho tot a la cua + Baixa-ho tot + Omet l\'episodi + + ha funcionat + ha fallat + Baixada pendent + Baixada en procés + No s\'ha trobat cap dispositiu d\'emmagatzemament + No hi ha prou espai + Error de fitxer + Error de dades HTTP + Error desconegut + Error de l\'analitzador + Tipus de canal no suportat + Error de connexió + Amfitrió desconegut + Error d\'autenticació + Cancel·la totes les baixades + S\'ha cancel·lat la baixada + Baixades completades + URL mal formatada + Error d\'E/S + Error de petició + Error d\'accés a la base de dades + \u0020Baixades pendents + S\'estan processant les baixades + S\'estan baixant les dades del podcast + %1$d baixades finalitzades, %2$d fallides + Títol desconegut + Canal + Fitxer + Imatge + S\'ha produït un error en intentar baixar el fitxer:\u0020 + Cal autenticar-se + Es necessita un usuari i una contrasenya per accedir al recurs + + Error + No s\'està reproduint res + S\'està preparant + Preparat + S\'està cercant + El servidor no està operatiu + Error desconegut + No s\'està reproduint res + 00:00:00 + S\'està carregant + Podcast en reproducció + AntennaPod - Control desconegut: %1$d + + Buida la cua + Desfés + Ítem esborrat + Mou al principi + Mou al final + + Inici de sessió a Flattr + Premeu el botó per iniciar el procés d\'autenticació. Quan s\'obri la pantalla d\'inici de sessió de Flattr al vostre navegador, introduïu les vostres credencials i concediu a AntennaPod els permisos de compartir mitjançant Flattr. En finalitzar el procés, tornareu automàticament a aquesta pantalla. + Autenticació + Torna a l\'inici + L\'autenticació ha acabat correctament. Ja podeu compartir amb Flattr des de l\'aplicació. + No s\'ha trobat cap testimoni Flattr + Sembla que el compte flattr no està vinculat amb AntennaPod. Toqueu aquí per autenticar-vos. + Sembla que el vostre compte de Flattr no està vinculat amb AntennaPod. Podeu connectar el vostre compte Flattr amb AntennaPod per a compartir continguts des de l\'aplicació, o bé accediu a la plana web de Flattr i compartiu els continguts des d\'allà. + Autentica + L\'acció no és permesa + AntennaPod no té permisos per executar aquesta acció. És possible que el testimoni d\'accés de Flattr per a AntennaPod hagi estat revocat. Podeu tornar-vos a autenticar amb el servei de Flattr, o podeu visitar el web del contingut directament. + L\'accés ha estat revocat + El testimoni d\'accés a Flattr de l\'AntennaPod s\'ha revocat correctament. Per completar el procés, heu de suprimir aquesta aplicació de la llista d\'aplicacions aprovades que trobareu a l\'apartat de configuració del compte de la plana web de Flattr. + + S\'ha compartit una cosa per Flattr! + S\'han compartit %d coses per Flattr! + Compartit per Flattr: %s. + No s\'han pogut compartir %d coses per Flattr! + No s\'ha compartit per Flattr: %s. + Es compartirà per Flattr després + %s s\'està compartint per Flattr + AntennaPod està compartint per Flattr + AntennaPod ha compartit per Flattr + AntennaPod no ha pogut compartir per Flattr + S\'estan recuperant les coses compartides per Flattr + + Baixa el connector + Connector no instal·lat + Per a què funcioni la velocitat de reproducció variable, cal instal·lar una biblioteca addicional.\n\nFeu un toc a «Baixa el connector» per baixar-vos el connector gratuït des de la Play Store.\n\nQualsevol problema que sorgeixi en utilitzar aquest connector no és culpa de l\'AntennaPod. Cal informar-ne, doncs, al propietari del connector. + Velocitats de reproducció + + No hi ha elements a la llista. + No us heu subscrit a cap canal. + + Altres + Quant a + Cua + Serveis + Flattr + Pausa la reproducció en desconnectar els auriculars. + Salta al següent element de la cua en acabar la reproducció + Reproducció + Xarxa + Interval d\'actualització + Especifiqueu l\'interval en què els canals s\'actualitzen de forma automàtica, o deshabiliteu la funcionalitat. + Només baixa fitxers a través d\'una xarxa sense fils + Reproducció continuada + Baixa a través de xarxes sense fils + Desconnexió d\'auriculars + Actualitzacions sobre xarxes mòbils + Permet actualitzacions a través de xarxes mòbils. + S\'està actualitzant + Configuració de Flattr + Inici de sessió Flattr + Inicieu sessió al vostre compte Flattr per compartir continguts directament des de l\'aplicació. + Compartiu aquesta aplicació amb Flattr + Doneu suport al desenvolupament d\'AntennaPod compartint l\'aplicació a través de Flattr. Gràcies! + Revoca l\'accés + Revoqueu el permís d\'accés d\'aquesta aplicació al vostre compte Flattr. + Flattr automàtic + Configura la compartició automàtica per Flattr + Interfície d\'usuari + Selecció de tema + Canvieu l\'aparença d\'AntennaPod. + Baixada automàtica + Configureu la baixada automàtica d\'episodis. + Activa el filtre de la xarxa sense fils + Permet les baixades automàtiques només per a les xarxes sense fils seleccionades. + Memòria d\'episodis + Clar + Fosc + Sense límits + hores + hora + Manual + Inici de sessió + Inicieu sessió a gpodder.net per tal de sincronitzar les vostres subscripcions. + Surt + Heu sortit de la sessió + Dades d\'inici de sessió + Canvia les dades d\'inici de sessió del vostre compte de gpodder.net + Velocitats de reproducció + Personalitzeu les velocitats disponibles per a una velocitat de reproducció d\'àudio variable + Salta a l\'instant + Salta aquesta quantitat de segons en rebobinar o en avançar ràpidament + Definex nom del servidor + Utilitza el servidor per defecte + + Activa la compartició automàtica per Flattr + Comparteix per Flattr l\'episodi en haver-ne reproduït el %d per cent + Comparteix per Flattr l\'episodi en haver-ne iniciat la reproducció + Comparteix per Flattr l\'episodi en acabar-se\'n la reproducció + + Cerca canals o episodis + Trobat a notes del programa + Trobat als capítols + No s\'ha trobat cap resultat + Cerca + Trobat al títol + + Els fitxers OPML us permeten moure els podcasts d\'un gestor de podcasts a un altre. + Per importar un fitxer OPML, ubiqueu-lo al següent directori i premeu el botó de sota per iniciar el procés. + Inicia la importació + Importació OPML + Error! + S\'està llegint el fitxer OPML + S\'ha produït un error en llegir el document OPML: + El directori d\'importacions és buit. + Selecciona-ho tot + Deselecciona-ho tot + Seleccioneu el fitxer a importar + Exportació OPML + S\'està exportant... + Error d\'exportació + S\'ha exportat l\'OPML correctament. + El fitxer OPML s\'ha escrit a:\u0020 + + Defineix un temporitzador + Desactiva el temporitzador + Introduïu l\'hora + Temporitzador + Temps restant:\u0020 + L\'entrada no és vàlida, ja que el temps ha de ser un nombre i no ho és + segons + minuts + hores + + CATEGORIES + TOP PODCASTS + SUGGERÈNCIES + Cerca a gpodder.net + Inici de sessió + Benvingut al procés d\'inici de sessió a gpodder.net. Primerament, introduïu la informació d\'accés: + Entra + Si encara no teniu un compte, creeu-ne un aquí:\nhttps://gpodder.net/register/ + Nom d\'usuari + Contrasenya + Selecció de dispositiu + Per a utilitzar gpodder.net, creeu un nou dispositiu o seleccioneu-ne un d\'existent: + ID de dispositiu:\u0020 + Llegenda + Crea nou dispositiu + Seleccioneu un dispositiu existent: + L\'ID de dispositiu no pot ser buit + L\'ID de dispositiu ja existeix + Selecciona + Heu iniciat la sessió! + Felicitats! El vostre compte de gpodder.net s\'ha enllaçat amb el dispositiu. D\'ara endavant, AntennaPod sincronitzarà automàticament les subscripcions del dispositiu al vostre compte. + Sincronitza ara + Vés a la pantalla principal + Error d\'autenticació a gpodder.net + Nom d\'usuari o contrasenya incorrectes + Error de sincronització a gpodder.net + S\'ha produït un error durant la sincronització:\u0020 + + Carpeta seleccionada: + Crea una carpeta + Selecció de la carpeta de dades + Voleu crear una nova carpeta amb el nom \"%1$s\"? + S\'ha creat la nova carpeta + No es pot escriure dins d\'aquesta carpeta + La carpeta ja existeix + No s\'ha pogut crear la carpeta + La carpeta no és buida + La carpeta que heu seleccionat no és buida. Les baixades i altres fitxers es copiaran directament a aquesta carpeta. Voleu continuar? + Selecciona la carpeta per defecte + Pausa la reproducció en lloc de baixar el volum quan una altra app necessiti reproduir sons + Pausa en interrompre + + Subscriu + Subscrit + S\'està baixant... + + Mostra els capítols + Mostra les notes del programa + Mosta la imatge + Rebobina + Avança ràpidament + Àudio + Vídeo + Navega cap amunt + Més accions + S\'està reproduïnt l\'episodi + S\'està baixant l\'episodi + S\'ha baixat l\'episodi + L\'element és nou + S\'ha afegit l\'episodi a la cua + Nombre d\'episodis nous + Nombre d\'episodis que heu començat a escoltar + Arrossegueu l\'element per canviar-ne la posició + + Autenticació + Canvieu el nom d\'usuari i contrasenya per a aquest podcast i els seus episodis. + + S\'estan important les subscripcions des de les apps de propòsit únic... + diff --git a/core/src/main/res/values-cs-rCZ/strings.xml b/core/src/main/res/values-cs-rCZ/strings.xml new file mode 100644 index 000000000..8792d1fc9 --- /dev/null +++ b/core/src/main/res/values-cs-rCZ/strings.xml @@ -0,0 +1,272 @@ + + + + AntennaPod + Zdroje + PODCASTY + EPIZODY + Nový + Seznam nepřečtených + Nastavení + Přidat podcast + Stahování + Zrušit stahování + Historie přehrávání + gpodder.net + gpodder.net uživatel + + + + Otevřít v prohlížeči + Kopírovat URL + Sdílet URL + URL zkopírováno do schránky. + + Vymazat historii + + Potvrdit + Zrušit + Autor + Jazyk + Nastavení + Chyba + Nastala chyba: + Obnovit + Není dostupné žádné externí uložiště. Pro správnou funkci aplikace se prosím ujistěte, že je připojeno externí úložiště. + Kapitoly + Poznámky + Popis + Poslední epizoda:\u0020 + \u0020epizod + Délka:\u0020 + Velikost:\u0020 + Zpracovávám + Načítám... + Uložit uživatelské jméno a heslo + Zavřít + Zkusit znovu + Zahrnout do automaticky stahovaných + + URL zdroje + Přidat podcast pomocí URL + + Označit vše jako přečtené + Informace o zdroji + Sdílet odkaz + Sdílet adresu zdroje + Prosím potvrďte, že chcete smazat tento zdroj včetně všech stažených epizod. + Odstranit feed + + Stáhnout + Přehrát + Pozastavit + Streamovat + Odstranit + Označit jako přečtené + Označit jako nepřečtené + Přidat do fronty + Odebrat z fronty + Navštívit stránku + Flattr + Vše do fronty + Stáhnout vše + Přeskočit epizodu + + Čekající na stažení + Probíhající stahování + Úložné zařízení nenalezeno + Nedostatek volného místa + Souborová chyba + HTTP chyba + Neznámá chyba + Výjimka parseru + Nepodporovaný typ zdroje + Chyba spojení + Neznámý host + Zrušit všechna stahování + Stahování zrušeno + Všechna stahování dokončena + Chybné URL + IO chyba + Chyba požadavku + Chyba přístupu do databáze + \u0020Stahování zbývá + Stahuji podcast data + %1$d úspěšných stahování, %2$d selhalo + Neznámý název + Zdroj + Soubor + Obrázek + Nastala chyba při pokusu o stažení souboru:\u0020 + + Chyba! + Žádné probíhající přehrávání + Připravuji + Připraven + Přetáčím + Server nereaguje + Neznámá chyba + Žádné probíhající přehrávání + 00:00:00 + Načítání + Přehrávaný podcast + + Vyprázdnit frontu + Zpět + Položka odebrána + Přejít na začátek + Přejít na konec + + Flattr přihlášení + Stiskněte následující tlačítko pro spuštění autentizačního procesu. Budete přesměrováni na přihlašovací obrazovku flattru a vyzváni k potvrzení udělení práv pro použití flattru aplikací AntennaPod. Po udělení práv se automaticky vrátíte na tuto obrazovku. + Přihlásit + Návrat domů + Úspěšně přihlášen. Nyní můžete využít flattru přímo v aplikaci. + Nenalezen Flattr token + Váš flattr učet není napojen do AntenaPodu. Můžete buďto napojit váš flattr účet do AntennaPodu a využít flattru přímo v aplikaci a nebo použít flattr přímo na webových stránkách zdroje v prohlížeči. + Přihlásit + Akce zakázána + AntennaPod nemá oprávnění pro tuto akci. Důvodem může být revokování přístupového tokenu AntennaPodu k vašemu účtu. Přístup můžete obnovit nebo využít prohlížeče k návštěvě stránky zdroje. + Přístup revokován + Úspěšně revokován přístup AntennPodu k vašemu účtu. Pro dokončení tohoto procesu je ještě zapotřebí na stránkách flattru odebrat z vašeho účtu AntennaPod ze seznamu povolených aplikací. + + + Stáhnout Plugin + Plugin není nainstalován + Pro nastavení rychlosti přehrávání musí být nainstalovaná knihovna třetí strany.\n\nKlikněte na \"Stáhnout Plugin\" ke stažení pluginu z Play Store.\n\nAntennaPod nenese žádnou odpovědnost, za jakékoliv problémy, způsobené tímto pluginem. + Rychlosti přehrávání + + Žádné položky v seznamu. + Zatím nebyly přidány žádné zdroje. + + Ostatní + O aplikaci + Fronta + Služby + Flattr + Při odpojení sluchátek automaticky pozastavit přehrávání. + Po přehrání položky z fronty přejít automaticky na další. + Přehrávání + Síť + Interval aktualizace zdrojů + Udává interval, ve kterém se zdroje automaticky aktualizují nebo tuto funkci deaktivuje. + Stahovat soubory pouze pomocí WiFi + Kontinuální přehrávání + WiFi stahování + Odpojení sluchátek + Mobilní aktualizace + Povolit aktualizace pomocí mobilního připojení. + Obnovuji + Nastavení Flattr + Flattr přihlášení + Přihlásit se k flattr účtu a umožnit flattrování přímo z aplikace. + Flattrovat tuto aplikaci + Podpořit vývoj AntennaPodu na flatteru. Děkujeme! + Odebrat přístup + Odebere aplikaci přístupová práva k vašemu flattr účtu. + Uživatelské rozhraní + Vybrat motiv + Změnit vzhled AntennaPod. + Automatické stahování + Nastavení automatického stahování epizod. + Zapnout Wi-Fi filtr + Povolit automatické stahování pouze pomocí vybraných Wi-Fi sítí. + Historie epizod + Světlý + Tmavý + Bez omezení + hodin + hodina + Ručně + Přihlásit + Přihlašte se pomocí vašeho gpodder.net účtu pro synchronizaci odebíraných podcastů. + Odhlásit + Úspěšně odhlášeno + Změna přihlašovacích údajů + Změní přihlašovací údaje k vašemu gpodder.net účtu. + Rychlosti přehrávání + Přizpůsobení rychlosti je dostupné pro přehrávání zvuku různými rychlostmi + Nastavit hostname + Použít přednastaveného hosta + + + Hledat zdroje a epizody + Nalezeno v poznámkách k show + Nalezeno v kapitolách + Žádné výsledky + Vyhledat + Nalezeno v názvu + + OPML soubory umožňují přenést vaše podcasty z jednoho podcast manažera do jiného. + Pro import OPML souboru je třeba ho nejdříve umístit do následujícího adresáře a poté pro zahájení procesu importu stisknout tlačítko. + Importovat + OPML import + CHYBA! + Načítání OPML souboru + Nastala chyba při čtení OPML souboru: + Adresář importu je prázdný. + Označit vše + Zrušit výběr + Vyberte soubor k importování + OPML export + Exportuji... + Chyba exportu + OPML export byl úspěšný. + OPML soubor byl zapsán do:\u0020 + + Nastavit časovač vypnutí + Deaktivovat časovač vypnutí + Zadejte čas + Časovač vypnutí + Zbývá času:\u0020 + Neplatný vstup, musí být zadáno celé číslo + + KATEGORIE + TOP PODCASTY + DOPOTUČENÉ + Vyhledat na gpodder.net + Přihlásit + Vítejte do průvodce přihlášením ke gpodder.net účtu. Zadejte vaše přihlašovací údaje: + Přihlásit + Jestliže nemáte účet, můžete si ho vytvořit zde:\nhttps://gpodder.net/register/ + Uživatleské jméno + Heslo + Výběr zařízení + Vytvořte nové nebo vyberte již existující zařízení pro použití s vašim gpodder.net účtem. + ID zařízení:\u0020 + Nadpis + Vytvořit nové zařízení + Vybrat existující zařízení: + ID zařízení nesmí být prázdné + ID zařízení je již obsazeno + Vybrat + Úspěšně přihlášeno! + Gratulujeme! Váš gpodder.net účet je nyná úspěšně propojen s vašim zařízením. AntennaPod bude automaticky synchronizovat odebírané podcasty s vaším gpodder.net účtem. + Synchronizovat nyní + Přejít na hlavní obrazovku + gpodder.net autentizace selhala + Špatné přihlašovací jméno nebo heslo + gpodder.net synchronizace selhala + V průběhu synchronizace nastala chyba:\u0020 + + Vybraný adresář: + Vytvořit adresář + Vybrat umístění dat + Vytvořit adresář \"%1$s\"? + Nový adresář vytvořen + Nelze zapisovat do adresáře + Adresář již existuje + Nelze vytvořit adresář + Adresář není prázdný + Vybraný adresář není prázdný. Stažená media a ostatní soubory budou umístěny přímo do tohoto adresáře. Přesto pokračovat? + Vybrat hlavní adresář + Místo snížení hlasitosti pozastavit přehrávání v případě, že jiná aplikace přehrává zvuk. + Automatické pozastavení přehrávání + + Odebírat + Odebíráno + Stahuji... + + + + diff --git a/core/src/main/res/values-da/strings.xml b/core/src/main/res/values-da/strings.xml new file mode 100644 index 000000000..5c41b6eb0 --- /dev/null +++ b/core/src/main/res/values-da/strings.xml @@ -0,0 +1,329 @@ + + + + AntennaPod + Feeds + PODCASTS + EPISODER + Nye episoder + Alle episoder + Nye + Venteliste + Indstillinger + Tilføj podcast + Downloads + Kører + Fuldført + Log + Annuller Download + Afspilnings historik + gpodder.net + gpodder.net login + + Nyligt udgivet + Hvis kun nye episoder + + Åben menu + Luk menu + + Åben i browser + Kopier URL + Del URL + URL kopieret til udklipsholderen. + + Fjern historik + + Bekræft + Annuller + Forfatter + Sprog + Indstillinger + Billed + Fejl + En fejl er opstået: + Opdater + Ingen ekstern harddisk er tilgængelig. Vær venlig at sørge for at den eksterne hukommelse er monteret så app\'en kan fungere korrekt. + Kapitler + Afsnitsnoter + Beskrivelse + Seneste episoder:\u0020 + \u0020episoder + Længde:\u0020 + Størrelse:\u0020 + Behandler + Indlæser... + Gem brugernavn og kodeord + Luk + Prøv igen + Inkluder i automatiske downloads + + Feed URL + Tilføj Podcast med URL + Find podcast i mappen + Du kan søge efter nye podcasts efter navn, kategori eller popularitet i gpodder.net biblioteket + Gennemse gpodder.net + + Marker alle som læst + Marker alle episoder som læst + Vis information + Fjern podcast + Del webside link + Del feed link + Bekræft venligst at du vil fjerne dette feed og ALLE episoder du har downloadet fra dette feed. + Fjerner feed + + Hent + Afspil + Pause + Stream + Fjern + Fjern episode + Marker som læst + Marker som ulæst + Tilføj til kø + Fjern fra kø + Besøg webside + Flattr dette + Sæt alle i kø + Download alle + Spring episode over + + succesfuld + fejlet + Download afventer + Download kører + Kan ikke finde lager-enhed + Ikke nok plads + Fil fejl + HTTP data fejl + Ukendt fejl + Parser undtagelse + Feed type er ikke understøttet + Forbindelsesfejl + Ukendt vært + Godkendelses fejl + Annuller alle downloads + Download afbrudt + Downloads afsluttet + Misdannet URL + IO fejl + Anmode fejl + Adgangsfejl i database + \u0020Downloads tilbage + Bearbejder downloads + Downloader podcast data + %1$d downloads lykkedes, %2$d fejlet + Ukendt titel + Feed + Medie fil + Billede + En fejl opstod under download af filen:\u0020 + Godkendelses krævet + Den ressource du efterspurgte kræver et brugernavn og et password + + Fejl! + Ingen medier afspiller + Forbereder + Klar + Søger + Server døde + Ukendt fejl + Ingen medier afspiller + 00:00:00 + Buffering + Afspiller podcast + + Fjern kø + Fortryd + Emne slettet + Flyt til toppen + Flyt til bunden + + Flattr log ind + Tryk på knappen nedenfor for at starte godkendelsesprocessen. Du vil blive ført til flattr log ind siden i din browser og bedt om at give AntennaPod tilladelse til at flattr emner. Efter at du har givet tilladelsen vil du automatisk vende tilbage til denne side. + Godkender + Retuner hjem + Godkendelse lykkedes! Du kan nu flattr emner inde i app\'en. + Ingen flattr polet fundet + Din flattr konto er vidst ikke forbundet til AntennaPod. Du kan forbinde din konto til AntennaPod for at flattr emner inde i app\'en, eller besøge websiden af mediet for at flattr det der. + Godkender + Forbudt handling + AntennaPod har ikke tilladelse til denne handling. Årsagen kunne være at adgangspoletten for AntennaPod til din konto er blevet tilbagekaldt. Du kan enten godkende den igen eller besøge websiden for mediet istedet. + Adgang tilbagekaldt + Du har succesfuldt tilbagekaldt AntennaPods adgangs polet til din konto. For at fuldføre processen skal du fjerne denne app fra listen af godkendte applikationer i din kontos indstillinger på flattr\'s hjemmeside. + + Flattr\'et en ting! + Flattr\'et %d ting! + Flattr\'et: %s. + Det mislykkedes at flattr %d ting! + Ikke flattr\'et: %s. + Ting vil blive flattr\'et senere + Flattr\'er %s + AntennaPod flatttr\'er + AntennaPod har flattr\'et + AntennaPod flattr mislykkedes + Hent flatt\'rede ting + + Hent Plugin + Plugin er ikke installeret + For at få variabel afspilningshastighed til at virke skal der installeres et tredjepartsprogram.\n\nTryk \'Download Plugin\' for at downloade et gratis plugin fra Play Store\n\nAlle problemer forårsaget ved at bruge dette plugin er ikke AntennaPods ansvar og bør meldes til ejeren af plugin\'et. + Afspilningshastigheder + + Der er ingen emner i denne liste. + Du har endnu ikke abonneret til nogle feeds. + + Andre + Om + + Tjenester + Flattr + Sæt afspilning på pause når hovedtelefoner afbrydes + Hop til næste medie i køen når afspilning er færdig + Afspilning + Netværk + Opdaterings interval + Specificer et interval indenfor hvilket feeds opdaterer automatisk eller deaktiver det + Download kun medie filer over WiFi + Kontinuerlig afspilning + WiFi medie download + Hovedtelefoner afbrudt + Mobile opdateringer + Tillad opdateringer over mobil data forbindelse + Opdaterer + Flattr indstillinger + Flattr log ind + Log ind til din flattr konto for at flattr emner direkte fra app\'en + Flattr denne app + Støt udviklingen af AntennaPod ved at flattr den. Tak! + Tilbagekald adgang + Tilbagekald adgangen til din flattr konto fra denne app. + Flattr\'er automatisk + Brugerflade + Vælg tema + Skift AntennaPods udseende. + Download automatisk + Konfigurer automatisk download af episoder + Sæt Wi-Fi filter til + Tillad kun automatisk download for de valgte Wi-Fi netværk + Episode cache + Lys + Mørk + Uendelig + timer + time + Manuelt + Log ind + Log ind med din gpodder.net konto for at synkronisere dine abonnementer. + Log ud + Logget ud + Skift login information + Skift din gpodder.net kontos login information. + Afspilningshastigheder + Tilpas tilgængelige hastigheder for variabelt afspilningshastigheds plugin + Indstil værtsnavn + Brug standard vært + + + Søg efter feeds eller episoder + Funder i showets noter + Fundet i kapitler + Fandt ingen resultater + Søg + Fundet i titel + + OPML filer lader dig flytte dine podcasts fra en podcastafspiller til en anden. + For at importere en OPML fil, skal du først placere den i følgende mappe og tryk på knappen nedenfor for at starte import-processen. + Start import + OPML import + FEJL! + Indlæser OPML fil + En fejl opstod under indlæsning af opml documentet: + Import mappen er tom. + Vælg alt + Fravælg alt + Vælg fil at importere + OPML eksport + Eksporterer... + Eksport fejl + Opml eksport lykkedes. + .opml filen var skrevet til:\u0020 + + Sæt søvn timer + Fjern søvn timer + Indtast tid + Søvn timer + Tid tilbage:\u0020 + Ugyldig indtastning, tid skal være et heltal + sekunder + minutter + timer + + KATEGORIER + TOP PODCASTS + FORSLAG + Søg på gpodder.net + Log ind + Velkommen til gpodder.nets login proces. Indsæt dine login informationer: + Log ind + Hvis du ikke har en konto endnu, så kan du oprette en her:\nhttps://gpodder.net/register/ + Brugernavn + Kodeord + Enheds valg + Tilføj en ny enhed for at bruge din gpodder.net konto eller vælg en eksisterende: + Enhed ID:\u0020 + Billedtekst + Opret en ny enhed + Vælg en eksisterende enhed: + Enheds ID må ikke være tomt + Enheds ID er allerede i brug + Vælg + Login lykkedes! + Tillykke! Din gpodder.net konto er nu forbundet med din enhed. AntennaPod vil fra nu af automatisk synkronisere dine abonnementer på din enhed med din gpodder.net konto. + Start synkronisering nu + Gå til hovedskærmen + gpodder.net autentificeringfejl + Forkert brugernavn eller kodeord + gpodder.net synkroniseringsfejl + En fejl opstod under synkronisering:\u0020 + + Valgte mappe: + Opret mappe + Vælg data mappe + Opret en ny mappe med navnet \"%1$s\"? + Opret en ny mappe + Kan ikke skrive til denne mappe + Mappen eksisterer allerede + Kunne ikke oprette ny mappe + Mappen er ikke tom + Mappen du har valgt er ikke tom. Medie downloads og andre filer vil blive placeret i denne mappe. Forsæt alligevel? + Vælg standard mappe + Sæt afspilning på pause i stedet for at sænke lydniveauet når en anden app vil afspille lyde + Pause for afbrydelser + + Abonner + Abonneret + Downloader... + + Vis kapitler + Vis shownoter + Vis billede + Spol tilbage + Hurtigt fremad + Lyd + Video + Naviger opad + Flere handlinger + Episode afspilles + Episode downloades + Episode er downloadet + Nyt emne + Episode er i køen + Antal nye episoder + Antallet af episoder du er begyndt at lytte til + Træk for at skifte denne tings position + + Godkendelse + Skift dit brugernavn og kodeord for denne podcast og dets episoder. + + Importerer abonnementer fra single-purpose apps… + diff --git a/core/src/main/res/values-de/strings.xml b/core/src/main/res/values-de/strings.xml new file mode 100644 index 000000000..539470a5e --- /dev/null +++ b/core/src/main/res/values-de/strings.xml @@ -0,0 +1,341 @@ + + + + AntennaPod + Feeds + Podcast hinzufügen + PODCASTS + EPISODEN + Neue Episoden + Alle Episoden + Neu + Warteliste + Einstellungen + Podcast hinzufügen + Downloads + Aktiv + Abgeschlossen + Log + Download abbrechen + Zuletzt gespielt + gpodder.net + gpodder.net Anmeldung + + Zuletzt veröffentlicht + Nur neue Episoden anzeigen + + Menü öffnen + Menü schließen + + Im Browser öffnen + URL kopieren + URL teilen + URL wurde in die Zwischenablage kopiert. + Gehe zu dieser Position + + Chronik löschen + + Bestätigen + Abbrechen + Autor + Sprache + Einstellungen + Bild + Fehler + Ein Fehler ist aufgetreten: + Aktualisieren + Der externe Speicher ist nicht verfügbar. Bitte stelle sicher, dass das externe Speichermedium eingelegt ist, damit die Anwendung funktioniert. + Kapitel + Notizen + Beschreibung + Letzte Episode:\u0020 + \u0020Episoden + Länge:\u0020 + Größe:\u0020 + Verarbeite + Lade ... + Benutzername und Password merken + Schließen + Erneut versuchen + Automatisch herunterladen + + Feed URL + URL des Feeds oder der Webseite + Podcast per URL hinzufügen + Podcast in Verzeichnis finden + Bei gpodder.net kannst du nach neuen Podcasts nach Name, Kategorie oder Popularität suchen. + gpodder.net durchsuchen + + Markiere alle als gelesen + Alle Episoden als gelesen markieren + Informationen anzeigen + Podcast entfernen + Webseiten-Link teilen + Feed-Link teilen + Bitte bestätige, dass du diesen Feed und ALLE heruntergeladenen Episoden dieses Feeds entfernen möchtest. + Entferne Feed + + Herunterladen + Abspielen + Pausieren + Streamen + Entfernen + Episode entfernen + Als gelesen markieren + Als ungelesen markieren + Zur Abspielliste hinzufügen + Aus der Abspielliste entfernen + Webseite besuchen + Flattr this + Alle zur Abspielliste hinzufügen + Alle herunterladen + Episode überspringen + + erfolgreich + fehlgeschlagen + Download anstehend + Download läuft + Speichermedium nicht gefunden + Zu wenig Speicherplatz + Dateifehler + HTTP Datenfehler + Unbekannter Fehler + Parserfehler + Nicht unterstützter Feed-Typ + Verbindungsfehler + Unbekannter Host + Authentifizierungsfehler + Alle Downloads abbrechen + Download abgebrochen + Download abgeschlossen + Fehler in URL + IO Error + Anfragefehler + Datenbankzugriffsfehler + \u0020Downloads übrig + Verarbeite Downloads + Lade Podcast-Daten + %1$d Downloads erfolgreich, %2$d fehlgeschlagen + Unbekannter Titel + Feed + Mediendatei + Bild + Beim Herunterladen der Datei ist ein Fehler aufgetreten:\u0020 + Authentifizierung erforderlich + Die angeforderte Quelle erfordert einen Benutzernamen und ein Passwort + + Fehler! + Keine Medienwiedergabe + Bereite vor + Fertig + Spule + Server ist offline + Unbekannter Fehler + Keine Medienwiedergabe + 00:00:00 + Puffert + Spiele Podcast ab + AntennaPod - Unbekannte Medientaste: %1$d + + Abspielliste leeren + Rückgängig + Element entfernt + Zum Anfang verschieben + Zum Ende verschieben + + Flattr Anmeldung + Drücke den Button unten um den Authentifizierungsprozess zu starten. Du wirst dann zur Flattr-Anmeldeseite weitergeleitet, wo du gefragt wirst, AntennaPod die Erlaubnis zu geben, Dinge zu flattrn. Nachdem du die Erlaubnis erteilt hast, kehrst du automatisch zu diesem Bildschirm zurück. + Authentifizieren + Zur Hauptseite zurückkehren + Die Authentifizierung war erfolgreich! Du kannst nun in der Anwendung Flattr verwenden. + Kein Flattr Token gefunden + Dein Flattr Account scheint nicht mit AntennaPod verbunden zu sein. Tippe hier zum authentifizieren. + Dein Flattr Account scheint nicht mit AntennaPod verbunden zu sein. Du kannst entweder deinen Account mit AntennaPod verbinden, um direkt in der Anwendung Flattr zu verwenden, oder du kannst die Flattr-Seite der Sache im Netz besuchen. + Authentifizieren + Aktion verboten + AntennaPod besitzt keine Erlaubnis für diese Aktion. Der Grund dafür könnte sein, dass AntennaPods Zugangstoken aufgehoben worden ist. Du kannst dich entweder erneut authentifizieren oder die Flattr-Seite der Sache im Web besuchen. + Zugriff widerrufen + Du hast AntennaPod das Zugangstoken zu deinem Account entzogen. Um diesen Prozess abzuschließen, musst du diese Anwendung aus der Liste der zugelassenen Anwendungen in deinen Account Einstellungen auf der Flattr Webseite entfernen. + + Eine Sache wurde geflattrt! + %d Sachen wurden geflattrt! + Geflattrt: %s + Flattrn von %d Sachen fehlgeschlagen! + Nicht geflattrt: %s + Sache wird später gelfattrt + Flattrt: %s + AntennaPod flattrt + AntennaPod hat geflattrt + AntennaPod Flattrn fehlgeschlagen + Rufe geflatterte Sachen ab + + Plugin herunterladen + Plugin nicht installiert + Um die Wiedergabegeschwindigkeit zu verändern, muss eine Drittanbieter-Bibliothek heruntegeladen werden.\n\nDrücke auf \"Plugin herunterladen\", um ein kostenloses Plugin aus dem Play Store zu installieren.\n\nProbleme, die bei der Benutzung des Plugins auftreten, sollten dem Entwickler des Plugins gemeldet werden. + Wiedergabegeschwindigkeiten + + Es sind keine Einträge in dieser Liste. + Du hast noch keine Feeds abonniert. + + Anderes + Über + Abspielliste + Dienste + Flattr + Pausiere die Wiedergabe wenn der Kopfhörer entfernt worden ist. + Springe zur nächsten Episode wenn die vorherige Episode endet + Wiedergabe + Netzwerk + Aktualisierungsintervall + Lege ein Intervall fest, in dem Feeds automatisch aktualisiert werden oder deaktiviere es + Lade Mediendateien nur über WiFi + Durchgehendes Abspielen + WiFi Medien-Download + Kopfhörer-Trennung + Mobile Aktualisierungen + Erlaube Aktualisierungen über die mobile Datenverbindung + Aktualisiere + Flattr Einstellungen + Flattr Anmeldung + Melde dich mit deinem Flattr Account an, um direkt in der Anwendung zu flattrn. + Flattr diese Anwendung + Unterstütze die Entwicklung von AntennaPod mit Flattr. Danke! + Zugriff entziehen + Entziehe dieser Anwendung die Zugriffserlaubnis für deinen Flattr Account. + Automatisches Flattrn + Automatisches Flattrn konfigurieren + Benutzeroberfläche + Theme auswählen + Ändere das Aussehen von AntennaPod. + Automatisches Herunterladen + Konfiguriere das automatische Herunterladen von Episoden. + W-LAN-Filter aktivieren + Erlaube das automatische Herunterladen nur in ausgewählten W-LAN Netzwerken. + Episodenspeicher + Hell + Dunkel + Unbegrenzt + Stunden + Stunde + Manuell + Anmelden + Melde dich mit deinem gpodder.net profil an um deine Abonnements zu synchronisieren + Abmelden + Abmeldung war erfolgreich + Anmeldeinformationen ändern + Ändere die Anmeldeinformationen deines gpodder.net profils + Wiedergabegeschwindigkeiten + Lege die verfügbaren Werte für die Veränderung der Wiedergabeschwindigkeit fest + Spul-Zeit + Spule so viele Sekunden vor oder zurück + Hostname ändern + Standard-Host verwenden + + Automatisches Flattrn aktivieren + Flattr eine Episode sobald %d Prozent gespielt worden sind + Flattr Episode, sobald die Wiedergabe beginnt + Flattr Episode, sobald die Wiedergabe endet + + Suche nach Feeds oder Episoden + In Sendungsnotizen gefunden + In Kapiteln gefunden + Keine Ergebnisse gefunden + Suche + In Titel gefunden + + Mit OPML Dateien kannst du deine Podcasts von einem Podcatcher auf einen anderen übertragen + Um eine OPML Datei zu importieren, musst du diese im folgenden Ordner platzieren und den unteren Button antippen, um den Import Prozess zu starten. + Import starten + OPML Import + FEHLER! + Lese OPML Datei + Ein Fehler is beim Lesen des OPML Dokuments aufgetreten: + Der Import-Ordner ist leer. + Alle auswählen + Auswahl zurücksetzen + Wähle eine Datei zum Importieren aus + OPML Export + Exportiere... + Exportfehler + OPML Export erfolgreich + Die .opml Datei wurde unter dem folgenden Pfad gespeichert:\u0020 + + Schlummerfunktion + Schlummerfunktion deaktivieren + Zeit eingeben + Schlummerfunktion + Zeit übrig:\u0020 + Ungültige Eingabe, Zeit muss eine Ganzzahl sein + Sekunden + Minuten + Stunden + + KATEGORIEN + BESTE PODCASTS + VORSCHLÄGE + gpodder.net durchsuchen + Anmeldung + Willkommen beim gpodder.net Anmeldeprozess. Gib zuerst deine Anmeldeinformationen ein: + Anmelden + Falls du noch kein gpodder.net profil hast, kannst du hier eines erstellen:\nhttps://gpodder.net/register/ + Benutzername + Passwort + Geräte-Auswahl + Erstelle ein neues Gerät für dein gpodder.net profil oder wähle ein bereits vorhandenes: + Geräte-ID:\u0020 + Beschreibung + Neues Gerät erstellen + Vorhandenes Gerät auswählen + Geräte-ID darf nicht leer sein + Geräte-ID wird bereits verwendet + Auswählen + Anmeldung erfolgreich! + Glückwunsch! Dein gpodder.net profil ist jetzt mit deinem Gerät verbunden. Von nun an wird AntennaPod automatisch deine Abonnements mit deinem gpodder.net profil synchronisieren. + Jetzt synchronisieren + Zum Hauptbildschirm zurückkehren + gpodder.net Anmeldefehler + Falscher Benutzername oder falsches Passwort + gpodder.net Synchronisierungsfehler + Ein Fehler ist beim Synchronisieren aufgetreten:\u0020 + + Ausgewählter Ordner + Neuer Ordner + Datenordner auswählen + Neuen Ordner mit Namen \"%1$s\" erstellen? + Neuer Ordner angelegt + Kann in diesem Ordner nicht schreiben + Ordner existiert bereits + Konnte Datenordner nicht erstellen + Ordner ist nicht leer + Der ausgewählte Ordner ist nicht leer. Medien-Downloads und andere Daten werden direkt in diesem Ordner gespeichert. Trotzdem fortfahren? + Standardordner auswählen + Pausiere die Wiedergabe anstatt die Lautstärke zu reduzieren, wenn eine andere Anwendung Töne abspielt + Bei Unterbrechungen pausieren + + Abonnieren + Abonniert + Lade herunter... + + Kapitel anzeigen + Sendungsnotizen anzeigen + Bild anzeigen + Zurückspulen + Vorspulen + Audio + Video + Nach oben navigieren + Mehr Aktionen + Episode wird gerade abgespielt + Episode wird gerade heruntergeladen + Episode ist heruntergeladen + Eintrag ist neu + Episode befindet sich inder Abspielliste + Anzahl neuer Episoden + Anzahl der Episoden, die du angefangen hast zu hören + Ziehe, um die Position dieses Objekts zu verändern + + Authentifizierung + Ändere den Benutzernamen und das Passwort für diesen Podcast und dessen Episoden. + + Importiere Abonnements aus Single-Purpose Apps + diff --git a/core/src/main/res/values-es-rES/strings.xml b/core/src/main/res/values-es-rES/strings.xml new file mode 100644 index 000000000..cd4949530 --- /dev/null +++ b/core/src/main/res/values-es-rES/strings.xml @@ -0,0 +1,200 @@ + + + + AntennaPod + Canales + PODCASTS + EPISODIOS + Nuevos + Lista de espera + Ajustes + Descargas + Cancelar descarga + Historial de reproducción + + + + Abrir en el navegador + Copiar URL + Compartir URL + URL copiada al portapapeles. + + Limpiar el historial + + Confirmar + Cancelar + Autor + Idioma + Error + Ha ocurrido un error: + Actualizar + No se encuentra un almacenamiento externo. Asegúrese de que su almacenamiento externo esté montado para que la aplicación funcione correctamente. + Capítulos + Notas del programa + \u0020episodios + Duración:\u0020 + Tamaño:\u0020 + Procesando + Cargando... + + URL del canal + + Marcar todo como leído + Información del programa + Compartir el enlace de la web + Compartir el enlace del canal + Confirme que quiere eliminar este canal y TODOS los episodios descargados del mismo. + + Descargar + Reproducir + Pausar + Transmitir + Quitar + Marcar como leído + Marcar como no leído + Añadir a la cola + Quitar de la cola + Visitar el sitio web + Añadir a Flattr + Ponerlos todos en cola + Descargarlos todos + Saltar episodio + + Descarga pendiente + Descarga en curso + No se ha encontrado un dispositivo de almacenamiento + Espacio insuficiente + Error de archivo + Error de datos HTTP + Error desconocido + Excepción del analizador + Tipo de canal no admitido + Error de conexión + Host desconocido + Cancelar todas las descargas + Descarga cancelada + Descargas completadas + URL malformada + Error de E/S + Error de petición + \u0020descargas restantes + Descargando datos del podcast + %1$d descargas exitosas, %2$d fallidas + Título desconocido + Canal + Archivo de medios + Imagen + Ha ocurrido un error al intentar descargar el archivo:\u0020 + + ¡Error! + No hay medios en reproducción + Preparando + Listo + Buscando + El servidor está inactivo + Error desconocido + No hay medios en reproducción + 00:00:00 + Almacenando + Reproduciendo el podcast + + Limpiar la cola + + Identificarse en Flattr + Pulse el botón inferior para comenzar la autenticación. Su navegador abrirá la pantalla de identificación de Flattr y le preguntará si quiere conceder permiso a AntennaPod para valorar cosas. Tras concederlo, volverá a esta pantalla automáticamente. + Autenticarse + Volver a la pantalla principal + Autentificación exitosa. Ya puede valorar cosas en Flattr desde la aplicación. + No se ha encontrado un token de Flattr + Su cuenta de Flattr no está conectada con AntennaPod. Puede conectarla o puede visitar la página web de cada cosa para valorarla desde allí. + Autenticarse + Acción prohibida + AntennaPod no tiene permiso para realizar esta acción. La razón puede ser que se haya revocado el token de acceso de AntennaPod para su cuenta. Puede re-autenticarse o visitar la página web de la cosa. + Acceso revocado + Ha revocado el token de acceso de AntennaPod a su cuenta. Para completar el proceso debe eliminar esta aplicación de la lista de aplicaciones aprobadas, en los ajustes de Flattr. + + + + Esta lista no tiene elementos. + No se ha suscrito a ningún canal. + + Otros + Acerca de + Cola + Pausar la reproducción al desconectar los auriculares + Saltar al siguiente elemento de la cola al acabar la reproducción + Reproducción + Red + Intervalo de actualización + Especificar el intervalo en que se actualizarán automáticamente los canales, o desactivarlo + Solo descargar los contenidos por WiFi + Reproducción continua + Descarga de contenidos por WiFi + Desconexión de los cascos + Actualizaciones por red móvil + Permitir actualizaciones por red de datos móvil + Actualizando + Ajustes de Flattr + Identificación en Flattr + Identifíquese en Flattr para valorar cosas directamente desde la aplicación + Valorar esta aplicación en Flattr + Apoye el desarrollo de AntennaPod valorándola en Flattr. ¡Gracias! + Revocar el acceso + Rescindir el permiso de acceso de esta aplicación a su cuenta de Flattr. + Interfaz de usuario + Elegir un tema + Cambiar la apariencia de AntennaPod. + Descarga automática + Configurar la descarga automática de episodios. + Activar el filtro WiFi + Permitir la descarga automática sólo para las redes WiFi marcadas. + Caché de episodios + + + Buscar canales o episodios + Encontrado en las notas del programa + Encontrado en los capítulos + No se han encontrado resultados + Buscar + Encontrado en el título + + Para importar un archivo OPML, debe copiarlo al siguiente directorio y pulsar el botón que se muestra abajo. + Comenzar la importación + Importación de OPML + ¡ERROR! + Leyendo el archivo OPML + Ha ocurrido un error al leer el archivo OPML + El directorio de importación está vacío + Seleccionar todo + Deseleccionar todo + Elegir qué archivo importar + Exportar a OPML + Exportando... + Error en la exportación + Exportación a OPML exitosa + El archivo OPML se ha escrito en:\u0020 + + Establecer un temporizador + Desactivar el temporizador + Introducir hora + Temporizador + Tiempo restante:\u0020 + Entrada no válida, el tiempo debe ser un entero + + + Carpeta seleccionada + Crear carpeta + Elegir carpeta de datos + ¿Crear carpeta con nombre «%1$s»? + Carpeta creada + No se puede escribir a esta carpeta + Ya existe la carpeta + No se ha podido crear la carpeta + La carpeta no está vacía + La carpeta elegida no está vacía. Las descargas y otros archivos se copiarán directamente en esta carpeta. ¿Continuar igualmente? + Elegir carpeta predeterminada + + + + + diff --git a/core/src/main/res/values-es/strings.xml b/core/src/main/res/values-es/strings.xml new file mode 100644 index 000000000..1b87e6dbc --- /dev/null +++ b/core/src/main/res/values-es/strings.xml @@ -0,0 +1,313 @@ + + + + AntennaPod + Canales + PODCASTS + EPISODIOS + Episodios nuevos + Todos los episodios + Nuevos + Lista de espera + Ajustes + Añadir podcast + Descargas + Cancelar descarga + Histórico de reproducción + gpodder.net + Iniciar sesión en gpodder.net + + Mostrar solo episodios nuevos + + + Abrir en el navegador + Copiar URL + Compartir URL + URL copiada al portapapeles. + + Vaciar el histórico + + Confirmar + Cancelar + Autor + Idioma + Ajustes + Imagen + Error + Ha ocurrido un error: + Actualizar + No se encuentra un almacenamiento externo. Asegúrese de que su almacenamiento externo esté montado para que la aplicación funcione correctamente. + Capítulos + Notas del programa + Descripción + Episodio más reciente:\u0020 + \u0020episodios + Duración:\u0020 + Tamaño:\u0020 + Procesando + Cargando... + Guardar usuario y contraseña + Cerrar + Reintentar + Incluir en auto descargas + + URL del canal + Añadir podcast por URL + Explorar gpodder.net + + Marcar todo como leído + Información del programa + Compartir el enlace de la web + Compartir el enlace del canal + Confirme que quiere eliminar este canal y TODOS los episodios descargados del mismo. + Eliminando el canal + + Descargar + Reproducir + Pausar + Reproducir por streaming + Quitar + Marcar como leído + Marcar como no leído + Añadir a la cola + Quitar de la cola + Visitar el sitio web + Añadir a Flattr + Ponerlos todos en cola + Descargarlos todos + Omitir episodio + + Descarga pendiente + Descarga en curso + No se ha encontrado un dispositivo de almacenamiento + Espacio insuficiente + Error de archivo + Error de datos HTTP + Error desconocido + Excepción del analizador + Tipo de canal no admitido + Error de conexión + Host desconocido + Error de autenticación + Cancelar todas las descargas + Descarga cancelada + Descargas completadas + URL con formato incorrecto + Error de E/S + Error de solicitud + Error de acceso a la base de datos + \u0020descargas restantes + Descargando datos del podcast + %1$d descargas exitosas, %2$d fallidas + Título desconocido + Canal + Archivo de medios + Imagen + Ha ocurrido un error al intentar descargar el archivo:\u0020 + Autenticación requerida + El recurso solicitado requiere usuario y contraseña + + ¡Error! + No hay medios en reproducción + Preparando + Listo + Buscando + El servidor está inactivo + Error desconocido + No hay medios en reproducción + 00:00:00 + Almacenando + Reproduciendo el podcast + + Vaciar la cola + Deshacer + Artículo eliminado + Mover al principio + Mover al final + + Identificarse en Flattr + Pulse el botón inferior para comenzar la autenticación. Su navegador abrirá la pantalla de identificación de Flattr y le preguntará si quiere conceder permiso a AntennaPod para valorar cosas. Tras concederlo, volverá a esta pantalla automáticamente. + Autenticarse + Volver a la pantalla principal + Autentificación exitosa. Ya puede valorar cosas en Flattr desde la aplicación. + No se ha encontrado un token de Flattr + Su cuenta de Flattr no está conectada con AntennaPod. Puede conectarla o puede visitar la página web de cada cosa para valorarla desde allí. + Autenticarse + Acción prohibida + AntennaPod no tiene permiso para realizar esta acción. La razón puede ser que se haya revocado el token de acceso de AntennaPod para su cuenta. Puede re-autenticarse o visitar la página web de la cosa. + Acceso revocado + Ha revocado el token de acceso de AntennaPod a su cuenta. Para completar el proceso debe eliminar esta aplicación de la lista de aplicaciones aprobadas, en los ajustes de Flattr. + + ¡Flattr una cosa! + ¡Flattr %d cosas! + Flattr: %s. + ¡Falló Flattr de %d cosas! + No se hizo Flattr: %s. + Se hará Flattr de esta cosa más tarde + Haciendo Flattr de %s + AntennaPod haciendo Flattr + AntennaPod hizo Flattr + AntennaPod Flattr falló + Obteniendo lista de Flattr + + Descargar complemento + Complemento no instalado + Para que la reproducción a velocidad variable funcione, es necesario instalar un complemento adicional.\n\nPulse «Descargar complemento» para descargar un complemento gratuito de la Play Store.\n\nSi aparece cualquier problema durante la utilización del complemento, informe de él al propietario, pues éste no es responsabilidad de AntennaPod. + Velocidades de reproducción + + Esta lista no tiene elementos. + No se ha suscrito a ningún canal. + + Otros + Acerca de + Cola + Servicios + Flattr + Pausar la reproducción al desconectar los auriculares + Saltar al siguiente elemento de la cola al acabar la reproducción + Reproducción + Red + Intervalo de actualización + Especificar el intervalo en que se actualizarán automáticamente los canales, o desactivarlo + Solo descargar los contenidos por WiFi + Reproducción continua + Descarga de contenidos por WiFi + Desconexión de los cascos + Actualizaciones por red móvil + Permitir actualizaciones por red de datos móvil + Actualizando + Ajustes de Flattr + Identificación en Flattr + Identifíquese en Flattr para valorar cosas directamente desde la aplicación + Valorar esta aplicación en Flattr + Apoye el desarrollo de AntennaPod valorándola en Flattr. ¡Gracias! + Revocar el acceso + Rescindir el permiso de acceso de esta aplicación a su cuenta de Flattr. + Uso de Flattr automático + Interfaz de usuario + Elegir un tema + Cambiar la apariencia de AntennaPod. + Descarga automática + Configurar la descarga automática de episodios. + Activar el filtro WiFi + Permitir la descarga automática sólo para las redes WiFi marcadas. + Caché de episodios + Claro + Oscuro + Ilimitado + horas + hora + Manual + Iniciar sesión + Inicie sesión con su cuenta de gpodder.net para sincronizar sus suscripciones. + Cerrar sesión + Ha cerrado la sesión correctamente. + Cambiar información de acceso + Modificar datos de inicio de sesión en gpodder.net. + Velocidades de reproducción + Personalice las velocidades disponibles para la reproducción de audio a velocidad variable + Definir nombre de equipo + Usar nombre de equipo por defecto + + + Buscar canales o episodios + Encontrado en las notas del programa + Encontrado en los capítulos + No se han encontrado resultados + Buscar + Encontrado en el título + + Los archivos OPML le permiten migrar sus podcasts de una aplicación a otra. + Para importar un archivo OPML, debe copiarlo al siguiente directorio y pulsar el botón que se muestra abajo. + Comenzar la importación + Importación de OPML + ¡ERROR! + Leyendo el archivo OPML + Ha ocurrido un error al leer el archivo OPML + El directorio de importación está vacío. + Seleccionar todo + Deseleccionar todo + Elegir qué archivo importar + Exportar a OPML + Exportando... + Error en la exportación + Exportación a OPML exitosa + El archivo OPML se ha escrito en:\u0020 + + Establecer un temporizador + Desactivar el temporizador + Introducir hora + Temporizador + Tiempo restante:\u0020 + Entrada no válida, el tiempo debe ser un entero + segundos + minutos + horas + + CATEGORÍAS + MEJORES PODCASTS + SUGERENCIAS + Buscar en gpodder.net + Iniciar sesión + Bienvenido al proceso de autenticación de gpodder.net. Primero, escriba sus datos de inicio de sesión: + Iniciar sesión + Si tiene una cuenta aún, puede crear una aquí:\nhttps://gpodder.net/register/ + Nombre de usuario + Contraseña + Selección del dispositivo + Cree un nuevo dispositivo para usar con su cuenta de gpodder.net o elija uno existente: + Id. de dispositivo:\u0020 + Descripción + Crear nuevo dispositivo + Elegir dispositivo existente: + El ID de dispositivo no puede estar vacío + El ID de dispositivo ya está en uso + Elegir + ¡Inicio de sesión correcto! + ¡Enhorabuena! Su cuenta de gpodder.net está ahora asociada con su dispositivo. A partir de ahora AntennaPod sincronizará automáticamente las suscripciones de su dispositivo con su cuenta de gpodder.net. + Comenzar sincronización ahora + Ir a la pantalla principal + Error de autenticación de gpodder.net + Usuario o contraseña incorrectos + Error de sincronización de gpodder.net + Ocurrió un error de sincronización:\u0020 + + Carpeta seleccionada + Crear carpeta + Elegir carpeta de datos + ¿Crear carpeta con nombre «%1$s»? + Carpeta creada + No se puede escribir a esta carpeta + Ya existe la carpeta + No se ha podido crear la carpeta + La carpeta no está vacía + La carpeta elegida no está vacía. Las descargas y otros archivos se copiarán directamente en esta carpeta. ¿Continuar igualmente? + Elegir carpeta predeterminada + Pausar la reproducción en lugar de bajar el volumen cuando otra aplicación reproduzca sonidos + Pausar durante las interrupciones + + Suscribirse + Suscrito + Descargando… + + Mostrar capítulos + Mostrar notas del programa + Mostrar imagen + Rebobinar + Avance rápido + Audio + Vídeo + Navegar hacia arriba + Más acciones + El episodio se está reproduciendo + El episodio se está descargando + El episodio está descargado + El elemento es nuevo + El episodio está en la cola + Cantidad de episodios nuevos + Cantidad de episodios que ha comenzado a escuchar + + Autenticación + + Importando subscripciones de aplicaciones de uso específico... + diff --git a/core/src/main/res/values-fr/strings.xml b/core/src/main/res/values-fr/strings.xml new file mode 100644 index 000000000..afc441b99 --- /dev/null +++ b/core/src/main/res/values-fr/strings.xml @@ -0,0 +1,340 @@ + + + + AntennaPod + Flux + Ajouter un podcast + PODCASTS + ÉPISODES + Nouveaux épisodes + Tous les épisodes + Nouveau + Liste d\'attente + Préférences + Ajouter un podcast + Téléchargements + En cours + Terminé + Journal d\'activités + Annuler les téléchargements + Journal des lectures + gpodder.net + identifiants gpodder.net + + Publié récemment + N\'afficher que les nouveaux épisodes + + Ouvrir le menu + Fermer le menu + + Ouvrir dans le navigateur + Copier l\'URL + Partager l\'URL + URL copiée dans le presse-papier + Aller à cette position + + Effacer le journal + + Confirmer + Annuler + Auteur + Langue + Préférences + Image + Erreur + Une erreur a eu lieu : + Rafraîchir + Aucun stockage externe n\'est disponible. Merci de connecter un volume de stockage externe pour que l\'application puisse fonctionner correctement. + Chapitres + Notes d\'épisode + Description + Épisode le plus récent :\u0020 + \u0020épisodes + Durée :\u0020 + Taille :\u0020 + Traitement en cours + En chargement... + Sauvegarder votre identifiant et votre mot de passe + Fermer + Réessayer + Télécharger automatiquement à l\'avenir + + URL du flux + URL ou flux ou site web + Ajouter un podcast par son URL + Trouver le podcast dans la bibliothèque + Vous pouvez chercher de nouveaux podcasts en filtrant par nom, catégorie ou popularité dans la bibliothèque gpodder.net + Chercher sur gpodder.net + + Tous marquer comme lus + Tous les épisodes ont été marqués comme lus + Voir les détails + Supprimer le podcast + Partager un lien vers le site + Partager le flux + Veuillez confirmer que vous voulez bien supprimer ce flux et TOUS ses épisodes que vous avez téléchargés. + Flux en cours de suppression + + Télécharger + Lire + Pause + Lire en ligne + Supprimer + Supprimer cet épisode + Marquer comme lu + Marquer comme non lu + Ajouter à la liste + Supprimer de la liste + Visiter le site + Flattr ça! + Ajouter tous à la liste + Tous télécharger + Passer cet épisode + + terminé + échoué + Téléchargement en attente + Téléchargement en cours + Volume de stockage non trouvé + Espace insuffisant + Accès au fichier impossible + Erreur de données HTTP + Erreur inconnue + Exception de l\'analyseur + Type de flux non géré + Erreur de connexion + Hôte inconnu + Erreur d\'authentification + Annuler tous les téléchargements + Téléchargement annulé + Téléchargements terminés + URL incorrecte + Erreur d\'E/S + Erreur de requête + Problème dans l\'accès à la base de données + \u0020téléchargements restants + Traitement des téléchargements + Téléchargement des données du podcast + %1$d téléchargements réussis, %2$d échoués + Titre inconnu + Flux + Fichier média + Image + Une erreur s\'est produite durant le téléchargement du fichier :\u0020 + Authentification requise + La ressource que vous avez demandé nécessite un nom d\'utilisateur et un mot de passe + + Erreur ! + Pas de lecture en cours + En préparation + Prêt + Recherche + Le serveur ne répond pas + Erreur inconnue + Aucune lecture + 00:00:00 + Mise en mémoire + Lecture de podcast en cours + AntennaPod - Touche média inconnue : %1$d + + Effacer la liste + Annuler + Élément retiré + Déplacer vers le haut de haut de la liste + Déplacer vers le bas de la liste + + Connecter à Flattr + Appuyez sur le bouton ci-dessous pour vous authentifier. Vous serez envoyés à l\'écran de connexion Flattr dans le navigateur, et il vous sera demandé de donner à AntennaPod la permission de flattr. Une fois ceci fait, vous reviendrez automatiquement à cet écran. + S\'authentifier + Revenir au départ + L\'authentification a réussi. Vous pouvez maintenant flattr depuis cette application. + Aucun jeton Flattr trouvé. + Votre compte flattr semble ne pas être connecté à AntennaPod. Touchez ici pour vous connecter. + Votre compte Flattr se semble pas être connecté à AntennaPod. Vous pouvez soit connecter votre compte Flattr à AntennaPod pour pouvoir flattr depuis l\'application, ou vous pouvez aller sur le site de ce que vous voulez flattr. + S\'authentifier + Action interdite + AntennaPod n\'a pas la permission pour cette action. Il est possible que l\'accès à votre compte depuis AntennaPod ait été révoqué. Vous pouvez vous authentifier à nouveau, ou bien visiter le site à flattr directement. + Accès révoqué + Vous avez révoqué le jeton d\'accès d\'AntennaPod à votre compte. Pour terminer cette opération, vous devez retirer AntennaPod de la liste des applications autorisées sur le site web de Flattr. + + Une chose de Flattré ! + %d choses de Flattré ! + Flattré : %s. + Impossible de Flattrer %d choses ! + Non Flattré : %s. + Cette chose sera Flattré plus tard + En train de Flattrer %s + AntennaPod est en train de Flattrer + AntennaPod a Flattré + Flattr d\'AntennaPod a échoué + Obtention de la liste des choses Flattrées + + Télécharger une extension + Extension non installée + Pour pouvoir changer la vitesse de lecture il est nécessaire d\'installer une librairie tierce.\n\nSélectionnez \"Télécharger une extension\" pour télécharger une extension gratuite depuis le Play Store\n\nLes problèmes concernant les extensions sont de la responsabilité de leur créateur et non d\'AntennaPod. Veillez à notifier le créateur de l\'extension de tout problème. + Vitesses de lecture + + Cette liste est vide. + Vous n\'êtes encore abonné à aucun flux. + + Autres + À propos + Liste + Services + Flattr + Interrompre la lecture lorsque le casque est débranché + Après la fin d\'un épisode, passer au suivant + Lecture + Réseau + Intervalle de mise à jour + Indiquer un intervalle de mise à jour automatique des flux, ou le désactiver + Ne télécharger les épisodes que par Wi-Fi + Lecture continue + Téléchargement en Wi-Fi + Déconnexion du casque + Mise à jour mobile + Autoriser les mises à jour à travers la connexion de données mobile + Mise à jour en cours + Paramètres Flattr + Connexion à Flattr + Connectez-vous à votre compte Flattr pour pouvoir flattr directement depuis l\'application. + Flattr cette application + Encouragez le développement d\'AntennaPod grâce à Flattr. Merci ! + Révoquer l\'accès + Révoquer la permission d\'accès à votre compte Flattr depuis cette application. + Flattr automatique + Configurer les paiements flattr automatiques + Interface utilisateur + Choisir un thème + Modifier l\'apparence d\'AntennaPod. + Téléchargement automatique + Configurer le téléchargement automatique des épisodes. + Activer le filtre Wi-Fi + Autoriser le téléchargement automatique uniquement sur les réseaux Wi-Fi sélectionnés. + Épisodes stockés localement + Clair + Sombre + Illimité + heures + heure + Manuel + Identifiant + Identifiez vous avec votre compte gpodder.net afin de synchroniser vos abonnements + Se déconnecter + Vous êtes maintenant déconnecté + Modifier les informations de connexion + Modifier les information de connexion pour votre compte gpodder.net + Vitesses de lecture + Modifier la liste des vitesses disponibles pour la lecture audio + Bouger d\'autant de secondes en rembobinant ou en faisant une avance rapide + Choisir un nom de domaine + Utiliser le nom de domaine par défaut + + Activer le paiement flattr automatique + Lancer un paiement flattr pour un épisode dès que %d de l\'épisode a été joué + Lancer le paiement flattr d\'un épisode dès que la lecture commence + Lancer le paiement flattr d\'un épisode à la fin de la lecture + + Chercher des flux ou épisodes + Trouvé dans les notes + Trouvé dans les titres de chapitre + Aucun résultat trouvé + Recherche + Trouvé dans le titre + + Les fichiers OPML vous permettent de bouger vos podcasts d\'un logiciel à un autre. + Pour importer un fichier OPML, copiez-le dans le répertoire suivant, et appuyez sur le bouton ci-dessous pour l\'importer. + Démarrer l\'importation + Importation OPML + ERREUR ! + Lecture du fichier OPML en cours + Une erreur s\'est produite à la lecture du document OPML : + Le répertoire d\'importation est vide. + Tout choisir + Ne rien choisir + Choisir le fichier à importer + Exportation OPML + Exportation en cours... + Erreur d\'exportation + Exportation OPML réussie. + Le fichier .opml a été écrit ici :\u0020 + + Définir le minuteur d\'arrêt automatique + Désactiver le minuteur d\'arrêt automatique + Entrer l\'heure + Arrêt automatique + Durée restante :\u0020 + Entrée invalide, la durée doit être un nombre entier + secondes + minutes + heures + + CATEGORIES + PODCASTS POPULAIRES + SUGGESTIONS + Chercher gpodder.net + Se connecter + Bienvenue dans le processus de connexion à gpodder.net. Premièrement, veuillez entrer vos informations de connexion : + Connexion + SI vous n\'avez pas encore de compte, vous pouvez en créer un⏎\nhttps://gpodder.net/register/ + Identifiant + Mot de passe + Choix de l\'appareil + Créez un nouvel appareil à utiliser pour votre compte gpodder.net ou choisissez un appareil existant : + ID de l\'appareil :\u0020 + Légende + Créer un nouvel appareil + Choisir un appareil existant : + L\'ID de l\'appareil ne peut pas être vide + L\'ID de cet appareil est déjà en cours d\'utilisation + Choisir + Connexion réussie ! + Félicitations ! Votre compte gpodder.net est maintenant lié à votre appareil. AntennaPod va désormais automatiquement synchroniser vos podcasts sur votre appareil avec votre compte gpodder. + Commencer la synchronisation + Aller à l\'écran d\'accueil + Erreur d\'identification à gpodder.net + Problème d\'identifiant et/ou de mot de passe + Problème de synchronisation avec gpodder.net + Une erreur est apparue lors de la synchronisation :\u0020 + + Répertoire choisi : + Créer répertoire + Choisir le répertoire + Créer un répertoire nommé \"%1$s\" ? + Répertoire créé + Impossible d\'écrire dans ce répertoire + Le répertoire existe déjà + Impossible de créer le répertoire + Le répertoire n\'est pas vide + Le répertoire que vous avez choisi n\'est pas vide. Les fichiers téléchargés seront ajoutés à ce répertoire. Continuer malgré tout ? + Choisir le répertoire par défaut + Mettre la lecture en pause au lieu de baisser le volume quand une autre application veut jouer un son + Mettre en pause lors des interruptions + + S\'abonner + Abonné + Téléchargement en cours + + Afficher chapitres + Afficher notes d\'épisode + Afficher image + Retour en arrière + Avance rapide + Audio + Vidéo + Naviguer vers le haut + Plus d\'actions + L\'épisode est en train d\'être joué + L\'épisode est en train d\'être téléchargé + L\'épisode a été téléchargé + L\'élément est nouveau + L\'épisode est dans la liste + Nombre de nouveaux épisodes + Nombre d\'épisodes que vous avez commencé à écouter + Faire glisser pour changer la position de cet élément + + Authentification + Modifier votre identifiant et mot de passe pour ce podcast et tous ses épisodes + + Importation des abonnements à partir d\'applications à usage unique... + diff --git a/core/src/main/res/values-hi-rIN/strings.xml b/core/src/main/res/values-hi-rIN/strings.xml new file mode 100644 index 000000000..43590f62a --- /dev/null +++ b/core/src/main/res/values-hi-rIN/strings.xml @@ -0,0 +1,281 @@ + + + + \tऐन्टेनापॉड + फिड्स + पॉडकास्ट + एपिसोड + नया + वेटिंग लिस्ट + सेटिंग्स + पॉडकास्ट जोड़ें + डाउनलोड + डाउनलोड रद्द करें + प्लेबैक इतिहास + gpodder.net + gpodder.net login + + + + ब्राउज़र में खोलें + कॉपी यूआरएल + शेयर यूआरएल + यूआरएल को क्लिपबोर्ड पर कॉपी कर लिया गया है + + हिस्ट्री हटाएँ + + पुष्टि करें + रद्द करें + \tनिर्माता + भाषा + सेटिंग्स + तस्वीर + त्रुटि + एक त्रुटि हो गई: + ताज़ा करें + कोई बाहरी भंडारण उपलब्ध नहीं है.सुनिश्चित करें कि आपने बाहरी भंडारण मुहिम शुरू की है ताकि अनुप्रयोग ठीक से काम कर सकते हैं + अध्याय + नोट्स दिखाएँ + विवरण + सबसे हाल का प्रकरण:\u0020 + \u0020एपिसोड + लंबाई:\u0020 + साइज:\u0020 + प्रसंस्करण + लोड हो रहा है ... + यूज़रनेम और पासवर्ड सहेजें + बंद करें + पुन: प्रयास + ऑटो डाउनलोड में शामिल करें + + यूआरएल फ़ीड + यूआरएल द्वारा पॉडकास्ट जोड़ें + पॉडकास्ट निर्देशिका + + पढ़ने के रूप में सभी को चिह्नित करें + जानकारी दिखाएँ + पॉडकास्ट हटाएँ\n + शेयर वेबसाइट लिंक + शेयर फ़ीड लिंक + इसकी पुष्टि करें कि आप इस फ़ीड और इस फ़ीड के सभी प्रकरणों को हटाना चाहते हैं जिन्हें आपने डाउनलोड किया है. + फ़ीड निकाल रहा है + + डाउनलोड + प्ले + रोकें + स्ट्रिम + हटाएँ + पढ़ा हुआ के रूप में चिह्नित करें + ना पढ़ा हुआ के रूप में चिह्नित करें + क़तार में जोड़ें + क़तार से हटाएँ + वेबसाइट पर जाएँ + इसे Flattr करें + पंक्ति में सभी को डालें + सभी डाउनलोड + एपिसोड छोङें + + सफल\n + डाउनलोड विफल + लंबित डाउनलोड + डाउनलोड चल रहा है + स्टोरेज डिवाइस नहीं मिला + अपर्याप्त स्थान + फ़ाइल त्रुटि + एचटीटीपी डेटा त्रुटि + अज्ञात त्रुटि + पार्सर अपवाद + असमर्थित फ़ीड प्रकार + कनेक्शन त्रुटि + अज्ञात होस्ट + सभी डाउनलोड रद्द करें + डाउनलोड रद्द + डाउनलोड पूरा हो गया है + गलत URL + आईओ त्रुटि + अनुरोध त्रुटि + डेटाबेस का उपयोग त्रुटि + \u0020Downloads छोड़ा + पॉडकास्ट डेटा डाउनलोड करें + %1$d डाउनलोड सफल रहा, %2$d में विफल रहा है + अज्ञात शीर्षक + फ़ीड + मीडिया फ़ाइल + छवि + फाइल डाउनलोड करने के लिए प्रयास करते समय एक त्रुटि हुई:\u0020 + + त्रुटि! + मीडिया नहीं चल रहा + तैयार किया जा रहा है + तैयार + मांग + सर्वर निरस्त + अज्ञात त्रुटि + मीडिया नहीं चल रहा + 00:00:00 + बफरिंग + प्लेईंग पॉडकास्ट + + कतार साफ + पूर्ववत् करें + आइटम हटाया + शीर्ष पर ले जाएं + नीचे जाएं + + Flattr पंजीकरण करें + प्रमाणीकरण प्रक्रिया शुरू करने के लिए नीचे दिए गए बटन को दबाएं. आपके ब्राउज़र में flattr लॉगिन स्क्रीन को भेजा जाएगा और flattr बातें करने के लिए अनुमति AntennaPod को देने के लिए कहा जाएगा. आपकि अनुमति देने के बाद, आप स्वतः ही इस स्क्रीन में वापस आ जाएगें. + प्रामाणीकरण + होम पर लौटें + प्रमाणीकरण सफल रहा था! अब आप अनुप्रयोग के भीतर चीजों को flattr कर सकते हैं. + कोई Flattr टोकन नहीं पाया गया + आपकी flattr खाते का AntennaPod से जुड़ा होना प्रतीत नहीं होता. आप या तो AntennaPod को अपने खाते से कनेक्ट कर सकते हैं अनुप्रयोग के भीतर चीजों को flattr करने के लिए या आप इसे वहाँ flattr करने के लिए वेबसाइट पर जा सकते हैं. + प्रामाणीकरण + कार्रवाई मना + AntennaPod को इस कार्रवाई के लिए अनुमति नहीं है.इस के लिए कारण हो सकता है की आपके खाते में AntennaPod की पहुँच टोकन को निरस्त किया गया है.आप या तो फिर से प्रमाणित कर सकते हैं या बजाय किसी बात के वेबसाइट पर जा सकते हैं. + प्रवेश निरस्त किया + आपने सफलतापूर्वक अपने खाते में AntennaPod पहुँच टोकन निरस्त कर दिया है. इस प्रक्रिया को पूरा करने के लिए, आपको flattr वेबसाइट पर अपने खाते की सेटिंग्स में अनुमोदित आवेदनों की सूची से इस एप्लिकेशन को हटाना होगा. + + सफलतापूर्वक यह बात Flattr किया + सफलतापूर्वक %d बातोंको Flattr किया + Flattr गिनती: %s + ऐन्टेनापॉड Flattr + + प्लगइन डाउनलोड करें + प्लगइन स्थापित नहीं हुआ + काम करने के लिए चर गति प्लेबैक के लिए, एक तीसरी पार्टी पुस्तकालय स्थापित किया जाना चाहिए. ⏎\n⏎\nप्ले स्टोर से एक मुक्त प्लगइन डाउनलोड करने के लिए \'डाउनलोड प्लगइन\' को ठोकें⏎\n⏎इस प्लगइन का उपयोग कर पाने में कोई समस्या है तो AntennaPod जिम्मेदार नहीं है और प्लगइन मालिक को सूचित किया जाना चाहिए. + प्लेबैक गति + + इस सूची में कोई आइटम नहीं हैं. + आपने अभी तक किसी भी फ़ीड की सदस्यता नहीं ली है. + + अन्य + के बारे में + पंक्ति + सेवाएं + Flattr + प्लेबैक रोकें जब हेडफोन काट रहे हैं + प्लेबैक के पूरा होने पर अगली पंक्ति आइटम के लिए जाएँ + प्लेबैक + संजाल + अंतराल अद्यतन + फ़ीड स्वचालित रूप से ताजा कर रहे हैं जिसमें एक अंतराल निर्दिष्ट करें या उसे निष्क्रिय करें + केवल वाईफ़ाई पर मीडिया फ़ाइलें डाउनलोड करें + सतत प्लेबैक + वाईफाई मीडिया डाउनलोड करें + headphones काटना + मोबाइल अपडेट + मोबाइल डेटा कनेक्शन पर अपडेट करने की अनुमति दें + रिफ्रेशिंग + Flattr सेटिंग्स + Flattr पंजीकरण करें + App से सीधे अपनी बातें flattr करने के लिए अपने flattr खाते में प्रवेश करें. + इस app को Flattr करें + यह flattring द्वारा AntennaPod के विकास का समर्थन करें. धन्यवाद! + उपयोग रद्द + इस अनुप्रयोग के लिए अपने flattr खाते के लिए उपयोग की अनुमति रद्द करें. + यूजर इंटरफेस + थीम का चयन करें + AntennaPod का प्रकटन बदलें. + स्वचालित डाउनलोड + एपिसोड के स्वत: डाउनलोड विन्यस्त करें. + वाई-फाई फिल्टर सक्षम करें + केवल चयनित वाई-फाई नेटवर्क के लिए स्वत: डाउनलोड की अनुमति दें. + \tगुप्त एपिसोड + हलका + अंधेरा + असीमित + घंटे + घंटा + मैनुअल + लॉगिन + अपनी सदस्यता सिंक करने के क्रम में अपने gpodder.net खाते के साथ लॉगिन करें . + लॉगआउट + लॉगआउट सफल रहा था + प्रवेश जानकारी बदलें + अपने gpodder.net खाते के लिए प्रवेश जानकारी बदलें. + प्लेबैक गति + चर गति ऑडियो प्लेबैक के लिए उपलब्ध गति बनाइए + होस्टनाम सेट + डिफ़ॉल्ट होस्ट का प्रयोग करें + + + फ़ीड या एपिसोड के लिए खोज + Shownotes में मिला + अध्यायों में मिला + कोई परिणाम नहीं मिले + खोज + शीर्षक में मिला + + OPML फ़ाइलें आपको एक podcatcher से दूसरे को अपने पॉडकास्ट स्थानांतरित करने के लिए अनुमति देते हैं. + एक OPML फ़ाइल आयात करने के लिए, आपको इसे निम्नलिखित निर्देशिका में डालना है और आयात की प्रक्रिया शुरू करने के लिए नीचे दिए गए बटन को प्रेस करना है. + आयात प्रारंभ + OPML आयात + त्रुटि! + OPML फ़ाइल पढ़ना + OPML दस्तावेज़ पढ़ते समय एक त्रुटि हुई है: + आयात निर्देशिका खाली है. + सभी का चयन करें + सभी का चयन रद्द करें + आयात करने के लिए फ़ाइल चुनें + OPML निर्यात + निर्यात ... + निर्यात त्रुटि + OPML निर्यात सफल. + .ompl फ़ाइल लिखा गया था:\u0020 + + स्लीप टाइमर सेट + स्लीप टाइमर अक्षम + समय दर्ज करें + स्लीप टाइमर + समय बचा है:\u0020 + अवैध इनपुट, समय को पूर्णांक में डालें + + श्रेणियाँ + शीर्ष पॉडकास्ट + सुझाव + gpodder.net खोज + लॉगिन + Gpodder.net प्रवेश प्रक्रिया में आपका स्वागत है.पहले, अपनी प्रवेश जानकारी टाइप करें: + लॉगिन + अगर आप अभी तक कोई खाता नहीं है, तो आप एक यहाँ बना सकते हैं:⏎\nhttps://gpodder.net/register/ + प्रयोक्ता नाम + पासवर्ड + डिवाइस चयन + अपने gpodder.net खाते के उपयोग के लिए एक नई डिवाइस बनाएँ या एक मौजूदा डिवाइस का चयन करें: + डिवाइस आईडी:\u0020 + शीर्षक + नई डिवाइस बनाएँ + मौजूदा डिवाइस चुनें: + डिवाइस आईडी खाली नहीं होना चाहिए + डिवाइस आईडी पहले से ही उपयोग में + चुनें + लॉगिन सफल! + बधाई हो! आपकी gpodder.net खाता अब आपके डिवाइस के साथ जुड़ा हुआ है. AntennaPod अब से स्वचालित रूप से आपके gpodder.net खाते के साथ अपने डिवाइस पर सदस्यता सिंक जाएगा. + अब सिंक प्रारंभ करें + मुख्य स्क्रीन पर जाएं + gpodder.net प्रमाणन त्रुटि + गलत उपयोगकर्ता नाम या पासवर्ड + gpodder.net सिंक त्रुटि + एक त्रुटि सिंक्रनाइज़ के दौरान हुई:\u0020 + + चयनित फ़ोल्डर: + फ़ोल्डर बनाएँ + डेटा फ़ोल्डर चुनें + \"%1$s\" नाम के साथ नया फ़ोल्डर बनाएँ? + नया फ़ोल्डर बनाया + इस फ़ोल्डर में लिख नहीं सकते + फ़ोल्डर पहले से मौजूद है + फ़ोल्डर नहीं बना सका + फ़ोल्डर खाली नहीं है + आपके द्वारा चुने गए फ़ोल्डर खाली नहीं है. मीडिया डाउनलोड और अन्य फ़ाइलें इस फ़ोल्डर में सीधे रखा जाएगा. फिर भी जारी रखें? + डिफ़ॉल्ट फ़ोल्डर चुनें + प्लेबैक रोकें बजाय ध्वनियों को कम करने के अगर कोई अन्य अनुप्रयोग इसे बजाना चाहता है + रुकावट के लिए रोकें + + सदस्यता लें + सदस्यता ली गई + डाउनलोड कर रहा है ... + + + + diff --git a/core/src/main/res/values-it-rIT/strings.xml b/core/src/main/res/values-it-rIT/strings.xml new file mode 100644 index 000000000..9bc81c269 --- /dev/null +++ b/core/src/main/res/values-it-rIT/strings.xml @@ -0,0 +1,289 @@ + + + + AntennaPod + Feed + PODCAST + EPISODI + Nuovo + Lista d\'attesa + Impostazioni + Aggiungi podcast + Download + Annulla download + Storico delle riproduzioni + gpodder.net + gpodder.net login + + + + Apri nel browser + Copia URL + Condividi URL + URL copiato negli appunti + + Cancella lo storico + + Conferma + Annulla + Autore + Lingua + Impostazioni + Immagine + Errore + Un errore è stato rilevato: + Aggiorna + Non risulta disponibile lo spazio di archiviazione esterno. Assicurati che lo spazio di archiviazione sia montato per permettere all\'applicazione di funzionare correttamente. + Capitoli + Note dell\'episodio + Descrizione + Episodi Recenti:\u0020 + \u0020episodi + Durata:\u0020 + Dimensione:\u0020 + Elaborazione in corso + Caricamento... + Salva nome utente e password + Chiudi + Riprova + Includi nei download automatici + + URL del feed + Aggiungi un Podcast tramite URL + + Segna tutti come letti + Informazioni + Condividi il link al sito + Condividi il link al feed + Per favore conferma la cancellazione di questo feed e di TUTTI gli episodi collegati che sono stati precedentemente scaricati. + Rimozione feed + + Download + Riproduci + Pausa + Stream + Rimuovi + Segna come letto + Segna come non letto + Aggiungi alla coda + Rimuovi dalla coda + Visita il sito + Flattr this + Accoda tutti + Scarica tutti + Salta episodio + + Download in attesa + Download in corso + Spazio di archiviazione non trovato + Spazio insufficiente + Errore su file + HTTP Data Error + Errore sconosciuto + Parser Exception + Tipo di feed non supportato + Errore di connessione + Host sconosciuto + Annulla tutti i download + Download annullato + Download completati + URL malformato + IO Error + Request error + Errore di accesso al database + \u0020Download rimasti + Download podcast in corso + %1$d download con successo, %2$d ko + Titolo sconosciuto + Feed + Media file + Immagine + Rilevato errore durante il download del file:\u0020 + + Errore! + Nessun media in riproduzione + Preparazione + Pronto + Ricerca posizione + Server died + Errore sconosciuto + Nessun media in riproduzione + 00:00:00 + Buffering + Riproduzione podcast in corso + + Svuota la coda + Undo + Oggetto rimosso + Sposta all\'inizio + Sposta in fondo + + Flattr sign-in + Premi il tasto seguente per iniziare il processo di autenticazione. Sarai trasferito alla pagina di login di flattr sul tuo browser e ti sarà richiesto di garantire ad AntennaPod il permesso di effettuare microdonazioni. Dopo la tua autorizzazione, sarai riportato alla seguente schermata in modo automatico. + Autenticazione + Ritorna alla home + Autenticazione avvenuta con successo! Adesso puoi microdonare con flattr dall\'interno dell\'app. + Nessun token flattr trovato + Il tuo account flattr non sembra essere collegato ad AntennaPod. Potresti collegare il tuo account ad AntennaPod per utilizzare flattr dall\'app oppure puoi visitare il sito per utilizzare flattr direttamente da lì. + Autenticazione + Azione inibita + AntennaPod non ha il permesso di effettuare questa azione. La ragione potrebbe essere che il token di accesso di AntennaPod al tuo account è stato revocato. Puoi eseguire la re-autenticazione o altrimenti visitare il sito web. + Accesso revocato + Hai revocato l\'accesso di AntennaPod al tuo account. Al fine di completare il processo devi rimuovere l\'app dalla lista delle applicazioni autorizzare nelle impostazioni del tuo account sul sito di flattr. + + AntennaPod sta eseguendo Flattr + + Scarica Plugin + Plugin non installato + Per la riproduzione a velocità variabile deve essere installata una libreria di terze parti.\n\nPremi \'Scarica Plugin\' per scaricare un plugin gratuito dal Play Store.\n\nEventuali problemi riscontrati utilizzando questo plugin non sono da imputare ad AntennaPod e devono essere segnalati al proprietario plugin. + Velocità di riproduzione + + Non ci sono oggetti in questa lista. + Non sei ancora abbonato a nessun feed. + + Altro + Informazioni + Coda + Servizi + Flattr + Metti in pausa quanto le cuffie vengono disconnesse + Passa al prossimo episodio in coda quanto si completa una riproduzione + Riproduzione + Rete + Intervallo di update + Specifica un intervallo per l\'aggiornamento automatico dei feed o disabilitalo + Abilita il download dei media solo tramite WiFi + Playback continuo + Download dei media su WiFi + Disconnessione cuffie + Update su rete mobile + Permetti gli aggiornamenti tramite connessione dati mobile + Aggiornamento + Impostazioni Flattr + Flattr sign-in + Collega il tuo account flattr per utilizzare flattr direttamente dall\'app + Supporta con flattr questa app + Supporta lo sviluppo di AntennaPod tramite flattr. Grazie! + Revoca l\'accesso + Revoca il permesso, a questa applicazione, di accedere al tuo account flattr. + Interfaccia utente + Seleziona il tema + Cambia l\'aspetto di AntennaPod + Download automatico + Configura il download automatico degli episodi + Abilita il filtro Wi-Fi + Abilita il download automatico solo per alcune reti Wi-Fi selezionate. + Cache degli episodi + Light + Dark + Illimitato + ore + ora + Manuale + Login + Effettua il login con il tuo account gpodder.net per sincronizzare le tue sottoscrizioni. + Logout + Logout effettuato + Cambia le informazioni di login + Cambia le informazioni di login per il tuo account gpodder.net. + Velocità di riproduzione + Personalizza le velocità disponibili per la riproduzione audio a velocità variabile + Imposta l\'hostname + Usa l\'host di default + + + Ricerca per Feed o Episodi + Trovato nelle note dell\'episodio + Trovato nei capitoli + Nessun risultato trovato + Ricerca + Trovato nel titolo + + I file OPML ti permettono di spostare i tuoi podcast da un programma ad un altro. + Per importare un file OPML devi posizionarlo nella directory indicata e premere il tasto seguente in modo da iniziare il processo di importazione. + Avvio importazione + Importazione OPML + ERRORE! + Lettura OPML file in corso + Un errore è stato rilevato mentre era in corso la lettura del documento opml: + La directory di importazione è vuota. + Seleziona tutti + Deseleziona tutti + Scegli il file da importare + Esportazione su OPML + Esportazione in corso... + Errore di esportazione + Esportazione OPML avvenuta con successo. + Il file .opml è stato scritto su:\u0020 + + Imposta timer + Disabilita il timer di spegnimento + Tempo di spegnimento + Timer di spegnimento + Tempo residuo:\u0020 + Input non valido, il campo deve essere un numero intero. + + CATEGORIE + TOP PODCAST + SUGGERIMENTI + Cerca su gpodder.net + Login + Benvenuto sul processo di login di gpodder.net. Per prima cosa, inserisci le tue informazioni di login: + Login + Se non possiedi ancora un account, puoi crearlo uno qui:\nhttps://gpodder.net/register/ + Username + Password + Scelta del dispositivo + Crea un nuovo dispositivo per utilizzare il tuo account gpodder.net o scegline uno esistente: + ID del dispositivo:\u0020 + Caption + Crea un nuovo dispositivo + Scegli un dispositivo esistente: + L\'ID del dispositivo non può essere vuoto + ID di dispositivo già in uso + Scegli + Login effettuato! + Congraturazioni! Il tuo account gpodder.net è stato collegato con il tuo dispositivo. Da ora AntennaPod sincronizzerà automaticamente le sottoscrizioni sul tuo dispositivo con il tuo account gpodder.net. + Avvia la sincronizzazione + Schermata principale + gpodder.net errore di autenticazione + Utente o password errata + gpodder.net errore di sincronizzazione + Rilevato un errore in fase di sincronizzazione:\u0020 + + Seleziona la directory: + Crea una directory + Scegli la directory per i dati + Crea una nuova directory con nome \"%1$s\"? + Crea una nuova directory + Impossibile scrivere in questa directory + La directory esiste già + Impossibile creare la directory + La directory non è vuota + La directory che hai selezionato non è vuota. I download dei media e altri file saranno creati in questa directory. Continuare? + Scegli la directory predefinita + Sospendi la riproduzione invece di abbassare il volume quando un\'altra app emette un suono + Pausa su interruzione + + Abbonati + Abbonato + Download in corso... + + Mostra i capitoli + Mostra le note dell\'episodio + Mosta l\'immagine + Riavvolgi + Avanti veloce + Audio + Video + Naviga su + Più azioni + L\'episodio è in corso di ripoduzione + L\'episodio sta per essere scaricato + L\'episodio è stato scaricato + L\'oggetto è nuovo + L\'episodio è in coda + Numero dei nuovi episodi + + + diff --git a/core/src/main/res/values-iw-rIL/strings.xml b/core/src/main/res/values-iw-rIL/strings.xml new file mode 100644 index 000000000..27f4b969d --- /dev/null +++ b/core/src/main/res/values-iw-rIL/strings.xml @@ -0,0 +1,305 @@ + + + + אנטנה-פוד + הזנות + פודקאסטים + פרקים + חדש + רשימת המתנה + הגדרות + הוסף פודקאסט + הורדות + בטל הורדה + היסטוריית ניגון + gpodder.net + התחברות אל gpodder.net + + + + פתח בדפדפן + העתק כתובת אתר + שתף כתובת אתר + כתובת אתרהועתקה ללוח. + + נקה היסטוריה + + אישור + בטל + מחבר + שפה + הגדרות + תמונה + שגיאה + אירעה שגיאה: + רענן + אין אחסון חיצוני זמין. אנא ודא כי אחסון חיצוני הוא מותקן כך שהאפליקציה תוכל לעבוד כמו שצריך. + פרקים + הערות פרק + תיאור + הפרק האחרון:\u0020 + \u0020פרקים + אורך:\u0020 + גודל:\u0020 + מעבד + טוען... + שמור שם משתמש וססמה + סגור + נסה שוב + כלול בהורדות אוטומטיות + + כתובת הזנה + הוסף פודקאסט לפי כתובת אתר + + סמן הכל כנקרא + הצג מידע + שתף קישור אתר + שתף קישור הזנה + אשר מחיקת הזנה זו ואת כל פרקי ההזנה שהורדת. + הסר הזנה + + הורד + נגן + השהה + הזרם + הסר + סמן כנקרא + סמן כלא נקרא + הוסף לתור + הסר מהתור + בקר באתר + תרום באמצעות Flattr + הכנס לתור הכל + הורד הכל + דלג על הפרק + + הורדה עתידית + הורדה מתבצעת + התקן איחסון לא נמצא + אין די שטח איחסון + שגיאת קובץ + שגיאת מידע HTTP + שגיאה לא ידועה + שגיאת תוכנית ניתוח + סוג ההזנה אינו נתמך + שגיאת חיבור + שרת לא ידוע + שגיאת אימות + בטל את כל ההורדות + הורדה בוטלה + הורדות הושלמו + כתובת אתר שגויה + שגיאת קלט פלט + שגיאת בקשה + שגיאת גישה למסד הנתונים + הורדות נותרוu0020\ + מוריד פודקאסט + %1$d הורדות הצליחו, %2$d ניכשלו + כותרת לא ידועה + הזנה + קובץ מדיה + תמונה + שגיאה אירעה בעת הניסיון הורדת הקובץ:\u0020 + נידרש אימות + המשאב אותה ביקשת דורש שם משתמש וססמה + + שגיאה! + מדיה לא מתנגנת + מתכונן + מוכן + מחפש + שרת מת + שגיאה לא ידועה + מדיה לא מתנגנת + 00:00:00 + ממלא חוצץ + מנגן פודקאסט + + נקה תור + בטל + הסר פריט + העבר למעלה + העבר למטה + + כניסה ל-Fattr + לחץ על הכפתור למטה כדי להתחיל את תהליך האימות. אתה תועבר למסך כניסת flattr בדפדפן שלך ותתבקש לתת לאנטנה-פוד רשות לתרום באמצעות flattr. לאחר שקבלת אישור, תוכל לחזור למסך זה באופן אוטומטי. + אימות + חזור למסך הבית + האימות הצליח! עכשיו אתה יכול לתרום באמצעות flattr מתוך האפליקציה. + אסימון flattr לא נמצא + חשבון ה-flattr שלך אינו מחובר לאנטנה-פוד. אתה יכול לקשראת לחשבונך לאנטנה-פוד לתרום באמצעות flattr מתוך האפליקציה או שאתה יכול לבקר באתר האינטרנט של הדבר לו תרצה לתרום. + אמת + הפעולה אסורה + לאנטנה-פוד אין הרשאה לפעולה זו. הסיבה לכך יכולה להיות שאסימון הגישה של אנטנה-פוד לחשבון שלך בוטל. אתה יכול לבצע אימות מחדש או לבקר באתר האינטרנט של הדבר במקום. + גישה בוטלה + אסימון הגישה של אנטנה-פוד לחשבונך בוטל. על מנת להשלים את התהליך, אתה צריך להסיר יישום זה מהרשימת היישומים שאושרו בהגדרות החשבונך באתר flattr. + + תרמת ב-Flattr! + תרמת ב-Flattr %d פעמים! + תרומות Flattr: %s. + כישלון לתרום ב-Flattr %d! + לא נתרם ב-Flattr: %s. + תרומות ב-Flattr מאוחר יותר + תורם ב-Flattr %s + אנטנה-פוד תורם ב-Flattr + אנטנה-פוד תרם ב-Flattr + כישלון תרומת אנטנה-פוד ב-Flattr + איחזור תרומות Flattr + + הורד תוסף + תוסף לא מותקן + לניגון במהירות משתנה תוסף מגורם שלישי צריך להיות מותקן. \n\nהקש על \'הורד תוסף\' להוריד תוסף חינמי מחנות Play\n\nבעיות בשימוש עם תוסף זה אינן באחריות אנטנה-פוד וצריך לדווחן ליוצר התוסף. + מהירויות ניגון + + אין פריטים ברשימה זו. + לא נרשמת עדיין להזנות. + + אחר + אודות + תור + שירותים + Flattr + השהה השמעה בניתוק האוזניות + עבור לפריט הבא בתור כאשר הניגון מסתיים + ניגון + רשת + זמן בין עידכונים + ציין פרק זמן שבו ההזנות עוברות רענון באופן אוטומטי או לבטל ריענון + הורד קבצי מדיה רק דרך חיבור אינטרנט אלחוטי + ניגון מתמשך + הורדת מדיה דרך אינטרנט אלחוטי + ניתוק אוזניות + עידכון דרך רשת סלולרית + אפשר עידכונים דרך רשת סלולרית + מרענן + הגדרות Flattr + כניסה ל-Fattr + היכנס לחשבון שלך לflattr לתרום ישירות מתוך האפליקציה. + תרום באמצעות Flattr לאפליקציה זו + תמוך בפיתוח אנטנה-פוד בתרומה עם Flattr. תודה! + בטל גישה + בטל הרשאת גישה לחשבון flattr ליישום זה. + תרומות Flattr אוטומטיות + ממשק משתמש + בחר ערכת נושא + שנה את מראה אנטנה-פוד + הורדה אוטומטית + הגדר הורדה אטומטית של פרקים. + אפשר סינון אינטרנט אלחוטי + אפשר הורדה אוטומטית דרך רשתות אלחוטייות נבחרות. + מטמון פרקים + בהיר + כהה + בלתי מוגבל + שעות + שעה + ידני + כניסה + כנס עם חשבון gpodder.net שלך על מנת לסנכרן את ההרשמות שלך. + התנתקות + ההתנתקות הייתה מוצלחת + שינוי פרטי התחברות + שנה פרטי התחברות של חשבון gpodder.net. + מהירויות ניגון + התאמת המהיריות הזמינות לניגון במהירות משתנה + הגדר שם שרת + השתמש בשרת ברירת מידל + + + חפש הזנות או פרקים + נמצא בהערות פרק + נמצא בפרקים + אין תוצאות + חיפוש + נמצא בכותרת + + קבצי OPML יאפשרו לכך לנייד פודקאסטים מלוכד פודקאסטים אחד למשנו. + לייבא קובץ OPML, אתה צריך למקם אותו בספרייה הבאה וללחוץ על הכפתור למטה כדי להתחיל את תהליך היבוא. + התחל יבוא + יבוא OPML + שגיאה! + קורא קובץ OPML + אירעה שגיאה בזמן קריאת קובץ OPML: + ספריית היבוא ריקה. + בחר הכל + בטל בחירות + בחר קובץ ליבוא + יצוא OPML + מייצא... + שגיאת יצוא + יצוא OPML הצליח. + קובץ OPML נכתב ל:\u0020 + + קבע טיימר שינה + בטל טיימר שינה + קבע זמן + טיימר שינה + זמן נותר:\u0020 + קלט לא חוקי, זמן חייב להיות מספר שלם + + קטגוריות + פודקאסטים בכירים + המלצות + חפש ב-gpodder.net + התחברות + ברוך הבא להתחברות ל-gpodder.net. ראשית, הקלד את פרטי הכניסה שלך: + התחברות + אם אין לך עדיין חשבון, אתה יכול ליצור אחד כאן:\nhttps://gpodder.net/register/ + שם משתמש: + ססמה: + בחירת מכשיר + צור מכשיר חדש לשימוש עבור חשבון gpodder.net או לבחר אחד קיים: + מזהה מכשיר:\u0020 + כותרת + צור מכשיר חדש + בחר מכשיר קיים: + מזהה המכשיר אינו יכול להיות ריק + מזהה המכשיר בשימוש + בחר + התחברות מוצלחת! + מזל טוב! חשבון gpodder.net שלך מקושר כעת עם המכשיר שלך. אנטנה-פוד מעתה יסנכרן באופן אוטומטי הרשמות במכשיר שלך עם חשבון gpodder.net שלך. + התחל סנכרון כעת + עבור למסך הראשי + שגיאת אימות של gpodder.net + שם משתמש או ססמה שגויים + שגיאת סנכרון של gpodder.net + שגיאה במהל סינכרון:\u0020 + + תיקיה נבחרת: + צור תיקיה + בחר תיקיית מידע + צור תיקיה חדשה בשם \"%1$s\"? + תיקיה חדשה נוצרה + לא ניתן לכתוב לתיקה זו + תיקה כבר קיימת + לא ניתן ליצור תיקיה + התיקיה אינה ריקה + התיקייה שבחרת אינה ריקה. הורדות מדיה וקבצים אחרים יהיו ממוקמות ישירות בתיקייה זו. להמשיך בכל זאת? + בחר תיקיית ברירת מחדל + השהה ניגון במקום החלשת עוצמת שמע כשאפליקציה אחרת מנגנת + השהה בזמן הפרעה + + הרשם + נרשם + מוריד... + + הצג פרקים + הצג הערות פרק + הצג תמונה + הרץ לאחור + הרץ קדימה + שמע + וידאו + נווט למעלה + עוד פעולות + הפרק מתנגן + הפרק יורד + הפרק ירד + פריט חדש + הפרק בתור + מספר הפרקים החדשים + מספר הפרקים שהתחלת להאזין להם + + + מייבא רישום מאפליקציות יעודיות... + diff --git a/core/src/main/res/values-ko/strings.xml b/core/src/main/res/values-ko/strings.xml new file mode 100644 index 000000000..4d783b83a --- /dev/null +++ b/core/src/main/res/values-ko/strings.xml @@ -0,0 +1,305 @@ + + + + 안테나팟 + 피드 + 팟캐스트 + 에피소드 + 신규 + 추가 대기 목록 + 설정 + 팟캐스트 추가 + 다운로드 + 다운로드 취소 + 재생 기록 + gpodder.net + gpodder.net 로그인 + + + + 브라우저에서 열기 + URL 복사 + URL 공유 + URL을 클립보드에 복사했습니다. + + 기록 지우기 + + 확인 + 취소 + 저자 + 언어 + 설정 + 그림 + 오류 + 오류가 발생했습니다: + 새로 고침 + 외부 저장 장치가 없습니다. 앱이 제대로 동작하려면 외부 저장장치를 마운트하십시오. + 챕터 + 프로그램 메모 + 설명 + 가장 최근 에피소드:\u0020 + \u0020에피소드 + 길이:\u0020 + 크기:\u0020 + 처리 중 + 읽어들이는 중... + 사용자 이름 및 암호 저장 + 닫기 + 다시 시도 + 자동 다운로드에 포함 + + 피드 URL + URL로 팟캐스트를 추가 + + 모두 읽은 것으로 표시 + 정보 표시 + 홈페이지 링크 공유 + 피드 링크 공유 + 이 피드와 이 피드에서 다운로드한 모든 에피소드를 삭제하시려면 확인을 누르십시오. + 피드 삭제하는 중 + + 다운로드 + 재생 + 일시 중지 + 스트리밍 + 제거 + 읽은 것으로 표시 + 읽지 않은 것으로 표시 + 대기열에 추가 + 대기열에서 제거 + 홈페이지 보기 + Flattr하기 + 모두 대기열에 추가 + 모두 다운로드 + 에피소드 건너뛰기 + + 다운로드 지연 중 + 다운로드 실행 중 + 저장 장치가 없습니다 + 저장 공간이 부족합니다 + 파일 오류 + HTTP 데이터 오류 + 알 수 없는 오류 + 파서 프로그램 예외 + 지원하지 않는 피드 종류 + 연결 오류 + 알 수 없는 호스트 + 인증 오류 + 모든 다운로드 취소 + 다운로드 취소됨 + 다운로드 마침 + URL 형식 틀림 + 입출력 오류 + 요청 오류 + 데이터베이스 접근 오류 + 개\u0020다운로드 남음 + 팟캐스트 데이터 다운로드 중 + 다운로드 %1$d개 성공, %2$d개 실패 + 알 수 없는 제목 + 피드 + 미디어 파일 + 그림 + 파일을 다운로드하는 중 오류가 발생했습니다:\u0020 + 인증이 필요합니다 + 요청한 자원은 사용자 이름과 암호가 필요합니다 + + 오류! + 재생 중인 미디어 없음 + 준비하는 중 + 준비 완료 + 이동 중 + 서버가 죽었습니다 + 알 수 없는 오류 + 재생 중인 미디어 없음 + 00:00:00 + 버퍼링 중 + 팟캐스트 재생 중 + + 대기열 지우기 + 실행 취소 + 항목을 제거했습니다 + 맨 위로 이동 + 맨 아래로 이동 + + Flattr 로그인 + 인증 절차를 시작하려면 아래 버튼을 누르십시오. 브라우저의 Flattr 로그인 화면으로 이동하고, 안테나팟에 Flattr를 사용을 허락 여부를 물어봅니다. 허락을 하면 자동으로 이 화면으로 돌아옵니다. + 인증 + 홈으로 돌아가기 + 인증이 성공했습니다! 이제 앱에서 Flattr 기능을 사용할 수 있습니다. + Flattr 토큰이 없습니다 + Flattr 계정이 안테나팟에 연결되지 않은 것 같습니다. 앱 안에서 안테나팟을 Flattr 계정에 연결할 수도 있고, Flattr 홈페이지에서 Flattr할 거리를 선택할 수 있습니다. + 인증 + 금지된 동작입니다 + 안테나팟에 이 동작을 할 권한이 없습니다. 가능한 원인은 안테나팟이 계정에 접근할 때 사용하는 토큰이 철회된 경우입니다. 다시 인증할 수도 있고, 직접 웹페이지를 이용할 수도 있습니다. + 접근이 철회되었습니다 + 안테나팟에서 계정에 대한 접근 토큰을 성공적으로 철회했습니다. 절차를 마치려면 Flattr 홈페이지, 계정 설정의 허용하는 응용 프로그램 목록에서 이 앱을 제거해야 합니다. + + 1개 Flattr했습니다! + %d개 Flattr했습니다! + Flattr함: %s + %d개 Flattr하는데 실패했습니다! + Flattr하지 않음: %s. + 나중에 Flattr합니다 + %s Flattr하는 중 + 안테나팟에서 Flattr하는 중 + 안테나팟에서 Flattr했음 + 안테나팟에서 Flattr 실패 + Flattr한 내용 가져오는 중 + + 다운로드 플러그인 + 플러그인을 설치하지 않았습니다 + 여러가지 속도로 재생하려면 외부 라이브러리를 설치해야 합니다.\n\n플레이 스토어에서 무료 플러그인을 설치하려면 \"플러그인 다운로드\"를 누르십시오.\n\n이 플러그인에서 발생하는 문제는 안테나팟의 책임이 아니므로 플러그인 개발자에게 문의하십시오. + 재생 속도 + + 이 목록에 항목이 없습니다. + 아직 어떤 피드도 구독하지 않았습니다. + + 기타 + 정보 + 대기열 + 서비스 + Flattr + 헤드폰의 연결이 끊어졌을 때 재생을 일시 중지 + 재생을 마쳤을 때 다음 대기열로 이동 + 재생 + 네트워크 + 업데이트 주기 + 피드를 새로 고칠 주기를 지정하거나 새로 고침을 하지 않음 + Wi-Fi를 통해서만 미디어 파일 다운로드 + 연속 재생 + Wi-Fi 미디어 다운로드 + 헤드폰 연결 끊김 + 휴대전화망 업데이트 + 휴대전화 데이터 연결을 통해 업데이트 허용 + 새로 고치는 중 + Flattr 설정 + Flattr 로그인 + Flattr 계정에 로그인하면 앱에서 직접 Flattr할 수 있습니다. + 이 앱 Flattr하기 + Flattr해서 안테나팟 개발을 지원할 수 있습니다. 고맙습니다! + 접근 철회 + 이 앱이 Flattr 계정에 접근할 권한을 철회합니다. + 자동 Flattr + 사용자 인터페이스 + 테마 선택 + 안테나팟의 겉모양을 바꿉니다. + 자동 다운로드 + 에피소드 자동 다운로드를 설정합니다. + Wi-Fi 필터 사용 + 선택한 Wi-Fi 네트워크에 대해서만 자동 다운로드를 허용합니다. + 에피소드 임시 저장 + 밝게 + 어둡게 + 무제한 + 시간 + 시간 + 수동 지정 + 로그인 + gpodder.net 계정으로 로그인해서 구독 정보를 동기화 + 로그아웃 + 로그아웃 성공 + 로그인 정보 바꾸기 + gpodder.net 계정의 로그인 정보를 바꿉니다. + 재생 속도 + 여러가지 오디오 재생 속도 직접 설정 + 호스트 이름 설정 + 기본 호스트 사용 + + + 피드나 에피소드 검색 + 프로그램 메모에서 발견 + 챕터에서 발견 + 검색 결과가 없습니다 + 검색 + 제목에서 발견 + + OPML 파일을 이용하면 팟캐스트 목록을 한 팟캐스트 프로그램에서 다른 팟캐스트 프로그램으로 옮길 수 있습니다. + OPML 파일을 가져오려면, 다음 디렉터리에 파일을 저장하고 아래 버튼을 누르면 가져오기 처리를 시작합니다. + 가져오기 시작 + OPML 가져오기 + 오류! + OPML 파일을 읽는 중 + OPML 문서를 읽는 중 오류가 발생했습니다: + 가져오기 디렉터리가 비어 있습니다. + 모두 선택 + 모두 선택 해제 + 가져올 파일을 고르십시오 + OPML 내보내기 + 내보내는 중... + 내보내기 오류 + OPML 내보내기가 성공했습니다. + OPML 파일을 다음에 저장했습니다:\u0020 + + 취침 타이머 설정 + 취침 타이머 사용 않음 + 시간 입력 + 취침 타이머 + 남은 시간:\u0020 + 입력이 잘못되었습니다. 시간으로 숫자를 입력해야 합니다. + + 분류 + 상위 팟캐스트 + 추천 + gpodder.net 검색 + 로그인 + gpodder.net 로그인입니다. 먼저 로그인 정보를 입력하십시오: + 로그인 + 아직 계정이 없으면, 다음 사이트에서 만들 수 있습니다:\nhttps://gpodder.net/register/ + 사용자 이름 + 암호 + 장치 선택 + gpodder.net 계정에서 사용할 장치를 새로 만들거나 기존 장치를 선택하십시오: + 장치 아이디:\u0020 + 설명 + 새 장치 만들기 + 기존 장치 선택: + 장치 ID는 비어 있으면 안 됩니다 + 장치 ID를 이미 사용 중입니다 + 선택 + 로그인이 성공했습니다! + 축하합니다! gpodder.net 계정이 장치와 연결되었습니다. 이제 안테나팟에서 gpodder.net 계정의 구독 정보와 자동으로 동기화합니다. + 지금 동기화 시작 + 메인 화면으로 이동 + gpodder.net 인증 오류 + 잘못된 사용자 이름 또는 암호 + gpodder.net 동기화 오류 + 동기화 중에 오류가 발생했습니다:\u0020 + + 선택한 폴더: + 폴더 만들기 + 데이터 폴더 선택 + 이름이 \"%1$s\"인 폴더를 만드시겠습니까? + 새 폴더를 만들었습니다 + 이 폴더에 쓸 수 없습니다 + 폴더가 이미 있습니다 + 폴더를 만들 수 없습니다 + 폴더가 비어 있지 않습니다 + 선택한 폴더가 비어 있지 않습니다. 다운로드한 미디어 파일 및 기타 파일이 이 폴더에 저장됩니다. 그래도 계속 하시겠습니까? + 기본 폴더 선택 + 다른 앱이 소리를 낼 때 볼륨을 줄이지 않고 재생을 일시 중지 + 끼어들면 일시 중지 + + 구독 + 구독함 + 다운로드하는 중... + + 챕터 보이기 + 프로그램 메모 표시 + 그림 보이기 + 뒤로 감기 + 앞으로 감기 + 오디오 + 비디오 + 위 단계로 이동 + 기타 동작 + 에피소드를 재생하는 중입니다 + 에피소드를 다운로드하는 중입니다 + 에피소드를 다운로드했습니다 + 새로운 항목입니다 + 에피소드가 대기열에 들어 있습니다 + 새 에피소드 개수 + 듣기를 시작한 에피소드 개수 + + + 단일 용도 앱에서 구독 정보를 가져옵니다... + diff --git a/core/src/main/res/values-land/styles.xml b/core/src/main/res/values-land/styles.xml new file mode 100644 index 000000000..d964ef3d4 --- /dev/null +++ b/core/src/main/res/values-land/styles.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/core/src/main/res/values-large/dimens.xml b/core/src/main/res/values-large/dimens.xml new file mode 100644 index 000000000..27b4868c7 --- /dev/null +++ b/core/src/main/res/values-large/dimens.xml @@ -0,0 +1,8 @@ + + + + 170dp + 80dp + 80dp + @dimen/text_size_medium + \ No newline at end of file diff --git a/core/src/main/res/values-nl/strings.xml b/core/src/main/res/values-nl/strings.xml new file mode 100644 index 000000000..a0c852059 --- /dev/null +++ b/core/src/main/res/values-nl/strings.xml @@ -0,0 +1,305 @@ + + + + AntennaPod + Feeds + PODCASTS + AFLEVERINGEN + Nieuw + Wachtlijst + Instellingen + Podcast toevoegen + Downloads + Annuleer download + Afspeelgeschiedenis + gpodder.net + gpodder.net login + + + + In de browser openen + URL kopieren + URL delen + URL naar klembord gekopieerd. + + Geschiedenis wissen + + Bevestig + Annuleer + Auteur + Taal + Instellingen + Beeld + Fout + Er is een fout opgetreden: + Verversen + Geen externe opslag beschikbaar. Zorg ervoor dat de externe opslag gemonteerd is, zodat de app goed kan werken. + Hoofdstukken + Shownotes + Beschrijving + Meest recente aflevering:\u0020 + \u0020afleveringen + Lengte:\u0020 + Grootte:\u0020 + Aan het verwerken + Laden... + Gebruikersnaam en wachtwoord opslaan + Sluiten + Opnieuw proberen + Voor het automatisch downloaden beschouwen + + Feed URL + Podcast toevoegen bij URL + + Alles als gelezen markeren + Toon informatie + Website link delen + Feed link delen + Bevestig dat u deze feed en ALLE afleveringen van deze feed die u hebt gedownload wilt verwijderen. + Feed verwijderen + + Download + Spelen + Pauze + Stream + Verwijderen + Als gelezen markeren + Als ongelezen markeren + Voeg toe aan wachtrij + Verwijder van wachtrij + Website bezoeken + Flattr dit + Alle in wachtrij plaatsen + Alles downloaden + Aflevering overslaan + + Download in afwachting + Aan het downloaden + Opslagmedium niet gevonden + Onvoldoende ruimte + Bestandsfout + HTTP data fout + Onbekende fout + Parser Exception + Niet ondersteunde feed soort + Verbindingsfout + Onbekende host + Authenticatie fout + Alle downloads annuleren + Download geannuleerd + Downloads afgerond + Misvormde URL + IO fout + Fout in de aanvraag + Databasetoegangsfout + Nog \u0020 downloads + Podcast gegevens aan het downloaden + %1$d downloads geslaagd, %2$d mislukt + Onbekende titel + Feed + Mediabestand + Beeld + Er is een fout opgetreden bij het ​​downloaden van bestand:\u0020 + Authenticatie vereist + De opgevraagde bron vereist een gebruikersnaam en een wachtwoord + + Fout! + Geen media aan het afspelen + Voorbereiding + Klaar + Aan het opzoeken + Server antwoord niet + Onbekende fout + Geen media aan het afspelen + 00:00:00 + Buffering + Podcast aan het afspelen + + Wachtrij leeg maken + Ongedaan maken + Item verwijderd + Naar boven verplaatsen + Naar beneden verplaatsen + + Flattr inloggen + Druk op onderstaande knop om het verificatieproces te starten. U wordt doorgestuurd naar de Flattr inlogscherm in uw browser en wordt gevraagd om toestemming aan AntennaPod te geven om dingen te Flattr\'en. Nadat u toestemming hebt gegeven, keert u automatisch terug naar dit scherm. + Authenticeren + Terug naar de startscherm + Authenticatie is geslaagd! U kunt nu dingen vanuit de app Flattr\'en. + Geen Flattr token gevonden + Uw Flattr account lijkt niet aangesloten te zijn op AntennaPod. U kunt uw account aan AntennaPod sluiten om dingen vanuit de app te Flattr\'en, of u kunt op de website van het ding terecht om het daar te Flattr\'en. + Authenticeren + Actie verboden + AntennaPod heeft geen toestemming voor deze actie. De reden hiervoor zou kunnen zijn dat de toegang token van AntennaPod voor uw account ingetrokken is. U kunt opnieuw authenticeren, of de website van het ding bezoeken. + Toegang ingetrokken + U heeft met succes het toegangstoken van AntennaPod tot uw account ingetrokken. Om het proces te voltooien, moet u deze app uit de lijst van goedgekeurde applicaties in uw accountinstellingen op de Flattr website verwijderen. + + Een ding geflattr\'d + %d dingen geflattr\'d! + Geflattr\'d: %s. + Kon %d dingen niet flattr\'n! + Niet geflattr\'d: %s. + Ding wordt later geflattr\'d + %s aan het flattren + AntennaPod is aan het flattren + AntennaPod heeft geflattr\'d + AntennaPod flattr niet gelukt + Geflattr\'de dingen aan het ontvangen + + Plugin downloaden + Plugin niet geinstalleerd + Voor variabele afspeelsnelheid moet er een derde partij bibliotheek geïnstalleerd worden.\n\nTik op \'Plugin downloaden\' om een ​​gratis plugin te downloaden uit de Play Store.\n\nEventuele problemen gevonden door het gebruik van deze plugin zijn niet de verantwoordelijkheid van AntennaPod en moeten aan de plugin ontwikkelaar gemeld worden. + Afspeelsnelheden + + Er zijn geen items in deze lijst. + U bent nog tot geen enkele feed geabonneerd. + + Overig + Over AntennaPod + Wachtrij + Services + Flattr + Afspelen pauzeren wanneer de hoofdtelefoon wordt losgekoppeld + Volgende wachtrij item afspelen als de episode voltooid is + Afspelen + Netwerk + Update interval + Voer een tijdsinterval in waarin de feeds automatisch worden vernieuwd, of schakel het uit + Download mediabestanden alleen via WiFi + Continu afspelen + WiFi download van media + Loskoppeling van de hoofdtelefoon + Mobiele updates + Updates toestaan ​​via de mobiele dataverbinding + Aan het verversen + Flattr settings + Flattr inlog + Log in je Flattr account om dingen rechtstreeks vanuit de app te flattr\'en. + Flattr deze app + Ondersteun de ontwikkeling van AntennaPod door het te flattr\'en. Bedankt! + Toegang intrekken + Trek de toegang van deze app in tot je Flattr account. + Automatische Flattr + User Interface + Kies theme + Verander het uiterlijk van AntennaPod. + Automatisch downloaden + Configureer het automatisch downloaden van afleveringen. + Wi-Fi filter inschakelen + Automatisch downloaden alleen toestaan voor geselecteerde Wi-Fi-netwerken. + Afleveringen cache + Licht + Donker + Onbeperkt + uren + uur + Handmatig + Log in + Log met je gpodder.net account in om je abonnementen te synchroniseren. + Log uit + Uitlog was succesvol + Aanmeldingsgegevens wijzigen + Wijzig de aanmeldingsgegevens van je gpodder.net account. + Afspeelsnelheden + Pas de beschikbare snelheden aan voor de variabele audio afspeelsnelheid + Definieer hostname + Gebruik standaard host + + + Feeds of afleveringen zoeken + Gevonden in de shownotes + Gevonden in hoofdstukken + Er zijn geen resultaten gevonden + Zoeken + Gevonden in de titel + + Met OPML-bestanden kan je podcasts van de ene naar de andere podcatcher verplaatsen. + Om een OPML-bestand te importeren moet je het in de volgende map zetten en op onderstaande knop drukken. + Start importeren + OPML import + FOUT! + OPML-bestand aan het lezen + Er is een fout opgetreden bij het lezen van het OPML-bestand: + De import map is leeg. + Selecteer alles + Deselecteer alles + Kies het te importeren bestand + OPML export + Aan het exporteren... + Export fout + OPML export succesvol. + Het OPML-bestand is in \u0020 geplaatst + + Slaap timer instellen + Slaap timer uitschakelen + Voer tijd in + Slaap timer + Resterende tijd:\u0020 + Ongeldige invoer, de tijd moet een geheel getal zijn + + CATEGORIEËN + TOP PODCASTS + SUGGESTIES + Zoek gpodder.net + Log in + Welkom op de gpodder.net login proces. Eerst, typ je login gegevens: + Log in + Als je nog geen account hebt, kun je er hier een aanmaken:\n https://gpodder.net/register/ + Gebruikersnaam + Wachtwoord + Apparaatselectie + Maak een nieuw apparaat aan om voor je gpodder.net account te gebruiken of kies een bestaande: + Device ID:\u0020 + Titel + Maak een nieuw apparaat aan + Kies een bestaand apparaat: + Apparaat ID mag niet leeg zijn + Apparaat ID al in gebruik + Kies + Login succesvol + Gefeliciteerd! Jou gpodder.net account is nu verbonden met je apparaat. AntennaPod zal voortaan abonnementen op je apparaat automatisch met je gpodder.net account synchroniseren. + Synchronisatie nu starten + Terug naar hoofdscherm + gpodder.net authenticatie fout + Ongeldig gebruikersnaam of wachtwoord + gpodder.net synchronisatie fout + Er is een fout opgetreden tijdens het synchroniseren:\u0020 + + Geselecteerde map: + Map aanmaken + Kies data map + Maak een nieuwe map aan met de naam \"%1$s\"? + Nieuwe map aangemaakt + Kan in deze map niet schrijven + Map bestaat al + Kon map niet aanmaken + Map is niet leeg + De map die je hebt gekozen is niet leeg. Media downloads en andere bestanden zullen rechtstreeks in deze map geplaatst worden. Toch doorgaan? + Kies default map + Het afspelen onderbreken in plaats van het volume te verlagen wanneer er een andere app geluiden af wilt spelen + Pauze voor onderbrekingen + + Abonneren + Geabonneerd + Aan het downloaden + + Hoofdstukken tonen + Shownotes tonen + Beeld tonen + Terugspoelen + Vooruitspoelen + Audio + Video + Navigeer naar boven + Meer acties + Aflevering wordt gespeeld + Aflevering wordt gedownload + Aflevering is gedownload + Item is nieuw + Aflevering is in de queue + Aantal nieuwe afleveringen + Aantal afleveringen dat begonnen te luisteren zijn + + + Abonnementen aan het importeren vanuit single-purpose apps... + diff --git a/core/src/main/res/values-pl-rPL/strings.xml b/core/src/main/res/values-pl-rPL/strings.xml new file mode 100644 index 000000000..fc56ab6bf --- /dev/null +++ b/core/src/main/res/values-pl-rPL/strings.xml @@ -0,0 +1,330 @@ + + + + AntennaPod + Kanały + Dodaj podcast + PODCASTY + ODCINKI + Nowe odcinki + Wszystkie odcinki + Nowy + Lista oczekujących + Ustawienia + Dodaj podcast + Pobrane + W toku + Ukończone + Dziennik + Anuluj pobieranie + Historia odtwarzania + gpodder.net + gpodder.net login + + Ostatnio opublikowane + Pokaż tylko nowe odcinki + + Otwórz menu + Zamknij menu + + Otwórz w przeglądarce + Kopiuj adres + Udostępnij adres + Skopiowano adres do schowka. + + Wyczyść historię + + Potwierdź + Anuluj + Autor + Język + Ustawienia + Obraz + Błąd + Wystąpił błąd: + Odśwież + Brak zewnętrznej pamięci. Sprawdź czy jest ona podłączona żeby aplikacja mogła pracować poprawnie. + Rozdziały + Opis odcinka + Opis + Najnowszy odcinek:\u0020 + :\u0020odcinków + Długość:\u0020 + Rozmiar:\u0020 + Przetwarzanie + Ładowanie... + Zapisz nazwę użytkownika i hasło + Zamknij + Spróbuj ponownie + Dołącz do automatycznego pobierania + + Adres kanału + Dodaj podcast przez adres + Znajdź podcast w folderze + Możesz wyszukiwać nowe podcasty ze względu na nazwę, kategorię lub popularność na gpodder.net + Przeglądaj gpodder.net + + Oznacz wszystkie jako przeczytane + Wszystkie odcinki zaznaczone jako przeczytane + Pokaż informacje + Usuń podcast + Udostępnij stronę + Udostępnij kanał + Potwierdź chęć usunięcia tego kanału wraz ze WSZYSTKIMI odcinkami, które zostały pobrane. + Usuwanie kanału + + Pobierz + Odtwórz + Pauza + Strumień + Usuń + Usuń odcinek + Oznacz jako przeczytane + Oznacz jako nieprzeczytane + Dodaj do kolejki + Usuń z kolejki + Odwiedź stronę + Wspomóż na Flattr + Dodaj wszystko do kolejki + Pobierz wszystkie + Pomiń odcinek + + Operacja zakończona sukcesem + Operacja nie powiodła się + Pobieranie w toku + Pobieram + Nie znaleziono urządzenia docelowego + Niewystarczająca ilość pamięci + Błąd pliku + Błąd danych HTTP + Nieznany błąd + Wyjątek parsera + Nieobsługiwany typ kanału + Błąd połączenia + Nieznany host + Błąd autoryzacji + Anuluj wszystkie pobierania + Pobieranie anulowane + Pobieranie ukończone + Niepoprawny adres + Błąd wejścia/wyjścia + Błąd żądania + Błąd dostępu do bazy danych + :\u0020pobrań pozostało + Przetwarzanie pobranych + Pobieranie danych podcastu + %1$d pobierania poprawne, %2$d nieudane + Nieznany tytuł + Kanał + Plik multimedialny + Obraz + Wystąpił błąd przy próbie pobierania:\u0020 + Wymagana autoryzacja + Żądany zasób wymaga podania nazwy użytkownika oraz hasła + + Błąd! + Żadne media nie odtwarzane + Przygotowuję + Gotowe + Szukam + Serwer zdechł + Nieznany błąd + Żadne media nie odtwarzane + 00:00:00 + Buferowanie + Odtwarzenie podcastu + + Wyczyść kolejkę + Cofnij + Element usunięty + Przesuń na górę + Przesuń na dół + + Logowanie do Flattr + Naciśnij przycisk poniżej by zacząć proces autoryzacji. Zostaniesz przekierowany na stronę logowania do flattr w przeglądarce i zostaniesz poproszony o przyznanie zezwolenia AntennaPod-owi na flattr-owanie. Po daniu zezwolenia powrócisz do tej strony automatycznie. + Autoryzacja + Wróć do ekranu głównego + Autoryzacja się powiodła. Możesz teraz używać flattr w aplikacji. + Nie znaleziono tokenu Flattr + Twoje konto Flattr wydaje się nie być podłączone do AntennaPod. Możesz połączyć konto do AntennaPod by przez program flattr-ować lub możesz odwiedzić stronę wątku by zrobić to tam. + Autoryzuj + Akcja zabroniona + AntennaPod nie ma zezwolenia na tą akcję. Powodem może być fakt iż dostęp dla AntennaPod do Twojego konta został cofnięty. Możesz ponownie autoryzować aplikację lub odwiedzić stronę. + Anulowano dostęp + Odwołałeś dostęp AntennaPod do swojego konta. W celu zakończenia procesu musisz usunąć aplikację z listy aplikacji dozwolonych na koncie Flattr. + + Poprawnie z-flattr-owano + Z-flattr-owano %d elementów + Z-flattr-owano: %s + Flattr-owanie %d elementów nie powiodło się + Flattr-owanie zakończone niepowodzeniem: %s + Elementy zostaną z-flattr-owane później + Flattr-owanie %s + Flattr-uję + AntennaPod z-flattr-owała + Flattr-owanie AntennaPod nie powiodło się + Wyszukiwanie z-flattr-owanych elementów + + Pobierz wtyczkę + Wtyczka nie zainstalowana + Do odtwarzania ze zmienną prędkością jest potrzebna biblioteka innej firmy. \n\nDotknij przycisku \"Pobierz wtyczkę\", aby pobrać darmową wtyczkę ze sklepu\n\nWszelkie znalezione za pomocą tej wtyczki problemy nie są odpowiedzialnością AntennaPod i należy zgłosić się do właściciela plugin. + Prędkość odtwarzania + + Brak elementów na tej liście. + Nie subskrybowałeś jeszcze żadnego kanału. + + Inne + O... + Kolejka + Usługi + Flattr + Wstrzymaj odtwarzanie kiedy słuchawki zostaną odłączone + Przeskocz do następnego elementu kolejki po zakończeniu odtwarzania + Odtwarzanie + Sieć + Częstość aktualizacji + Określ częstotliwość automatycznego odświeżania lub je wyłącz + Pobieraj pliki tylko przez WiFi + Odtwarzanie ciągłe + WiFi media pobrane + Słuchawki odłączone + Aktualizacje mobilne + Zezwól na aktualizacje poprzez sieć komórkową + Odświeżanie + Ustawienia Flattr + Logowanie do Flattr + Zaloguj się do konta Flattr aby wspierać twórców bezpośrednio z aplikacji. + Wesprzyj aplikację na Flattr + Wesprzyj twórcę AntennaPod przez Flattr. Dzięki! + Anuluj dostęp + Anuluj dostęp tej aplikacji do konta Flattr + Automatyczne wsparcie na Flattr + Interfejs użytkownika + Wybierz motyw + Zmień wygląd AntennaPod. + Automatyczne pobieranie + Skonfiguruj automatyczne pobieranie odcinków. + Włącz filtr Wi-Fi + Zezwól na automatyczne pobieranie tylko dla określonych sieci Wi-Fi. + Pamięć podręczna odcinków + Jasny + Ciemny + Nielimitowane + godziny + godzina + Instrukcja + Zaloguj + Zaloguj się swoim kontem na gpodder.net w celu synchronizacji Twoich subskrypcji. + Wyloguj + Wylogowanie się powiodło + Zmień informacje logowania + Zmień dane logowania konta gpodder.net. + Prędkość odtwarzania + Dostosuj prędkości dostępne dla odtwarzania audio o zmiennej prędkości + Ustaw nazwę hosta + Użyj domyślnego hosta + + + Szukaj kanałów lub odcinków + Znaleziono w notatkach + Znaleziono w rozdziałach + Brak wyników + Szukaj + Znaleziono w tytułach + + Pliki OPML pozwalają na przenoszenie podcastów między aplikacjami. + W celu importu pliku OPML musisz umieścić go w poniższym folderze i nacisnąć przycisk poniżej w celu rozpoczęcia importu. + Rozpocznij import + Import OPML + BŁĄD! + Odczytuję plik OPML + Wystąpił błąd w czasie odczytu dokumentu OPML: + Katalog importowania jest pusty. + Zaznacz wszystko + Odznacz wszystko + Wybierz plik do importu + Eksport OPML + Eksportowanie... + Błąd eksportu + Eksport OPML udany. + Plik .opml został zapisany do:\u0020 + + Ustaw czas do wyłączenia + Wyłącz wyłącznik czasowy + Podaj czas + Wyłącznik czasowy + Pozostały czas:\u0020 + Błąd wpisu, czas musi być liczbą całkowitą + sekundy + minuty + godziny + + KATEGORIE + TOP PODCASTY + SUGESTIE + Szukaj na gpodder.net + Login + Witamy w procesie logowania do gpodder.net. Najpierw podaj swoje dane logowania: + Login + Jeśli nie masz jeszcze konta, możesz utworzyć je tutaj:\nhttps://gpodder.net/register/ + Nazwa użytkownika + Hasło + Wybór urządzenia + Utwórz nowe urządzenie dla swojego konta na gpodder.net lub wybierz istniejące: + Identyfikator urządzenia:\u0020 + Tytuł + Utwórz nowe urządzenie + Wybierz istniejące urządzenie: + Identyfikator urządzenia nie może być pusty + Identyfikator urządzenia w użyciu + Wybierz + Logowanie zakończone sukcesem! + Gratulacje! Twoje konto na gpodder.net jest połączone z urządzeniem. AntennaPod będzie automatycznie synchronizować subskrypcje na urządzeniu z kontem na gpodder.net. + Rozpocznij synchronizację + Idź do strony głównej + Błąd autoryzacji na gpodder.net + Niepoprawna nazwa użytkownika lub hasło + Błąd synchronizacji z gpodder.net + Wystąpił błąd podczas synchronizacji:\u0020 + + Wybrany folder: + Utwórz folder + Wybierz folder danych + Utworzyć nowy folder o nazwie \"%1$s\"? + Utworzono nowy folder + Nie można zapisać do tego folderu + Folder już istnieje + Nie można utworzyć folderu + Folder nie jest pusty + Wybrany folder nie jest pusty. Pobierane media i inne pliki będą umieszczane bezpośrednio w folderze, czy kontynuować? + Wybierz domyślny folder + Wstrzymaj odtwarzanie zamiast wyciszenia jeśli inna aplikacja chce odtworzyć dźwięk. + Wstrzymaj przy przerwaniu + + Subskrybuj + Subskrybowane + Pobieranie... + + Pokaż rozdziały + Pokaż opis odcinka + Pokaż obraz + Cofnij + Przewiń + Audio + Wideo + Przesuń w górę + Więcej akcji + Odcinek jest odtwarzany + Odcinek jest pobierany + Odcinek pobrany + Nowa pozycja + Odcinek jest w kolejce + Liczba nowych odcinków + Liczba odcinków, których zacząłeś słuchać + Przeciągnij aby zmienić pozycję elementu + + Autoryzacja + Zmień swoją nazwę użytkownika oraz hasło dla tego podcastu i jego odcinków + + Importowanie subskrybcji z jednozadaniowych aplikacji + diff --git a/core/src/main/res/values-pt-rBR/strings.xml b/core/src/main/res/values-pt-rBR/strings.xml new file mode 100644 index 000000000..62fd9c046 --- /dev/null +++ b/core/src/main/res/values-pt-rBR/strings.xml @@ -0,0 +1,280 @@ + + + + AntennaPod + Feeds + PODCASTS + EPISÓDIOS + Novo + Lista de espera + Configurações + Adicionar podcast + Downloads + Cancelar Download + Histórico de reprodução + gpodder.net + gpodder.net login + + + + Abrir no navegador + Copiar URL + Compartilhar URL + URL copiada para área de transferência. + + Apagar histórico + + Confirmar + Cancelar + Autor + Idioma + Configurações + Capa + Erro + Um erro ocorreu: + Atualizar + Não há dispositivos de armazenamento externo disponíveis. Por favor, certifique-se de que um dispositivo de armazenamento externo está montado para que o aplicativo possa funcionar adequadamente. + Capítulos + Notas do podcast + Descrição + Episódio mais recente:\u0020 + \u0020episódios + Duração:\u0020 + Tamanho:\u0020 + Processando + Carregando... + Salvar nome do usuário e senha + Fechar + Tentar novamente + Incluir em downloads automáticos + + URL do Feed + Adicionar podcast por URL + + Marcar todos como lido + Mostrar informação + Compartilhar link do site + Compartilhar link do feed + Por favor confirme que você deseja apagar este feed e TODOS os episódios que você fez download deste feed. + Removendo feed + + Download + Reproduzir + Pausar + Stream + Remover + Marcar como lido + Marcar como não lido + Adicionar à fila + Remover da fila + Visitar Website + Adicionar ao Flattr + Enfileirar todos + Baixar todos + Pular episódio + + Download pendente + Download em execução + Dispositivo de armazenamento não encontrado + Espaço insuficiente + Erro de arquivo + Erro de HTTP Data + Erro desconhecido + Parser Exception + Tipo de feed não suportado + Erro de conexão + Host desconhecido + Cancelar todos os downloads + Download cancelado + Downloads finalizados + URL inválida + Erro de IO + Erro de requisição + Erro no acesso ao Banco de dados + \u0020Downloads restantes + Baixando dados do podcast + %1$d downloads com sucesso, %2$d falharam + Título desconhecido + Feed + Arquivo de mídia + Imagem + Ocorreu um erro durante download do arquivo:\u0020 + + Erro! + Nenhuma mídia tocando + Preparando + Pronto + Buscando + Servidor morreu + Erro desconhecido + Nenhuma mídia tocando + 00:00:00 + Armazenando + Reproduzindo podcast + + Limpar fila + Desfazer + Item removido + Mover para o topo + Mover para o fim + + Logar no Flattr + Pressione o botão abaixo para iniciar o processo de autenticação. Você será direcionado para a tela de login do Flattr, que pedirá autorização para que o AntennaPod utilize o Flattr. Após conceder a permissão, você retornará a esta tela automaticamente. + Autenticar + Retornar ao início + Autenticado com sucesso! Agora você poderá utilizar o Flattr de dentro do AntennaPod. + Nenhum token do Flattr encontrado + Sua conta Flattr não está conectada ao AntennaPod. Você pode conectar sua conta ao AntennaPod para usar o Flattr de dentro da aplicação ou pode visitar o website do feed para usar o Flattr por lá. + Autenticar + Ação proibida + AntennaPod não tem permissão para esta ação. A permissão de acesso do AntennaPod pode ter sido revogada. Você pode re-autenticar ou visitar o website do feed. + Acesso revogado + Você revogou o token de acesso do AntennaPod com sucesso. Para finalizar o processo, você deve remover esta app da lista de aplicativos aprovados nas configurações de sua conta no website do Flattr. + + + Download Plugin + Plugin Não Instalado + Para velocidade variável de reprodução funcionar uma biblioteca de terceiros deve ser instalada.\n\nToque em \'Download Plugin\' para baixar um plugin grátis na Play Store.\n\nQuaisquer problemas encontrados usando esse plugin não é responsabilidade do AntennaPod e deve ser reportado ao proprietário do plugin. + Velocidades de Reprodução + + Não existem itens nesta lista. + Você ainda não assinou nenhum feed. + + Outros + Sobre + Fila + Serviços + Flattr + Interromper a reprodução quando o fone de ouvido for desconectado + Pular para próximo item da fila quando a reprodução terminar + Reprodução + Rede + Intervalo de atualização + Especifica o intervalo com que os feeds serão atualizados automaticamente ou desabilita esta funcionalidade + Fazer download dos arquivos apenas via rede WiFi + Reprodução contínua + Download de mídia via WiFi + Fones de ouvido desconectados + Atualizações via Rede de Dados Celular + Permite atualizações quando conectado na rede de dados celular + Atualizando + Configurações do Flattr + Logar no Flattr + Loga na sua conta Flattr para utilizá-lo diretamente da aplicação + Registra este aplicativo no Flattr + Suportar o desenvolvimento do AntennaPod usando o Flattr. Obrigado! + Revogar acesso + Cancelar permissão de acesso à sua conta Flattr + Interface com usuário + Selecionar tema + Altera a aparência do AntennaPod + Download automático + Configurar download automático de episódios. + Habilitar filtro Wi-Fi + Permitir download automático somente pelas redes Wi-Fi selecionadas. + Cache de episódios + Claro + Escuro + Ilimitado + horas + hora + Manual + Login + Faça o login na sua conta gpodder.net para sincronizar suas assinaturas. + Sair + Saiu com sucesso + Alterar informações de login + Alterar informações de login da sua conta gpodder.net + Velocidades de Reprodução + Personalize as velocidades variáveis de reprodução de áudio. + Configurar hostname + Usar host padrão + + + Procurar por Feeds ou Episódios + Encontrado nas notas do podcast + Encontrado nos capítulos + Nenhum resultado encontrado + Pesquisar + Encontrado no título + + Arquivos OPML permitem que você mova seus podcasts de um programa de podcasts para outro. + Para importar um arquivo OPML, você precisa armazená-lo neste diretório e pressionar o botão abaixo para iniciar o processo de importação. + Iniciar importação + Importação de OPML + ERRO! + Lendo arquivo OPML + Ocorreu um erro durante a leitura do documento OPML: + O diretório de importação está vazio. + Selecionar todos + Remover seleção + Escolher arquivo para importar + Exportar OPML + Exportando... + Erro na exportação + OMPL exportado com sucesso + O arquivo .opml foi gravado em:\u0020 + + Configura desligamento automático + Desabilita desligamento automático + Informe a duração + Desligamento automático + Tempo restante:\u0020 + Entrada inválida, a duração precisa ser um número inteiro + + CATEGORIAS + TOP PODCASTS + SUGESTÕES + Buscar no gpodder.net + Login + Bem-vindo ao processo de login gpodder.net. Primeiramente, digite suas informações: + Login + Se ainda não possui uma conta, você pode criar uma aqui:\nhttps://gpodder.net/register/ + Nome do usuário + Senha + Seleção de dispositivo + Crie um novo dispositivo para usar em sua conta gpodder.net ou escolha um já existente: + ID do dispositivo:\u0020 + Descrição do dispositivo + Criar novo dispositivo + Escolher dispositivo existente: + ID do dispostivo não pode estar em branco + ID do dispositivo já está em uso + Escolher + Login realizado com sucesso! + Parabéns! Sua conta gpodder.net agora está conectada ao seu dispositivo. O AntennaPod irá, daqui em diante, sincronizar automaticamente assinaturas do seu dispositivo com sua conta gpodder.net. + Iniciar sincronização agora + Ir para tela principal + gpodder.net: erro de autenticação + Nome do usuário ou senha incorreta + gpodder.net: erro de sincronização + Ocorreu um erro durante a sincronização:\u0020 + + Selecionar pasta: + Criar pasta + Escolher pasta de dados + Criar nova pasta com o nome \"%1$s\"? + Nova pasta criada + Não é possível escrever nesta pasta + Pasta já existente + Não foi possível criar pasta + A pasta não está vazia + A pasta que você selecionou não está vazia. Os downloads de mídia e outros arquivos serão colocados diretamente nesta pasta. Deseja mesmo continuar? + Escolher pasta padrão + Pause a reprodução em vez de abaixar o volume quando outro aplicativo reproduzir sons + Pausar em interrupções + + Assinar + Assinado + Baixando... + + Mostrar imagem + Mais ações + Episódio está sendo reproduzido + Episódio foi baixado + Item é novo + Episódio está na fila + Numero de novos episódios + + + diff --git a/core/src/main/res/values-pt/strings.xml b/core/src/main/res/values-pt/strings.xml new file mode 100644 index 000000000..f1e525384 --- /dev/null +++ b/core/src/main/res/values-pt/strings.xml @@ -0,0 +1,341 @@ + + + + AntennaPod + Fontes + Adicionar podcast + Podcasts + Episódios + Novos episódios + Todos os episódios + Novo + Lista de espera + Definições + Adicionar podcast + Transferências + Em curso + Terminadas + Registo + Cancelar transferência + Histórico de reprodução + gpodder.net + Acesso gpodder.net + + Publicados recentemente + Mostrar apenas novos episódios + + Abrir menu + Fechar menu + + Abrir no navegador + Copiar URL + Partilhar URL + URL copiado para a área de transferência. + Ir para esta posição + + Limpar histórico + + Confirmar + Cancelar + Autor + Idioma + Definições + Imagem + Erro + Ocorreu um erro: + Atualizar + Não existe um cartão SD. Certifique-se que inseriu o cartão corretamente. + Capítulos + Notas + Descrição + Episódio mais recente:\u0020 + \u0020episódios + Duração:\u0020 + Tamanho:\u0020 + A processar... + A carregar... + Gravar utilizador e senha + Fechar + Tentar novamente + Incluir nas transferências automáticas + + URL da fonte + URL da fonte ou sítio web + Adicionar podcast via URL + Localizar podcasts no diretório + Pode procurar os novos podcasts no gPodder.net por nome, categoria ou popularidade. + Procurar no gPodder.net + + Marcar tudo como lido + Marcar todos os episódios como lidos + Mostrar informações + Remover podcast + Partilhar ligação do sítio web + Partilhar ligação da fonte + Confirme a eliminação desta fonte e de todos os episódios a ela petencentes. + Remover fonte + + Transferir + Reproduzir + Pausa + Emitir + Remover + Remover episódio + Marcar como lido + Marcar como novo + Adicionar à fila + Remover da fila + Aceder ao sítio web + Flattr + Colocar tudo na fila + Transferir tudo + Ignorar episódio + + sucesso + falha + Transferência pendente + Transferência atual + Cartão SD não encontrado + Espaço insuficiente + Erro no ficheiro + Erro HTTP + Erro desconhecido + Exceção do processador + Fonte não suportada + Erro de ligação + Servidor desconhecido + Erro de autenticação + Cancelar transferências + Transferência cancelada + Transferências terminadas + URL inválido + Erro I/O + Erro de pedido + Erro de acesso à base de dados + \u0020Transferências em falta + Processamento de transferências + A transferir dados... + %1$d transferências efetuadas, %2$d falhadas + Título desconhecido + Fonte + Ficheiro multimédia + Imagem + Ocorreu um erro ao transferir o ficheiro:\u0020 + Requer autenticação + O recurso solicitado requer um utilizador e uma senha + + Erro! + Nada em reprodução + A preparar + Pronto + A procurar + Erro de servidor + Erro desconhecido + Nada em reprodução + 00:00:00 + A processar... + Reproduzir podcast + Tecla multimédia desconhecida: %1$d + + Limpar fila + Anular + Item removido + Mover para o topo + Mover para o fundo + + Sessão Flattr + Prima o botão abaixo para iniciar a autenticação. O seu navegador web abrirá o ecrã da sessão flattr e ser-lhe-á solicitada a permissão para o AntennaPod efetuar as alterações. Após ser dada a permissão, voltará novamente a este ecrã. + Autenticar + Voltar ao ecrã + Autenticação efetuada! Já pode fazer o flattr com a aplicação. + Token flattr não encontrado + Parece que a sua conta flattr não está integrada ao AntennaPod. Clique aqui para autenticar. + Parece que a sua conta flattr não está vinculada ao AntennaPod. Pode vincular a sua conta ao AntennaPod ou aceder ao sítio web para fazer o flattr. + Autenticar + Ação negada + O AntennaPod não possui as permissões para esta ação. É possível que o token de acesso ao flattr via AntennaPod tenha sido revogado. Pode efetuar nova autenticação ou aceder ao sítio web do item. + Acesso revogado + Você revogou o token de acesso do AntennaPod à sua conta. Para concluir o processo, tem que remover esta aplicação da lista de aplicações presentes nas definições de conta no sítio web do flattr. + + Flattr de um item! + Flattr de %d itens! + Flattr: %s + Falha ao efetuar flattr de %d itens! + Não flattr: %s. + O flattr deste item será feito mais tarde + Flattring %s + O AntennaPod está a flattring + O AntennaPod fez o flattr + O AntennaPod não fez o flattr + A obter itens com flattr + + Transferir extra + Extra não instalado + Para melhorar a reprodução, deve transferir e instalar um biblioteca de terceiros.\nClique Transferir extra para transferir o extra através da loja Google.\n\nSe encontrar problemas ao utilizar esta biblioteca, os programadores do AntennaPod não podem ser responsabilizados e deve contactar o programador do extra. + Velocidades de reprodução + + Não existem itens na lista. + Ainda não possui quaisquer fontes. + + Outras + Sobre + Fila + Serviços + Flattr + Parar reprodução ao remover os auscultadores + Ir para a faixa seguinte ao terminar a reprodução + Reprodução + Rede + Intervalo entre atualizações + Indique o intervalo de tempo entre as atualizações de fontes ou desative a opção + Apenas transferir pelas redes sem fios + Reprodução contínua + Transferência Wi-Fi + Auscultadores removidos + Atualizações móveis + Permitir atualizações através da rede de dados + A atualizar + Definições flattr + Sessão flattr + Inicie sessão na sua conta flattr para fazer o flattr no AntennaPod. + Flattr desta aplicação + Ajude no desenvolvimento do AntennaPod através do Flattr. Obrigado! + Revogar acesso + Revogar permissões de acesso da aplicação à sua conta flattr. + Flattr automático + Configurar flattr automático + Interface + Tema + Mudar o aspeto do AntennaPod. + Transferência automática + Configure a transferência automática dos episódios. + Ativar filtro Wi-Fi + Apenas permitir transferências automáticas através de redes sem fios. + Cache de episódios + Claro + Escuro + Sem limite + horas + hora + Manual + Acesso + Aceda à sua conta gpodder.net para poder sincronizar as subscrições. + Sair + Sessão terminada + Mudar informação de acesso + Mudar informação de acesso à sua conta gpodder.net. + Velocidades de reprodução + Personalize as velocidades de reprodução disponíveis. + Intervalo de procura + Ao recuar ou avançar, procurar este valor de segundos + Definir nome de servidor + Utilizar pré-definição + + Ativar flattr automático + Flattr de episódios ao atingir %d porcento de reprodução + Flattr de episodios ao iniciar a reprodução + Flattr de episódios ao terminar a reprodução + + Procurar fontes ou episódios + Encontrado nas notas + Encontrado nos capítulos + Nenhum resultado + Procura + Encontrado no título + + Os ficheiros OPML permitem-lhe mover os podcasts entre aplicações. + Para importar um ficheiro OPML, tem que o colocar neste diretório e premir o botão abaixo para iniciar o processo. + Iniciar importação + Importação OPML + Erro! + A ler ficheiro OPML + Ocorreu um erro ao ler o ficheiro OPML: + O diretório de importação está vazio. + Marcar tudo + Desmarcar tudo + Escolha o ficheiro a importar + Exportação OPML + Exportação... + Erro de exportação + Exportação efetuada. + O ficheiro .opml foi gravado em:\u0020 + + Definir temporizador + Desativar temporizador + Introduza o tempo + Temporizador + Tempo restante:\u0020 + Valor inválido. Tem que ser um inteiro. + segundos + minutos + horas + + Categorias + Melhores + Sugestões + Procurar no gpodder.net + Acesso + Bem-vindo ao processo de acesso ao gpodder.net. Introduza os dados de acesso: + Acesso + Se ainda não possui uma conta, pode criar uma em:\nhttps://gpodder.net/register/ + Utilizador + Senha + Seleção de dispositivo + Criar um novo dispositivo ou escolher um existente para aceder à sua conta gpodder.net + ID do dispositivo:\u0020 + Legenda + Criar novo dispositivo + Escolher dispositivo: + ID do dispositivo não pode estar vazia + ID de dispositivo já utilizada + Escolher + Sessão iniciada! + Parabéns! A sua conta gpodder.net está vinculada ao seu dispositivo. Agora, já pode sincronizar as subscrições no dispositivo com a sua conta gpodder.net. + Sincronizar agora + Ir para o ecrã principal + Erro de autenticação gpodder.net + Utilizador ou senha inválido + Erro de sincronização gpodder.net + Ocorreu um erro ao sincronizar:\u0020 + + Diretório escolhido: + Criar diretório + Escolha o diretório + Criar um diretório com o nome \"%1$s\"? + Novo diretório criado + Não é possível gravar neste diretório + O diretório já existe + Não é possível criar o diretório + Diretório não vazio + O diretório escolhido não está vazio. As transferências serão colocadas neste diretório. Continuar? + Escolha a pasta pré-definida + Pausa na reprodução em vez de baixar o volume se outra aplicação quiser reproduzir sons + Pausa nas interrupções + + Subscrever + Subscrito + Transferência... + + Mostrar capítulos + Mostrar notas + Mostrar imagem + Recuar + Avanço rápido + Áudio + Vídeo + Navegar para cima + Mais ações + Episódio em reprodução + Episódio a ser transferido + Episódio transferido + Novo item + Episódio está na fila + Número de novos episódios + Número de episódios que já foi iniciada a reprodução + Arraste para mudar a posição deste item + + Autenticação + Altere o seu nome de utilizador e senha para este podcast e seus episódios. + + Importar subscrições de aplicações single-purpose... + diff --git a/core/src/main/res/values-ro-rRO/strings.xml b/core/src/main/res/values-ro-rRO/strings.xml new file mode 100644 index 000000000..a6e782f74 --- /dev/null +++ b/core/src/main/res/values-ro-rRO/strings.xml @@ -0,0 +1,245 @@ + + + + AntennaPod + Feeduri + PODCASTURI + EPISOADE + Nou + Listă de așteptare + Setări + Descărcări + Anulează descărcare + Istorie ascultare + gpodder.net + autentificare gpodder.net + + + + Deschide în browser + Copiază URL + Împarte URL + URL copiat în clipboard + + Golește istoric + + Confirmă + Anulează + Autor + Limbă + Setări + Eroare + A avut loc o eroare: + Reîncarcă + Nu exista stocare externă. Asigurați-vă că stocarea externă este conectată pentru ca aplicația să funcționeze corespunzător. + Capitole + Notițe + Descriere + Cel mai recent episod:\u0020 + \u0020episoade + Durată:\u0020 + Dimensiune:\u0020 + Procesează + Încărcare... + Salvează numele de utilizator și parola + închide + Reîncearcă + + Adresă feed + + Marchează toate ca citite + Arată informații + Împarte adresă website + Împarte adresă feed + Confirmați ștergerea feedului și a TUTUROR episoadelor pe care le-ați descărcat. + + Descarcă + Play + Pauză + Stream + Elimină + Marchează ca citit + Marchează ca necitit + Adaugă la Coadă + Șterge din Coadă + Vizitează Website + Flattr aceasta + Adaugă toate în coadă + Descarcă toate + Sari peste episod + + Descărcare în așteptare + Se descarcă + Mediu de stocare lipsă + Spațiu insuficient + Eroare fișier + Eroare Date HTTP + Eroare necunoscută + Excepție parser + Tip de feed nesuportat + Eroare de conexiune + Host necunoscut + Anulează toate descărcările + Descărcare anulată + Descărcări terminate + URL malformat + Eroare IO + Eroare cerere + \u0020descărcări rămase + Descarcă date podcast + %1$d descărcari cu succes, %2$d eșuate + Titlu necunoscut + Feed + Fișier media + Imagine + O eroare a avut loc când se descărca fișierul:\u0020 + + Eroare! + Nu se ascultă nimic + Pregătește + Pregătit + Căutare + Server mort + Eroare necuonscută + Nu se ascultă nimic + 00:00:00 + Buffering + Cântă podcast + + Golește coada + Refă + Element înlăturat + + Flattr sign-in + Apăsați butonul de mai jos pentru a începe procesul de autentificare. Veți fi îndreptat spre pagina de logare flattr în browser și veți fi rugat să acordați permisiuni AntennaPod sa flattr. După ce veți acorda permisiunile veți fi readuși la acest ecran automat. + Autentificare + Întoarcere acasă + Autentificare cu succes! Acum puteți flattr din aplicație. + Nu s-a găsit token Flattr + Contul flattr nu pare șa fie conectat la AntennaPod. Puteți fie conecta contul cu AntennaPod pentru a flattr lucruri din aplicație sau puteți vizita site-ul pentru a flattr acolo. + Autentificați-vă + Acțiune interzisă + AntennaPod nu are permisiuni pentru această acțiune. Motivul poate fi că tokenul de acces al AntennaPod pentru contul vostru a fost revocat. Vă puteți fie re-autentifica fie vizita direct site-ul. + Acces revocat + Ați revocat cu succes accesul AntennaPod la contul vostru. Pentru a completa acest proces trebuie să ștergeți aplicația din lista de aplicații aprobate din setările contului de pe site-ul flattr. + + + Descarcă plugin + Plugin neinstalat + Pentru ca viteza variabilă de ascultare să funcționeze este necesară o librărie externă.\n\nApăsați \'Descarcă Plugin\' pentru a descărca un plugin gratuit din Play Store\n\nOrice probleme găsite folosind acest plugin nu sunt responsabilitatea AntennaPod și trebuie raportate autorului pluginului. + Viteze de ascultare + + Nu sunt elemente în listă. + Nu v-ați abonat la nici un feed momentan. + + Altele + Despre + Coadă + Servicii + Flattr + Pune pauză când căștile sunt deconectate + Sari la următorul element din coadă cand se termină ascultarea + Ascultare + Rețea + Interval actualizare + Specifică un interval în care feedurile sunt actualizate automat sau oprește funcția + Descarcă fișiere media doar pe WiFi + Ascultare continuă + Descărcare media pe WiFi + Căști deconectate + Actualizări mobile + Permite actualizări pe conexiunea de date mobilă + Reîncarcare + Setări Flattr + Sign-in Flattr + Logați la contul flattr pentru a flattr lucruri direct din aplicație. + Flattr această aplicație + Ajutați dezvoltarea AntennaPod prin flattr. Mulțumesc! + Revocare acces + Revocă accesul permisiunilor pentru contul de flattr. + Interfața grafică + Alege temă + Schimbă aspectul AntennaPod. + Descărcare automată + Configurează descărcarea automată a episoadelor. + Pornește filtru Wi-Fi + Pornește descărcarea automată doar pentru rețele Wi-Fi selectate. + Cache de episoade + Deschis + Întunecat + Nelimitat + ore + oră + Manual + Autentificare + Viteze de ascutare + Modifică vitezele disponibile pentru viteza de ascultare. + + + Caută feeduri sau episoade + Găsit în notițe + Găsit în capitole + Nu s-a găsit nici un rezultat + Caută + Găsit în titlu + + Pentru a importa un fișier OPML trebuie să-l salvați în următorul director și apăsați butonul de mai jos pentru a începe procesul. + Începe importarea + OPML import + EROARE! + Citește fișierul OPML + A avut loc o eroare la citirea documentului opml: + Directorul de import este gol. + Selectează toate + Deselectează toate + Alege fișier pentru import + Exportă OPML + Exportă... + Eroare exportare + Exportare opml cu succes. + Fișierul .opml a fost scris în:\u0020 + + Setează cronometru somn + Oprește cronometru somn + Introdu timp + Cronometru somn + Timp rămas:\u0020 + Input invalid, timpul trebuie să fie un întreg + + CATEGORII + SUGESTII + Autentificare + Conectare + Utilizator + Parolă + Alegere Dispozitiv + ID dispozitiv:\u0020 + Creează dispozitiv nou + ID-ul dispozitivului nu trebuie să fie gol + ID-ul de dispozitiv este deja în uz + Alege + Începe sincronizarea acum + Mergi la ecranul principal + eroare de autentificare la gpodder.net + Nume utilizator sau parolă greșite + eroare de sincronizare gpodder.net + + Fișier selectat: + Crează fișier + Alege fișier date + Crează fișier nou cu numele \"%1$s\"? + A fost creat fișierul + Nu poate fi scris în fișier + Fișier deja existent + Nu poate creea fișier + Fișierul nu este gol + Fișierul selectat nu este gol. Descărcările media și alte fișiere vor fi plasate direct în acest director. Continuați oricum? + Alege fișier implicit + + Abonează-te + Abonat + Se descarcă... + + + + diff --git a/core/src/main/res/values-ru/strings.xml b/core/src/main/res/values-ru/strings.xml new file mode 100644 index 000000000..c5c642da0 --- /dev/null +++ b/core/src/main/res/values-ru/strings.xml @@ -0,0 +1,311 @@ + + + + AntennaPod + Каналы + Подкасты + Выпуски + Новые выпуски + Все выпуски + Новые + В ожидании + Настройки + Добавить подкаст + Загрузки + Отменить загрузку + История воспроизведения + gpodder.net + Войти на gpodder.net + + + + Открыть в браузере + Скопировать ссылку + Поделиться ссылкой + Ссылка скопирована в буфер + + Очистить историю + + Подтвердить + Отмена + Автор + Язык + Настройки + Обложка + Ошибка + Произошла ошибка: + Обновить + Внешний носитель недоступен. Убедитесь что внешний носитель установлен, иначе приложение не сможет нормально работать. + Главы + Примечания к выпуску + Описание + Последний выпуск:\u0020 + \u0020выпуск(ов) + Продолжительность:\u0020 + Размер:\u0020 + Обработка + Загрузка... + Сохранить имя пользователя и пароль + Закрыть + Повторить + Добавить в автозагрузки + + URL канала + Добавить подкаст по URL + Найти подкаст в каталоге + + Отметить все как прочитанное + Показать информацию + Удалить подкаст + Ссылка на сайт + Ссылка на канал + Подтвердите удаление канала и ВСЕХ загруженных с этого канала выпусков. + Удаление канала + + Загрузить + Воспроизвести + Пауза + Потоковое воспроизведение + Удалить + Отметить как прочитанное + Отметить как непрочитанное + Добавить в очередь + Удалить из очереди + Посетить сайт + Поддержать через Flattr + Добавить всё в очередь + Загрузить всё + Пропустить выпуск + + успешно + не удалось + Загрузка в ожидании + Загрузка в процессе + Устройство хранения не найдено + Недостаточно места + Ошибка файла + Ошибка протокола HTTP + Неизвестная ошибка + Ошибка обработки + Неподдерживаемый тип канала + Ошибка соединения + Неизвестный узел + Ошибка авторизации + Отменить все загрузки + Загрузка отменена + Загрузки завершены + Неправильный адрес + Ошибка ввода-вывода + Ошибка запроса + Ошибка доступа к базе данных + Осталось\u0020загрузок + Получение данных подкаста + %1$d загрузок завершено, %2$d не удалось + Неизвестное название + Канал + Медиафайл + Изображение + Ошибка при загрузки файла:\u0020 + Необходима авторизация + Для доступа к ресурсу необходимо ввести имя пользователя и пароль + + Ошибка + Ничего не воспроизводится + Подготовка + Готово + Перемотка + Сервер недоступен + Неизвестная ошибка + Ничего не воспроизводится + 00:00:00 + Буферизация + Воспроизведение подкаста + + Очистить очередь + Отмена + Удалено + Переместить вверх + Переместить вниз + + Авторизоваться в Flattr + Нажмите кнопку, чтобы начать процесс авторизации. Вы будете перенаправлены на сайт Flattr, где нужно будет разрешить AntennaPod использовать ваш аккаунт. После этого вы автоматически будете перенаправлены обратно. + Авторизовать + Вернуться к началу + Успешная авторизация. Теперь можно использовать Flattr прямо из приложения. + Токен Flattr не найден + Кажется, ваш аккаунт Flattr не подключен к AntennaPod. Можно подключить аккаунт к AntennaPod или посетить сайт канала, чтобы пожертвовать через Flattr прямо на сайте. + Авторизоваться + Действие запрещено + AntennaPod не имеет прав для выполнения этого действия. Возможно, доступ к вашему аккаунту был отозван. Можно авторизоваться заново или посетить сайт, которому вы пожертвовали через Flattr. + Доступ отозван + Вы успешно отключили AntennaPod от аккаунта в Flattr. Чтобы завершить этот процесс нужно удалить AntennaPod из списка приложений подключенных к аккаунту на сайте Flattr. + + Один поддержан через Flattr! + Поддержано через Flattr: %d. + Поддержано через Flattr: %s. + Не удалось поддержать через Flattr: %d! + Не поддержано через Flattr: %s. + Будет поддержано через Flattr потом + %s поддерживается через Flattr + AntennaPod поддерживает через Flattr + Вы поддержали AntennaPod через Flattr + Ошибка + Получение списка поддержаного через Flattr + + Загрузить плагин + Плагин не установлен + Для изменения скорости воспроизведения должна быть установлена сторонняя библиотека.⏎\n⏎\nНажмите «Загрузить плагин», чтобы загрузить беспалтный плагин из Play Store⏎\n⏎\nЛюбые проблемы при использовании плагина не являются ответственностью AntennaPod и о них следует сообщать владельцу плагину. + Скорость воспроизведения + + Список пуст + Вы еще не подписаны ни на один канал + + Прочее + О программе + Очередь + Сервисы + Flattr + Приостановить воспроизведение, когда наушники отсоединены + После завершения воспроизведения перейти к следующему в очереди + Воспроизведение + Сеть + Интервал обновлений + Укажите интервал через который каналы обновляются автоматически, или отключите его + Загружать файлы только через Wi-Fi + Непрерывное воспроизведение + Загрузка по Wi-Fi + Наушники отсоединены + Мобильные обновления + Позволить обновления через мобильное интернет-подключение + Обновление + Настройки Flattr + Авторизация Flattr + Авторизуйтесь во Flattr чтобы поддерживать каналы прямо из приложения + Поддержать это приложение в Flattr + Поддержите разработку AntennaPod через Flattr. Спасибо! + Отозвать доступ + Отменить доступ этого приложения к вашему аккаунту Flattr. + Автоматически поддерживать через Flattr + Интерфейс + Выбор темы + Изменить тему оформления AntennaPod + Автоматическая загрузка + Настроить автоматическую загрузку выпусков. + Включить фильтр Wi-Fi + Разрешать автоматическую загрузку только для выбранных сетей Wi-Fi. + Кэш выпусков + Светлая + Тёмная + Неограничен + ч. + ч. + Вручную + Войти + Вход в ваш аккаунт gpodder.net для синхронизации ваших подписок. + Выход из gpodder.net + Выход произведён успешно + Изменить информацию авторизации + Изменить информацию авторизации для аккаунта gpodder.net + Скорость воспроизведения + Настроить скорости воспроизведения + Задать имя узла + Использовать узел по умолчанию + + + Поиск каналов или выпусков + Найдено в описании выпуска + Найдено в главах + Ничего не найдено + Поиск + Найдено в заголовке + + OPML файлы позволяют перемещать ваши подкасты из одного менеджера подкастов в другой. + Для импорта файла OPML его нужно поместить в указанный каталог и нажать кнопку внизу для запуска импорта. + Начать импорт + Импорт OPML + Ошибка + Чтение файла OPML + Ошибка чтения файла OPML + Каталог для импорта пуст. + Отметить все + Снять все отметки + Выбрать файл для импорта + Экспорт в OPML + Экспортируется... + Ошибка экспорта + Экспорт OPML завершён. + Файл OPML был записан в:\u0020 + + Установить таймер сна + Отключить таймер сна + Введите время + Таймер сна + Осталось времени:\u0020 + Неправильный ввод, время должно быть в виде числа + + Категории + Лучшее + Рекомендации + Искать на gpodder.net + Войти + Добро пожаловать в процесс авторизации на gpodder.net. Сначала введите вашу информацию для авторизации: + Войти + Если у вас ещё нет аккаунта, то вы можете создать его здесь:⏎\nhttps://gpodder.net/register/ + Имя пользователя + Пароль + Выбор устройства + Создайте новое устройство, чтобы использовать ваш аккаунт на gpodder.net или выберите существующее: + Идентификатор устройства:\u0020 + Название устройства + Создайте новое устройство + Выберите существующее устройство: + Поле с Device ID не должно быть пустым + Device ID уже используется + Выберите + Авторизация успешна! + Поздравляем! Ваш аккаунт на gpodder.net теперь связан с вашим устройством. AntennaPod теперь сможет автоматически синхронизировать ваши подписки с аккаунтом gpodder.net + Начать синхронизацию + Перейти на главный экран + Ошибка авторизации на gpodder.net + Неправильное имя пользователя или пароль + Ошибка синхронизации с gpodder.net + Произошла ошибка во время синхронизации:\u0020 + + Выбранная папка: + Создать папку + Выбрать папку для хранения данных + Создать папку \"%1$s\"? + Новая папка создана + Запись в эту папку невозможна + Папка уже существует + Невозможно создать папку + Папка не пуста + Выбранная папка не пуста. Загрузки и прочие файлы будут сохранены в эту папку. Продолжить? + Выберите папку по умолчанию + Пауза вместо уменьшения громкости, когда другое приложение проигрывает звуки + Пауза при смене аудиофокуса + + Подписаться + Подписка оформлена + Загрузка... + + Показать разделы + Показать заметки к эпизодам + Показать изображение + Назад + Вперед + Аудио + Видео + Перейти выше + Другие действия + Эпизод воспроизводится + Эпизод загружается + Эпизод загружен + Новый + Эпизод в очереди + Количество новых эпизодов + Количество начатых эпизодов + + + Импорт подписок из одноцелевых приложений… + diff --git a/core/src/main/res/values-sv-rSE/strings.xml b/core/src/main/res/values-sv-rSE/strings.xml new file mode 100644 index 000000000..e17f54fa5 --- /dev/null +++ b/core/src/main/res/values-sv-rSE/strings.xml @@ -0,0 +1,341 @@ + + + + AntennaPod + Flöden + Lägg till podcast + PODCASTS + AVSNITT + Nya episoder + Alla episoder + Ny + Väntelista + Inställningar + Lägg till podcast + Nedladdningar + Körs + Färdiga + Logg + Avbryt nedladdnin + Uppspelningshistorik + gpodder.net + gpodder.net login + + Nyligen publicerade + Visa bara nya episoder + + Öppna meny + Stäng meny + + Öppna i webbläsare + Kopiera URL + Dela URL + Kopierade URL till clipboard. + Gå hit + + Rensa historik + + Bekräfta + Avbryt + Skapare + Språk + Inställningar + Bild + Fel + Ett fel inträffade: + Uppdatera + Ingen extern lagring är tillgänglig. Se till att montera en extern lagringsenhet så att appen kan fungera korrekt. + Kapitel + Shownotes + Beskrivning + Senaste avsnittet:\u0020 + \u0020episoder + Längd:\u0020 + Storlek:\u0020 + Bearbetar + Laddar... + Spara användarnamn och lösenord + Stäng + Försök igen + Inkludera i automatiska nedladdningar + + Flödets URL + URL till flöde eller webbsida + Lägg till podcast via URL + Hitta podcast i mapp + Du kan söka efter podcasts baserat på namn, kategori eller populäritet på tjänsten gpodder.net + Bläddra på gpodder.net + + Markera alla som lästa + Markera alla episoder som lästa + Visa information + Ta bort podcast + Dela hemsidans länk + Dela flödeslänk + Bekräfta att du vill ta bort denna feed och ALLA avsnitt av denna feed som du har hämtat. + Tar bort flöde + + Ladda ned + Spela + Pausa + Stream + Ta bort + Ta bort episod + Markera som läst + Markera som oläst + Lägg till i kön + Ta bort från Kön + Besök websidan + Flattr det här + Lägg till alla i kön + Ladda ner alla + Hoppa över avsnitt + + lyckades + misslyckades + Avvaktar nedladdning + Nedladdning pågår + Lagringsenhet hittades inte + Otillräckligt utrymme + Filfel + HTTP data fel + Okänt fel + Parserfel + Flödestyp utan stöd + Anslutningsfel + Okänd värd + Autentiseringsproblem + Avbryt alla nedladdningar + Nedladdning avbruten + Nedladdningar färdiga + Felaktig webbadress + IO fel + Request fel + Ingen tillgång till databasen + \u0020Nedladdningar kvar + Bearbetar nedladdningar + Laddar ner podcastdata + %1$d nedladdningar lyckades, %2$d misslyckades + Okänd titel + Flöde + Mediafil + Bild + Ett fel uppstod vid försöket att ladda ner filen:\u0020 + Autentisering krävs + Resursen du begärde kräver ett användarnamn och ett lösenord + + Fel! + Inget media spelar + Förbereder + Beredd + Söker + Servern dog + Okänt fel + Inget media spelar + 00:00:00 + Buffrar + Spelar podcast + AntannaPod - Okänd mediaknapp: %1$d + + Rensa kön + Ångra + Föremålet avlägsnades + Flytta längst upp + Flytta längst ned + + Flattr inloggning + Tryck på knappen nedan för att starta autentiseringen. Du kommer att vidarebefordras till Flattrs inloggningsskärm i din webbläsare och uppmanas att ge AntennaPod tillstånd att Flattra saker. Efter att du har gett tillstånd, kommer du automatiskt tillbaka till den här skärmen. + Autentisera + Återgå till Startsidan + Autentiseringen lyckades! Du kan nu Flattra saker i appen. + Ingen Flattr token hittades + Ditt Flattr-konto verkar inte vara anslutet till AntennaPod. Tryck här för att autentisera. + Ditt Flattr konto verkar inte vara ansluten till AntennaPod. Du kan antingen ansluta ditt konto till AntennaPod att Flattr saker i app eller så kan du besöka webbplatsen för att Flattr det där. + Autentisera + Åtgärd förbjuden + AntennaPod saknar behörighet för den här åtgärden. Anledningen till detta kan vara att AntennaPods tillgång till ditt konto har återkallats. Du kan antingen åter autentisera AntennaPod eller besöka hemsidan istället. + Tillgång återkallad + Du har nu återkallat AntennaPods tillgång till ditt konto. För att slutföra processen, måste du ta bort den här appen från listan godkända appar i dina kontoinställningar på Flattrs hemsida. + + Flattrade en sak! + Flattrade %d saker! + Flattrade: %s. + Misslyckades att flattra %d saker! + Ej flattrade: %s. + Saker som kommer att flattras senare + Flattrar %s + AnntennaPod flattrar + AntennaPod har flattrat + AntennaPod misslyckades att flattra + Hämtar flattrade saker + + Ladda ner tillägg + Tillägg ej installerat + För att variabel uppspelningshastighet skall fungera måste ett tredjepartstillägg installeras.\n\nTryck på \'Ladda ner tillägg\' för att ladda ner ett gratis tillägg från Play Store.\n\nAntennaPod ansvarar inte för problem med detta tillägg och de bör rapporteras till tilläggets skapare. + Uppspelningshastigheter + + Det finns inget i denna lista. + Du har inte prenumererat på något flöde ännu. + + Annat + Om + + Tjänster + Flattr + Pausa uppspelningen när hörlurarna bortkopplas + Hoppa till nästa i kön när uppspelningen är klar + Uppspelning + Nätverk + Uppdateringsintervall + Ange ett intervall för att automatiskt uppdatera flödet eller avaktivera det + Hämta mediefiler endast över WiFi + Kontinuerlig uppspelning + WiFi nedladdning + Hörlurar bortkopplade + Mobila uppdateringar + Tillåt uppdateringar via mobil dataanslutning + Uppdatera + Flattr inställningar + Flattr inloggning + För att Flattra saker direkt från appen, logga in på ditt Flattr-konto. + Flattra den här appen + Stöd utvecklingen av AntennaPod genom att flattra den. Tack! + Återkalla åtkomst + Återkalla behörigheten till ditt Flattr-konto för denna app. + Automatisk Flattring + Konfigurerar automatisk Flattring + Användargränssnitt + Välj tema + Ändra utseendet på AntennaPod. + Automatisk nedladdning + Konfigurera automatisk nedladdning av episoder. + Aktivera WiFi filtrering + Tillåt automatisk nedladdning endast för utvalda WiFi-nätverk. + Avsnittscache + Ljust + Mörkt + Obegränsat + timmar + timme + Manuell + Logga in + Logga in med ditt gpodder.net konto för att synkronisera dina prenumerationer. + Logga ut + Utloggning lyckades + Ändra inloggningsinformation + Ändra inloggningsinformationen för ditt gpodder.net konto. + Uppspelningshastigheter + Anpassa de tillgängliga hastigheterna för variabel uppspelningshastighet. + Söktid + Sök så här många sekunder vid snabbspolning bakåt eller framåt + Sätt värdnamn + Använd standardvärden + + Aktivera automatisk Flattring + Flattra episoden så snart %d procent har spelats + Flattra episoden när den startas + Flattra episoden när den spelats klart + + Sök efter flöden eller avsnitt + Hittad i shownotes + Hittad i kapitel + Inga resultat hittades + Sök + Hittad i titeln + + OPML-filer låter dig flytta dina podcasts från en podcatcher till en annan. + Om du vill importera en OPML-fil, måste du placera den i följande katalog och tryck på knappen nedan för att starta importen. + Påbörja importering + Importera OPML-fil + FEL! + Läser OPML-fil + Ett fel har skett vid iläsning av opml dokumentet: + Katalogen är tom. + Välj alla + Avmarkera alla + Välj fil att importera + OPML export + Exporterar... + Exporteringsfel + OPML export lyckades + .opml filen skrevs till:\u0020 + + Ställ in sömntimer + Stäng av sömntimer + Ange tid + Sömntimer + Återstående tid:\u0020 + Ogiltigt tal, tiden måste vara ett heltal + sekunder + minuter + timmar + + KATEGORIER + BÄSTA PODCASTS + FÖRSLAG + Sök på gpodder.net + Inloggning + Välkommen till inloggningsprocessen för gpodder.net. Först, skriv in din inloggningsinformation: + Logga in + Om du inte har ett konto än, så kan du skapa ett här:\nhttps://gpodder.net/register/ + Användarnamn + Lösenord + Enhetsval + Skapa en ny enhet för ditt gpodder.net konto eller välj en befintlig: + Enhets ID:\u0020 + Rubrik + Skapa ny enhet + Välj befintlig enhet: + Enhets ID måste fyllas i + Enhets ID används redan + Välj + Inloggning lyckades! + Grattis! Ditt gpodder.net konto är nu länkat med din enhet. AntennaPod kommer från och med nu automatiskt synkronisera dina prenumerationer på din enhet med ditt gpodder.net konto. + Starta synkronisering nu + Gå till huvudskärmen + gpodder.net autentiseringsfel + Fel användarnamn eller lösenord + gpodder.net synkroniseringsfel + Ett fel uppstod under synkronisering:\u0020 + + Vald mapp: + Skapa mapp + Välj mapp + Skapa ny mapp med namnet \"%1$s\"? + Skapade ny mapp + Kan inte skriva till den här mappen + Mappen finns redan + Kunde inte skapa mapp + Mappen är inte tom + Den mapp du har valt är inte tom. Filer kommer att placeras direkt i denna mapp. Fortsätt ändå? + Välj standardmapp + Pausa uppspelning istället för att sänka volymen när en annan app vill spela ljud + Pausa för avbrott + + Prenumerera + Prenumererar + Laddar ner... + + Visa kapitel + Visa shownotes + Visa bild + Backa + Snabbspola + Ljud + Video + Navigera upp + Fler åtgärder + Episoden spelas + Episoden laddas ner + Episoden är nedladdad + Föremålet är nytt + Episoden är i kön + Antal nya episoder + Antal episoder du har börjat lyssna på + Dra för att ändra dess position + + Autentisering + Byt ditt användarnamn och lösenord för den här podcasten och dess episoder. + + Importerar prenumerationer från appar gjorda för ett enda syfte... + diff --git a/core/src/main/res/values-uk-rUA/strings.xml b/core/src/main/res/values-uk-rUA/strings.xml new file mode 100644 index 000000000..6653e6614 --- /dev/null +++ b/core/src/main/res/values-uk-rUA/strings.xml @@ -0,0 +1,329 @@ + + + + AntennaPod + Канали + Подкасти + Епізоди + Нові епізоди + Всі епізоди + Нові + Черга + Налаштування + Додати подкаст + Завантаження + В процесі + Завершено + Журнал + Скасувати завантаження + Що грало + gpodder.net + gpodder.net логін + + Щойно опубліковано + Показати тількі нові епізоди + + Показати меню + Сховати меню + + Відкрити в браузері + Копія URL + Поділитися URL + Копіювати URL в clipboard + + Забути + + Підтвердити + Скасувати + Автор + Мова + Налаштування + Зображення + Помилка + Трапилась помілка: + Оновити + Немає доступної флешки. Зовнішній носій потрібен для коректної роботи додатку + Глави + Нотатки до епізода + Опис + Найновіший епізод:\u0020 + \u0020епізодів + Довжина:\u0020 + Розмір:\u0020 + Обробка + Завантаження категорій ... + Зберегти ім\'я користувача та пароль + Закрити + Повторити знову + Включити до автозавантаження + + Посилання на канал + Додати подкаст за URL + Знайти подкаст в каталозі + В каталозі gpodder.net можливий пошук за назвою, категорією або популярністю. + Переглянути gpodder.net + + Все прочитано + Позначити всі епізоди як переглянуті + Інформація + Видалити подкаст + Поділитися URL сайту + Поділитися URL каналу + Ви впенені що хочете видаліти канал та всі завантажені епізоди + Удаляю канал + + Завантажити + Грати + Пауза + Прослухати без завантаження + Видалити + Видалити епізод + Прочитано + Непрочитано + Додати до черги + Видалити з черги + Відкрити сайт + Підтримати за допомогою Flattr + Додати до черги + Завантажити все + Пропустити епізод + + успішно + з помилками + Потрібно завантажити + Завантаження + Немає куди зберігати + Мало місця + Помилка файлу + Помилка HTTP + Щось трапилось + Помилка парсера + Непідтримую такий канал + Помилка з\'єднання + Невідомий host + Помилка автентифікації + Скасувати всі завантаження + Відмінено завантаження + Завантажили + Невірний URL + Помилка IO + Помилка запиту + Помилка бази даних + \0020 залишилось завантажити + Обробка завантаженого + Завантаження даних подкасту + Завантажилось %1$d успішно, %2$d з помилками + Невідома назва + Канал + Файл з медіа + Зображення + Помилка при завантажені файлу:\u0020 + Потрібна автентифікація + Для доступа до цього ресурса потрібні ім\'я та пароль + + Помилка! + Нічого грати + Підготовка + Готов + Шукаю + Сервер помер + Невідома помилка + Німає що грати + 00:00:00 + Буферізую + Грає подкаст + + Очистити чергу + Скасувати + Видалено + Догори + Донизу + + Увійти до Flattr + Нажміть цю кнопку для початку авторізації. Буде відкрито flattr в браузері, буде запит на дозвіл доступу Antennapod до flattr. Після надання доступу ви повернетесь до цього екрану автоматично + Ввісти ім\'я та пароль + Повернення до початку + Вийшло авторізуватись. Тепер ви можете flattr things за допомогою додатку + Немає flattr token + Здається ваш обліковий запис flattr не під\'єднано до AntennaPod. Ви можете або під\'єднати її або відкривати web сторінку в браузері + Пароль та логін + Заборонено + AntennaPod не маэ дозвілу це зробити. Можливо відкликаний доступ до AntennaPod. Або ввідіть логін пароль в налаштуваннях або зробить це на сайті + Доступ відкликано + Ви відкликали доступ AntennaPod до облікового запису. Для закінчення процессу вам потрібно видалити додаток з затвержденного списку в вашому облікову запису на сайті flattr + + Flattr\'ed one thing! + Flattr\'ed %d things! + Flattr\'ed: %s. + Failed to flattr %d things! + Not flattr\'ed: %s. + Thing will be flattr\'ed later + Flattring %s + AntennaPod is flattring + AntennaPod has flattr\'ed + AntennaPod flattr failed + Retrieving flattr\'ed things + + Завантажити Plugin + Plugin не встановлено + Для керування швидкістю програвання потрібно встановити plugin\nНатисніть \"Завантажити Plugin\" для завантаження безкоштовного plugin з Play Store\nЯкщо при використанні plugin будуть які небудь проблеми це відповідальність автору plugin, а не автору AntennaPod + Швидкість програвання + + Нічного в цьому списку + Немає підписаних каналів + + Інше + О + Черга + Сервіси + Flattr + Зупинятись коли навушники витягнуті + До наступної черги коли дограє до кінця + Грає + Мережа + Коли оновлювати + Визначати як час для автооновлювання або відключити автооновлення + Завантажувати тільки через Wifi + Грати безперервно + Завантаження через Wifi + Навушники витягнуті + Мобільне оновлення + Дозволити оновлення через оператора зв\'язку + Оновлення + Налаштування Flattr + Увійти до Flattr + Увійти в облікову flattr в flattr things напряму з додатку + Flattr цій додаток + Підтримайте розробку AntennaPod за допомогою flattr. Дякую! + Відкликати доступ + Відкликати дозвіл на доступ до вашого flattr з цього додатку + Automatic Flattr + Зовнішній вид + Обрати тему + Змінити появу AntennaPod + Автоматичне завантаження + Налаштування автоматичного завантаження епізодів + Увімкнути фільтр Wi-Fi + Дозволити автоматичне завантаження тільки в цих Wi-Fi мережах + Кеш епізодів + Світла + Темна + Без обмежень + годин + година + Інструкція + Логін + Увійти до свого облікового запису gpodder.net для сінхронізації ваших каналів + Виход + Успішно закрили доступ + Змінити інформацію для входу + Змінити вашу інформацію для вашего gpodder.net облікового запису + Швидкість програвання + Налаштування швідкості доступно для змінної швидкості програвання + Встановити ім\'я хоста + Використати хост по замовчанню + + + Пошук каналів та епізодів + Знайдено у примітках + Знайдено в главах + Жодних результатів немає + Пошук + Знайдено у назві + + OPML файли дозволяют вам перенести подскати з однієї программи до іншої + Для імпорту OPML файлу, скопіюйте його в цю папку та натіснить кнопку внизу для початку імпорту + Почати імпорт + OPML імпорт + Помилка! + Читаємо OPML файл + Трапилась помілка коли читали OPML документ: + Директорія імпорту пуста + Обрати все + Убрати виділення + Обрати файл для імпорту + OPML экспорт + Експорт ... + Помилка експорту + OPML експорт успішний + OPML файл записаний в:\u0020 + + Таймер сну + Вимкнути засинання + Встановити час + Таймер сну + Залишилось:\u0020 + Помилка вводу, час повинен бути цілим + секунд + хвилин + годин + + КАТЕГОРІЇ + ТОП ПОДКАСТІВ + РЕКОМЕНДАЦІЇ + Пошук на gpodder.net + Логін + Ласкаво просимо до gpodder.net. Зпочатку заповнить вашу інформацію для входу + Логін + Якщо ви щє не маєте логіну, ви можете отримати тут:\nhttps://gpodder.net/register + Ім\'я користувача + Пароль + Обрати пристрій + Під\'єднати новий пристрій к gpodder.net обліковому запису о обрати інсуючий + ID Пристрою:\u0020 + Заголовок + Створити новий пристрій + Вибрати існуючий пристрій + ID пристрою не можете бути пустим + Таке ID пристрою вже є + Обрати + Успішно зайшли + Поздоровляємо! Ваш обліковий запис на gpodder.net зараз пов\'язаний за вашим пристроєм + Почати синхронізацію + Перейти до основного екрана + Помилка авторізації на gpodder.net + Помилка в імені користувача або паролі + gpodder.net помилка синхронізації + Трапилась помилка при сінхронизації:\u0020 + + Обрати папку: + Нова папка + Обрати папку + Створити папку з ім\'ям \"%1$s\"? + Створена нова папка + Не можу записати в цю папку + Папка вже є + Не можу создати папку + В папці щось є + В папці щось є. Всі завантаження зберігаються в цю папку. Все рівно продовжувати? + Обрати папку по замовчанню + Призупиняти програвання замість зниження гучності коли інша програма хоче програти звук + Пауза для перевивання + + Підписатися + Підписано + Завантаження... + + Показати глави + Показати нотатки + Показати зображення + Перемотка назад + Перемотка вперед + Звук + Відео + Догори + Додаткові дії + Епізод програється + Епізод завантажується + Епізод завантажено + Нове + Епізод чекає в черзі + Кількість нових епізодів + Кількість епізодів що ви почали слухати + Перетягніть щоб змінити позицію + + Автентикація + Змінити ваші логін та пароль для подкаста та епізодів + + Імпорт подкастів з інших програм... + diff --git a/core/src/main/res/values-v11/colors.xml b/core/src/main/res/values-v11/colors.xml new file mode 100644 index 000000000..520efaa06 --- /dev/null +++ b/core/src/main/res/values-v11/colors.xml @@ -0,0 +1,5 @@ + + + #286E8A + #81CFEA + \ No newline at end of file diff --git a/core/src/main/res/values-v14/dimens.xml b/core/src/main/res/values-v14/dimens.xml new file mode 100644 index 000000000..090a476a8 --- /dev/null +++ b/core/src/main/res/values-v14/dimens.xml @@ -0,0 +1,5 @@ + + + 0dp + + \ No newline at end of file diff --git a/core/src/main/res/values-v14/styles.xml b/core/src/main/res/values-v14/styles.xml new file mode 100644 index 000000000..6a39d6175 --- /dev/null +++ b/core/src/main/res/values-v14/styles.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/core/src/main/res/values-v16/styles.xml b/core/src/main/res/values-v16/styles.xml new file mode 100644 index 000000000..e7c56b5f5 --- /dev/null +++ b/core/src/main/res/values-v16/styles.xml @@ -0,0 +1,17 @@ + + + + + + + \ No newline at end of file diff --git a/core/src/main/res/values-v19/colors.xml b/core/src/main/res/values-v19/colors.xml new file mode 100644 index 000000000..16c065d75 --- /dev/null +++ b/core/src/main/res/values-v19/colors.xml @@ -0,0 +1,5 @@ + + + #484B4D + #E3E3E3 + \ No newline at end of file diff --git a/core/src/main/res/values-zh-rCN/strings.xml b/core/src/main/res/values-zh-rCN/strings.xml new file mode 100644 index 000000000..63320b851 --- /dev/null +++ b/core/src/main/res/values-zh-rCN/strings.xml @@ -0,0 +1,317 @@ + + + + AntennaPod + 订阅 + 添加博客 + 播客 + 曲目 + 新曲目 + 所有曲目 + 最新 + 等待列表 + 设置 + 添加播客 + 下载 + 正在运行 + 已完成 + 日志 + 取消下载 + 播放历史 + gpodder.net + gpodder.net 登录 + + 最近发布 + 仅显示新曲目 + + 打开菜单 + 关闭菜单 + + 在浏览器打开 + 复制 URL + 分享 URL + 复制 URL 到剪贴板. + + 清空历史 + + 确定 + 取消 + 作者 + 语言 + 设置 + 图片 + 错误 + 出错: + 刷新 + 没有可用的外部存储. 请确保安装外部存储器, 这样本应用才可以正常工作. + 章节 + 笔记 + 描述 + 最近曲目:\u0020 + \u0020 曲 + 长度:\u0020 + 大小:\u0020 + 处理中 + 加载中... + 保存用户名密码 + 关闭 + 重试 + 包含到自动下载 + + 订阅 URL + 添加播客 URL + 您可以在 gpodder.net 通过名称、类别或热门来搜索新播客 + 浏览 gpodder.net + + 全部标识已读 + 将所有曲目标记为已读 + 查看信息 + 删除播客 + 分享网站链接 + 分享订阅链接 + 确认要删除这些订阅吗? 该订阅所有已经下载的曲目将一并删除. + 删除订阅 + + 下载 + 播放 + 暂停 + 流媒体 + 删除 + 移除曲目 + 标记已读 + 标记未读 + 添加到播放列表 + 从播放列表中删除 + 访问网站 + Flattr 他 + 全部添加到播放列表 + 全部下载 + 跳过曲目 + + 成功 + 失败 + 下载等待 + 下载中 + 没有找到存储设备 + 空间不足 + 文件错误 + HTTP 数据错误 + 未知错误 + 解析异常 + 未提供的订阅类型 + 链接错误 + 未知主机 + 认证错误 + 取消所有下载 + 已取消下载 + 下载完成 + 畸形 URL + IO 错误 + 请求出错 + 数据库访问错误 + \u0020 下载剩余 + 正在处理下载 + 下载播客数据 + %1$d 下载成功, %2$d 失败 + 未知标题 + 订阅 + 媒体文件 + 图片 + 尝试下载文件:\u0020 时出错 + 需要认证 + 您所请求的资源需要用户名和密码 + + 错误! + 没有可播放媒体 + 预备 + 准备 + 查找 + 服务器宕机 + 未知错误 + 没有可播放的媒体 + 00:00:00 + 缓冲中 + 播客播放中 + + 清空播放列表 + 撤消 + 已删除项 + 移到顶端 + 移到下部 + + Flattr 登录 + 按下面的按钮开始身份验证过程. 将在浏览器中打开 Flattr 登录界面并要求给予 AntennaPod 访问 Flattr 的权限. 权限许可后, 将自动回到这个界面. + 验证 + 返回主页 + 验证成功! 现在可以使用应用内 Flattr 相关功能了. + 没有找到 Flattr 验证令牌信息 + 您的 flattr 账户似乎并没有连接到 AntennaPod. You can either connect your account to AntennaPod to flattr things within the app or you can visit the website of the thing to flattr it there. + 验证 + 被禁止 + AntennaPod 没有权限执行本动作. 原因可能是: AntennaPod 对您账户的访问令牌被撤销. 你可以重新\"验证\"或访问该网站来授权. + 撤销访问 + 您已经成功撤销 AntennaPod 对账户令牌的访问. 为了完成这个过程, 您必须到 Flattr 网站 \"账户设置->已批准应用\" 列表内删除本应用. + + + 插件下载 + 插件没有安装 + 安装第三方库后播放速度设置起作用.\n点击 \'插件下载\' 从 \'Pay 商店\' 下载免费插件.\n使用这些插件中碰到的任何问题请报告给插件作者, 跟 AntennaPod 无关. + 播放速度 + + 列表为空. + 还没有任何订阅. + + 其他 + 关于 + 播放列表 + 服务 + Flattr + 耳机断开时暂停播放 + 播放完成跳转到播放列表下一项 + 播放 + 网络 + 更新周期 + 设置订阅自动刷新周期 + 仅在 WIFI 情况下载媒体文件 + 连续播放 + 仅在 WIFI 情况下载 + 耳机断开 + 数据网络时更新 + 允许移动数据网络情况下进行数据链接 + 刷新中 + Flattr 设置 + Flattr 登录 + 登录 Flattr 账户, 以便直接使用本应用中的相关功能. + Flattr 本应用 + 支持 AntennaPod 发展, 请 Flattring 他. 谢谢!!\n + 撤销访问 + 撤销访问本应用对您 Flattr 账户的访问权限. + 界面 + 主题选择 + 改变 AntennaPod 外观 + 自动下载 + 配置自动下载的曲目 + 打开 Wi-Fi 过滤器 + 只允许在 Wi-Fi 网络下自动下载 + 曲目缓存 + 浅色 + 暗色 + 无限 + 小时 + + 手动 + 登录 + 登录 gpodder.net 账户同步订阅 + 注销 + 注销成功 + 改变登录信息 + 改变 gpodder.net 账户登录信息. + 播放速度 + 自定义音频播放速度 + 设置主机名 + 使用默认主机 + + + 搜索订阅或者曲目 + 笔记中查找 + 章节中查找 + 没有找到任何结果 + 搜索 + 标题中查找 + + OPML 文件可以方便的从别的播客转移数据过来。 + 导入 OPML 文件, 您必须将它放在以下目录, 之后按下面的按钮开始导入处理. + 开始导入 + OPML 导入 + 错误! + OPML 文件读取中 + 读取 OPML 文件内容出错: + 导入目录为空. + 全选 + 取消所有选择 + 选择导入文件 + OPML 导出 + 导出中... + 导出出错 + OPML 导出成功. + .opml 文件已保存到:\u0020 + + 设置休眠计时器 + 禁用休眠计时器 + 输入时间 + 休眠计时器 + 计时剩余:\u0020 + 无效的输入, 时间是一个整数 + + 分钟 + 小时 + + 目录 + 头条播客 + 建议 + 搜索 gpodder.net + 登录 + 欢迎进入 gpodder.net 登录流程. 首先, 输入请你的登录信息: + 登录 + 如果还没有账户, 从这里创建:⏎\nhttps://gpodder.net/register/ + 用户名 + 密码 + 设备选择 + 为你的 gpodder.net 账户创建一个新设备或者选择一个已存在的: + 设备编号: \u0020 + 标题 + 创建新设备 + 选择已存在设备 + 设备编号必须填写 + 设备编号已被使用 + 选择 + 登录成功! + 恭喜! 你的 gpodder.net 帐户与设备已连结完成. 现在开始 AntennaPod 将自动同步你 gpodder.net 帐户内的订阅信息到设备上. + 开始同步 + 返回主屏 + gpodder.net 验证错误 + 错误的用户名或者密码 + gpodder.net 同步错误 + 同步过程中发生错误: \u0020 + + 已选文件夹: + 穿件文件夹 + 选择数据文件夹 + 确实创建 \"%1$s\" 文件夹? + 创建新文件夹 + 本文件夹不能写入 + 文件夹已存在 + 不能创建文件夹 + 文件夹不能为空 + 您所选择的文件夹不能为空. 媒体下载和其他文件将被直接放在本文件夹. 确认继续吗? + 选择默认文件夹 + 当另一个应用程序要播放声音时暂停播放, 而不是降低音量 + 中断暂停 + + 订阅 + 已订阅 + 下载中... + + 显示章节 + 显示笔记 + 显示图片 + 回放 + 快进 + 音频 + 视频 + 向上导航 + 更多动作 + 曲目正在播放 + 曲目正在下载 + 曲目已下载 + 新项目 + 曲目已经在播放列表中 + 新曲目数 + 已收听曲目数 + 拖动以变更本项目的位置 + + 验证 + 给本播客及曲目变更用户名及密码 + + 正在从选定的应用中导入订阅... + diff --git a/core/src/main/res/values/arrays.xml b/core/src/main/res/values/arrays.xml new file mode 100644 index 000000000..f09c76080 --- /dev/null +++ b/core/src/main/res/values/arrays.xml @@ -0,0 +1,114 @@ + + + + + 5 + 10 + 15 + 20 + 30 + 45 + 60 + + + + Manual + 1 hour + 2 hours + 4 hours + 8 hours + 12 hours + 24 hours + + + + 0 + 1 + 2 + 4 + 8 + 12 + 24 + + + @string/pref_episode_cache_unlimited + 10 + 20 + 40 + 60 + 80 + 100 + + + -1 + 10 + 20 + 40 + 60 + 80 + 100 + + + 0.5 + 0.6 + 0.7 + 0.8 + 0.9 + 1.0 + 1.05 + 1.10 + 1.15 + 1.20 + 1.25 + 1.30 + 1.35 + 1.40 + 1.45 + 1.50 + 1.55 + 1.60 + 1.65 + 1.70 + 1.75 + 1.80 + 1.85 + 1.90 + 1.95 + 2.00 + 2.10 + 2.20 + 2.30 + 2.40 + 2.50 + 2.60 + 2.70 + 2.80 + 2.90 + 3.00 + 3.10 + 3.20 + 3.30 + 3.40 + 3.50 + 3.60 + 3.70 + 3.80 + 3.90 + 4.00 + + + + N/A + + + 0 + + + @string/pref_theme_title_light + @string/pref_theme_title_dark + + + 0 + 1 + + \ No newline at end of file diff --git a/core/src/main/res/values/attrs.xml b/core/src/main/res/values/attrs.xml new file mode 100644 index 000000000..08a8063c1 --- /dev/null +++ b/core/src/main/res/values/attrs.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/core/src/main/res/values/colors.xml b/core/src/main/res/values/colors.xml new file mode 100644 index 000000000..6b535079d --- /dev/null +++ b/core/src/main/res/values/colors.xml @@ -0,0 +1,24 @@ + + + + #FFFFFF + #808080 + #000000 + #33B5E5 + #858585 + #DDDDDD + #669900 + #CC0000 + #E033B5E5 + #E0EE5F52 + #262C31 + #DDDDDD + #EDEDED + #060708 + #669900 + + + #FEBB20 + #FEBB20 + + \ No newline at end of file diff --git a/core/src/main/res/values/dimens.xml b/core/src/main/res/values/dimens.xml new file mode 100644 index 000000000..1ebcdb76d --- /dev/null +++ b/core/src/main/res/values/dimens.xml @@ -0,0 +1,23 @@ + + + + 8dp + 70dp + 70dp + 20dp + 12sp + 14sp + 16sp + 18sp + 22sp + 32dp + 85dp + 70dp + 70dp + 110dp + 42dp + 48dp + 280dp + @dimen/text_size_small + + \ No newline at end of file diff --git a/core/src/main/res/values/ids.xml b/core/src/main/res/values/ids.xml new file mode 100644 index 000000000..90e405fde --- /dev/null +++ b/core/src/main/res/values/ids.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/core/src/main/res/values/integers.xml b/core/src/main/res/values/integers.xml new file mode 100644 index 000000000..33501d9fb --- /dev/null +++ b/core/src/main/res/values/integers.xml @@ -0,0 +1,4 @@ + + 5000 + -1 + diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml new file mode 100644 index 000000000..b5cc4ee86 --- /dev/null +++ b/core/src/main/res/values/strings.xml @@ -0,0 +1,374 @@ + + + + + AntennaPod + Feeds + Add podcast + PODCASTS + EPISODES + New episodes + All episodes + New + Waiting list + Settings + Add podcast + Downloads + Running + Completed + Log + Cancel Download + Playback history + gpodder.net + gpodder.net login + + + Recently published + Show only new episodes + + + Open menu + Close menu + + + Open in browser + Copy URL + Share URL + Copied URL to clipboard. + Go to this position + + + Clear history + + + Confirm + Cancel + Author + Language + Settings + Picture + Error + An error occurred: + Refresh + No external storage is available. Please make sure that external storage is mounted so that the app can work properly. + Chapters + Shownotes + Description + Most Recent Episode:\u0020 + \u0020episodes + Length:\u0020 + Size:\u0020 + Processing + Loading... + Save username and password + Close + Retry + Include in auto downloads + + + Feed URL + URL of feed or website + Add Podcast by URL + Find podcast in directory + You can search for new podcasts by name, category or popularity in the gpodder.net directory. + Browse gpodder.net + + + Mark all as read + Marked all episodes as read + Show information + Remove podcast + Share website link + Share feed link + Please confirm that you want to delete this feed and ALL episodes of this feed that you have downloaded. + Removing feed + + + Download + Play + Pause + Stream + Remove + Remove episode + Mark as read + Mark as unread + Add to Queue + Remove from Queue + Visit Website + Flattr this + Enqueue all + Download all + Skip episode + + + successful + failed + Download pending + Download running + Storage device not found + Insufficient space + File error + HTTP Data Error + Unknown Error + Parser Exception + Unsupported Feed type + Connection error + Unknown host + Authentication error + Cancel all downloads + Download cancelled + Downloads completed + Malformed URL + IO Error + Request error + Database access error + \u0020Downloads left + Processing downloads + Downloading podcast data + %1$d downloads succeeded, %2$d failed + Unknown title + Feed + Media file + Image + An error occurred when trying to download the file:\u0020 + Authentication required + The resource you requested requires a username and a password + + + Error! + No media playing + Preparing + Ready + Seeking + Server died + Unknown Error + No media playing + 00:00:00 + Buffering + Playing podcast + AntennaPod - Unknown media key: %1$d + + + Clear queue + Undo + Item removed + Move to top + Move to bottom + + + Flattr sign-in + Press the button below to start the authentication process. You will be forwarded to the flattr login screen in your browser and be asked to give AntennaPod the permission to flattr things. After you have given permission, you will return to this screen automatically. + Authenticate + Return to home + Authentication was successful! You can now flattr things within the app. + No Flattr token found + Your flattr account does not seem to be connected to AntennaPod. Tap here to authenticate. + Your flattr account does not seem to be connected to AntennaPod. You can either connect your account to AntennaPod to flattr things within the app or you can visit the website of the thing to flattr it there. + Authenticate + Action forbidden + AntennaPod has no permission for this action. The reason for this could be that the access token of AntennaPod to your account has been revoked. You can either re-reauthenticate or visit the website of the thing instead. + Access revoked + You have successfully revoked AntennaPod\'s access token to your account. In order to complete the process, you have to remove this app from the list of approved applications in your account settings on the flattr website. + + + Flattr\'ed one thing! + Flattr\'ed %d things! + Flattr\'ed: %s. + Failed to flattr %d things! + Not flattr\'ed: %s. + Thing will be flattr\'ed later + Flattring %s + AntennaPod is flattring + AntennaPod has flattr\'ed + AntennaPod flattr failed + Retrieving flattr\'ed things + + + Download Plugin + Plugin Not Installed + For variable speed playback to work, a third party library must be installed.\n\nTap \'Download Plugin\' to download a free plugin from the Play Store\n\nAny problems found using this plugin are not the responsibility of AntennaPod and should be reported to the plugin owner. + Playback Speeds + + + There are no items in this list. + You haven\'t subscribed to any feeds yet. + + + Other + About + Queue + Services + Flattr + Pause playback when the headphones are disconnected + Jump to next queue item when playback completes + Playback + Network + Update interval + Specify an interval in which the feeds are refreshed automatically or disable it + Download media files only over WiFi + Continuous playback + WiFi media download + Headphones disconnect + Mobile updates + Allow updates over the mobile data connection + Refreshing + Flattr settings + Flattr sign-in + Sign in to your flattr account to flattr things directly from the app. + Flattr this app + Support the development of AntennaPod by flattring it. Thanks! + Revoke access + Revoke the access permission to your flattr account for this app. + Automatic Flattr + Configure automatic flattring + User Interface + Select theme + Change the appearance of AntennaPod. + Automatic download + Configure the automatic download of episodes. + Enable Wi-Fi filter + Allow automatic download only for selected Wi-Fi networks. + Episode cache + Light + Dark + Unlimited + hours + hour + Manual + Login + Login with your gpodder.net account in order to sync your subscriptions. + Logout + Logout was successful + Change login information + Change the login information for your gpodder.net account. + Playback Speeds + Customize the speeds available for variable speed audio playback + Seek time + Seek this many seconds when rewinding or fast-forwarding + Set hostname + Use default host + + + Enable automatic flattring + Flattr episode as soon as %d percent have been played + Flattr episode when playback starts + Flattr episode when playback ends + + + Search for Feeds or Episodes + Found in shownotes + Found in chapters + No results were found + Search + Found in title + + + OPML files allow you to move your podcasts from one podcatcher to another. + To import an OPML file, you have to place it in the following directory and press the button below to start the import process. + Start import + OPML import + ERROR! + Reading OPML file + An error has occurred while reading the opml document: + The import directory is empty. + Select all + Deselect all + Choose file to import + OPML export + Exporting... + Export error + Opml export successful. + The .opml file was written to:\u0020 + + + Set sleep timer + Disable sleep timer + Enter time + Sleep timer + Time left:\u0020 + Invalid input, time has to be an integer + seconds + minutes + hours + + + CATEGORIES + TOP PODCASTS + SUGGESTIONS + Search gpodder.net + Login + Welcome to the gpodder.net login process. First, type in your login information: + Login + If you do not have an account yet, you can create one here:\nhttps://gpodder.net/register/ + Username + Password + Device Selection + Create a new device to use for your gpodder.net account or choose an existing one: + Device ID:\u0020 + Caption + Create new device + Choose existing device: + Device ID must not be empty + Device ID already in use + + Choose + Login successful! + Congratulations! Your gpodder.net account is now linked with your device. AntennaPod will from now on automatically sync subscriptions on your device with your gpodder.net account. + Start sync now + Go to main screen + + gpodder.net authentication error + Wrong username or password + gpodder.net sync error + An error occurred during syncing:\u0020 + + + Selected folder: + Create folder + Choose data folder + Create new folder with name "%1$s"? + Created new folder + Cannot write to this folder + Folder already exists + Could not create folder + Folder is not empty + The folder you have selected is not empty. Media downloads and other files will be placed directly in this folder. Continue anyway? + Choose default folder + Pause playback instead of lowering volume when another app wants to play sounds + Pause for interruptions + + + Subscribe + Subscribed + Downloading... + + + Show chapters + Show shownotes + Show picture + Rewind + Fast forward + Audio + Video + Navigate upwards + More actions + Episode is being played + Episode is being downloaded + Episode is downloaded + Item is new + Episode is in the queue + Number of new episodes + Number of episodes you have started listening to + Drag to change the position of this item + + + Authentication + Change your username and password for this podcast and its episodes. + + + + Importing subscriptions from single-purpose apps… + diff --git a/core/src/main/res/values/styles.xml b/core/src/main/res/values/styles.xml new file mode 100644 index 000000000..e42072afa --- /dev/null +++ b/core/src/main/res/values/styles.xml @@ -0,0 +1,174 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/settings.gradle b/settings.gradle index d33586960..de34bc1c1 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,2 +1,2 @@ -include ':app' +include ':app', ':core' include ':app:dslv:library' -- cgit v1.2.3