From edb440a5a9a05e24c344a71b272b9238217e9c55 Mon Sep 17 00:00:00 2001 From: ByteHamster Date: Sun, 31 Mar 2024 18:40:15 +0200 Subject: Restructure related UI classes together (#7044) --- app/build.gradle | 4 + .../java/de/test/antennapod/EspressoTestUtils.java | 2 +- .../test/antennapod/dialogs/ShareDialogTest.java | 2 +- .../test/antennapod/ui/NavigationDrawerTest.java | 12 +- .../de/test/antennapod/ui/PreferencesTest.java | 2 +- .../de/test/antennapod/ui/QueueFragmentTest.java | 2 +- app/src/main/AndroidManifest.xml | 31 +- .../de/danoeh/antennapod/ClientConfigurator.java | 2 - .../de/danoeh/antennapod/CrashReportWriter.java | 67 ++ .../main/java/de/danoeh/antennapod/PodcastApp.java | 24 +- .../de/danoeh/antennapod/PreferenceUpgrader.java | 170 +++++ .../danoeh/antennapod/RxJavaErrorHandlerSetup.java | 35 + .../actionbutton/CancelDownloadActionButton.java | 41 ++ .../actionbutton/DeleteActionButton.java | 53 ++ .../actionbutton/DownloadActionButton.java | 74 ++ .../antennapod/actionbutton/ItemActionButton.java | 64 ++ .../actionbutton/MarkAsPlayedActionButton.java | 41 ++ .../antennapod/actionbutton/PauseActionButton.java | 42 ++ .../antennapod/actionbutton/PlayActionButton.java | 60 ++ .../actionbutton/PlayLocalActionButton.java | 46 ++ .../actionbutton/StreamActionButton.java | 56 ++ .../actionbutton/VisitWebsiteActionButton.java | 38 + .../antennapod/activity/BugReportActivity.java | 128 ---- .../danoeh/antennapod/activity/MainActivity.java | 32 +- .../activity/OnlineFeedViewActivity.java | 713 ------------------ .../activity/PlaybackSpeedDialogActivity.java | 29 - .../antennapod/activity/PreferenceActivity.java | 194 ----- .../danoeh/antennapod/activity/SplashActivity.java | 2 +- .../antennapod/activity/VideoplayerActivity.java | 815 --------------------- .../antennapod/adapter/ChaptersListAdapter.java | 177 ----- .../de/danoeh/antennapod/adapter/CoverLoader.java | 132 ---- .../antennapod/adapter/DownloadLogAdapter.java | 155 ---- .../antennapod/adapter/EpisodeItemListAdapter.java | 234 ------ .../adapter/FeedItemlistDescriptionAdapter.java | 112 --- .../adapter/HorizontalFeedListAdapter.java | 143 ---- .../adapter/HorizontalItemListAdapter.java | 138 ---- .../danoeh/antennapod/adapter/NavListAdapter.java | 420 ----------- .../antennapod/adapter/QueueRecyclerAdapter.java | 95 --- .../antennapod/adapter/SelectableAdapter.java | 200 ----- .../antennapod/adapter/SimpleChipAdapter.java | 57 -- .../adapter/SubscriptionsRecyclerAdapter.java | 308 -------- .../actionbutton/CancelDownloadActionButton.java | 41 -- .../adapter/actionbutton/DeleteActionButton.java | 53 -- .../adapter/actionbutton/DownloadActionButton.java | 74 -- .../adapter/actionbutton/ItemActionButton.java | 64 -- .../actionbutton/MarkAsPlayedActionButton.java | 41 -- .../adapter/actionbutton/PauseActionButton.java | 42 -- .../adapter/actionbutton/PlayActionButton.java | 60 -- .../actionbutton/PlayLocalActionButton.java | 46 -- .../adapter/actionbutton/StreamActionButton.java | 56 -- .../actionbutton/VisitWebsiteActionButton.java | 38 - .../antennapod/dialog/AllEpisodesFilterDialog.java | 31 - .../dialog/DownloadLogDetailsDialog.java | 65 -- .../antennapod/dialog/DrawerPreferencesDialog.java | 50 -- .../antennapod/dialog/EditUrlSettingsDialog.java | 88 --- .../antennapod/dialog/EpisodeFilterDialog.java | 112 --- .../antennapod/dialog/FeedItemFilterDialog.java | 26 - .../dialog/FeedPreferenceSkipDialog.java | 48 -- .../danoeh/antennapod/dialog/FeedSortDialog.java | 39 - .../danoeh/antennapod/dialog/ItemFilterDialog.java | 123 ---- .../danoeh/antennapod/dialog/ItemSortDialog.java | 104 --- .../antennapod/dialog/MediaPlayerErrorDialog.java | 31 - .../antennapod/dialog/PlaybackControlsDialog.java | 78 -- .../de/danoeh/antennapod/dialog/ProxyDialog.java | 316 -------- .../danoeh/antennapod/dialog/RemoveFeedDialog.java | 84 --- .../danoeh/antennapod/dialog/RenameItemDialog.java | 81 -- .../de/danoeh/antennapod/dialog/ShareDialog.java | 100 --- .../antennapod/dialog/SkipPreferenceDialog.java | 64 -- .../danoeh/antennapod/dialog/SleepTimerDialog.java | 214 ------ .../dialog/StreamingConfirmationDialog.java | 38 - .../dialog/SubscriptionsFilterDialog.java | 133 ---- .../antennapod/dialog/SwipeActionsDialog.java | 223 ------ .../antennapod/dialog/TagSettingsDialog.java | 156 ---- .../danoeh/antennapod/dialog/TimeRangeDialog.java | 187 ----- .../antennapod/dialog/VariableSpeedDialog.java | 181 ----- .../dialog/rating/RatingDialogFragment.java | 79 -- .../dialog/rating/RatingDialogManager.java | 94 --- .../danoeh/antennapod/error/CrashReportWriter.java | 67 -- .../antennapod/error/RxJavaErrorHandlerSetup.java | 36 - .../antennapod/fragment/AddFeedFragment.java | 216 ------ .../antennapod/fragment/AllEpisodesFragment.java | 147 ---- .../antennapod/fragment/AudioPlayerFragment.java | 562 -------------- .../antennapod/fragment/ChaptersFragment.java | 192 ----- .../fragment/CompletedDownloadsFragment.java | 392 ---------- .../danoeh/antennapod/fragment/CoverFragment.java | 341 --------- .../antennapod/fragment/DownloadLogFragment.java | 123 ---- .../antennapod/fragment/EpisodesListFragment.java | 463 ------------ .../fragment/ExternalPlayerFragment.java | 222 ------ .../antennapod/fragment/FeedInfoFragment.java | 362 --------- .../antennapod/fragment/FeedItemlistFragment.java | 653 ----------------- .../antennapod/fragment/FeedSettingsFragment.java | 529 ------------- .../danoeh/antennapod/fragment/InboxFragment.java | 153 ---- .../fragment/ItemDescriptionFragment.java | 189 ----- .../danoeh/antennapod/fragment/ItemFragment.java | 435 ----------- .../antennapod/fragment/ItemPagerFragment.java | 188 ----- .../antennapod/fragment/NavDrawerFragment.java | 479 ------------ .../fragment/PlaybackHistoryFragment.java | 109 --- .../danoeh/antennapod/fragment/QueueFragment.java | 639 ---------------- .../danoeh/antennapod/fragment/SearchFragment.java | 460 ------------ .../antennapod/fragment/SubscriptionFragment.java | 367 ---------- .../antennapod/fragment/TransitionEffect.java | 5 - .../actions/EpisodeMultiSelectActionHandler.java | 133 ---- .../actions/FeedMultiSelectActionHandler.java | 138 ---- .../preferences/DownloadsPreferencesFragment.java | 108 --- .../ImportExportPreferencesFragment.java | 413 ----------- .../preferences/MainPreferencesFragment.java | 157 ---- .../preferences/PlaybackPreferencesFragment.java | 128 ---- .../preferences/SwipePreferencesFragment.java | 59 -- .../UserInterfacePreferencesFragment.java | 171 ----- .../preferences/dialog/PreferenceListDialog.java | 49 -- .../preferences/dialog/PreferenceSwitchDialog.java | 57 -- .../swipeactions/AddToQueueSwipeAction.java | 47 -- .../fragment/swipeactions/DeleteSwipeAction.java | 50 -- .../swipeactions/MarkFavoriteSwipeAction.java | 43 -- .../swipeactions/RemoveFromHistorySwipeAction.java | 58 -- .../swipeactions/RemoveFromInboxSwipeAction.java | 45 -- .../swipeactions/RemoveFromQueueSwipeAction.java | 57 -- .../swipeactions/ShowFirstSwipeDialogAction.java | 42 -- .../swipeactions/StartDownloadSwipeAction.java | 44 -- .../fragment/swipeactions/SwipeAction.java | 36 - .../fragment/swipeactions/SwipeActions.java | 268 ------- .../TogglePlaybackStateSwipeAction.java | 48 -- .../menuhandler/FeedItemMenuHandler.java | 282 ------- .../antennapod/menuhandler/FeedMenuHandler.java | 63 -- .../antennapod/preferences/PreferenceUpgrader.java | 170 ----- .../preferences/VolumeAdaptationPreference.java | 28 - .../receiver/ConnectivityActionReceiver.java | 25 - .../receiver/PowerConnectionReceiver.java | 48 -- .../de/danoeh/antennapod/receiver/SPAReceiver.java | 54 -- .../java/de/danoeh/antennapod/spa/SPAReceiver.java | 54 ++ .../java/de/danoeh/antennapod/spa/SPAUtil.java | 1 - .../antennapod/ui/AllEpisodesFilterDialog.java | 32 + .../java/de/danoeh/antennapod/ui/CoverLoader.java | 132 ++++ .../danoeh/antennapod/ui/FeedItemFilterDialog.java | 27 + .../de/danoeh/antennapod/ui/MenuItemUtils.java | 28 + .../de/danoeh/antennapod/ui/SelectableAdapter.java | 200 +++++ .../de/danoeh/antennapod/ui/SimpleChipAdapter.java | 57 ++ .../antennapod/ui/StreamingConfirmationDialog.java | 38 + .../de/danoeh/antennapod/ui/TransitionEffect.java | 5 + .../antennapod/ui/cleaner/HtmlToPlainText.java | 123 ++++ .../antennapod/ui/cleaner/ShownotesCleaner.java | 208 ++++++ .../ui/episodeslist/EpisodeItemListAdapter.java | 233 ++++++ .../episodeslist/EpisodeItemListRecyclerView.java | 84 +++ .../ui/episodeslist/EpisodeItemViewHolder.java | 279 +++++++ .../EpisodeMultiSelectActionHandler.java | 133 ++++ .../ui/episodeslist/EpisodesListFragment.java | 457 ++++++++++++ .../ui/episodeslist/FeedItemMenuHandler.java | 282 +++++++ .../ui/episodeslist/HorizontalItemListAdapter.java | 136 ++++ .../ui/episodeslist/HorizontalItemViewHolder.java | 130 ++++ .../ui/episodeslist/MediaSizeLoader.java | 78 ++ .../ui/episodeslist/MoreContentListFooterUtil.java | 53 ++ .../de/danoeh/antennapod/ui/home/HomeFragment.java | 207 ------ .../de/danoeh/antennapod/ui/home/HomeSection.java | 105 --- .../ui/home/HomeSectionsSettingsDialog.java | 42 -- .../home/sections/AllowNotificationsSection.java | 68 -- .../ui/home/sections/DownloadsSection.java | 140 ---- .../antennapod/ui/home/sections/EchoSection.java | 71 -- .../ui/home/sections/EpisodesSurpriseSection.java | 155 ---- .../antennapod/ui/home/sections/InboxSection.java | 141 ---- .../antennapod/ui/home/sections/QueueSection.java | 163 ----- .../ui/home/sections/SubscriptionsSection.java | 111 --- .../antennapod/ui/screen/AddFeedFragment.java | 217 ++++++ .../antennapod/ui/screen/AllEpisodesFragment.java | 148 ++++ .../danoeh/antennapod/ui/screen/InboxFragment.java | 154 ++++ .../ui/screen/PlaybackHistoryFragment.java | 110 +++ .../antennapod/ui/screen/SearchFragment.java | 460 ++++++++++++ .../ui/screen/chapter/ChaptersFragment.java | 191 +++++ .../ui/screen/chapter/ChaptersListAdapter.java | 177 +++++ .../download/CompletedDownloadsFragment.java | 393 ++++++++++ .../ui/screen/download/DownloadErrorLabel.java | 46 ++ .../ui/screen/download/DownloadLogAdapter.java | 153 ++++ .../screen/download/DownloadLogDetailsDialog.java | 64 ++ .../ui/screen/download/DownloadLogFragment.java | 121 +++ .../screen/download/DownloadLogItemViewHolder.java | 40 + .../ui/screen/drawer/DrawerPreferencesDialog.java | 50 ++ .../ui/screen/drawer/NavDrawerFragment.java | 484 ++++++++++++ .../ui/screen/drawer/NavListAdapter.java | 419 +++++++++++ .../antennapod/ui/screen/episode/ItemFragment.java | 436 +++++++++++ .../ui/screen/episode/ItemPagerFragment.java | 189 +++++ .../ui/screen/feed/FeedInfoFragment.java | 362 +++++++++ .../ui/screen/feed/FeedItemFilterGroup.java | 37 + .../ui/screen/feed/FeedItemlistFragment.java | 654 +++++++++++++++++ .../ui/screen/feed/ItemFilterDialog.java | 122 +++ .../antennapod/ui/screen/feed/ItemSortDialog.java | 104 +++ .../ui/screen/feed/RemoveFeedDialog.java | 84 +++ .../ui/screen/feed/RenameFeedDialog.java | 81 ++ .../ui/screen/feed/ToolbarIconTintManager.java | 59 ++ .../feed/preferences/EditUrlSettingsDialog.java | 88 +++ .../feed/preferences/EpisodeFilterDialog.java | 112 +++ .../feed/preferences/FeedPreferenceSkipDialog.java | 48 ++ .../feed/preferences/FeedSettingsFragment.java | 526 +++++++++++++ .../feed/preferences/SkipPreferenceDialog.java | 64 ++ .../screen/feed/preferences/TagSettingsDialog.java | 156 ++++ .../preferences/VolumeAdaptationPreference.java | 28 + .../antennapod/ui/screen/home/HomeFragment.java | 207 ++++++ .../antennapod/ui/screen/home/HomeSection.java | 105 +++ .../ui/screen/home/HomeSectionsSettingsDialog.java | 42 ++ .../home/sections/AllowNotificationsSection.java | 68 ++ .../ui/screen/home/sections/DownloadsSection.java | 140 ++++ .../ui/screen/home/sections/EchoSection.java | 71 ++ .../home/sections/EpisodesSurpriseSection.java | 155 ++++ .../ui/screen/home/sections/InboxSection.java | 141 ++++ .../ui/screen/home/sections/QueueSection.java | 163 +++++ .../screen/home/sections/SubscriptionsSection.java | 111 +++ .../ui/screen/onlinefeedview/FeedDiscoverer.java | 79 ++ .../FeedItemlistDescriptionAdapter.java | 112 +++ .../onlinefeedview/OnlineFeedViewActivity.java | 711 ++++++++++++++++++ .../ui/screen/playback/MediaPlayerErrorDialog.java | 31 + .../antennapod/ui/screen/playback/PlayButton.java | 52 ++ .../ui/screen/playback/PlaybackControlsDialog.java | 78 ++ .../playback/PlaybackSpeedDialogActivity.java | 29 + .../ui/screen/playback/PlaybackSpeedSeekBar.java | 76 ++ .../ui/screen/playback/SleepTimerDialog.java | 214 ++++++ .../ui/screen/playback/TimeRangeDialog.java | 187 +++++ .../ui/screen/playback/VariableSpeedDialog.java | 180 +++++ .../screen/playback/audio/AudioPlayerFragment.java | 561 ++++++++++++++ .../ui/screen/playback/audio/ChapterSeekBar.java | 148 ++++ .../ui/screen/playback/audio/CoverFragment.java | 342 +++++++++ .../playback/audio/ExternalPlayerFragment.java | 222 ++++++ .../playback/audio/ItemDescriptionFragment.java | 189 +++++ .../screen/playback/audio/NoRelayoutTextView.java | 42 ++ .../playback/video/AspectRatioVideoView.java | 115 +++ .../playback/video/PictureInPictureUtil.java | 27 + .../screen/playback/video/VideoplayerActivity.java | 815 +++++++++++++++++++++ .../ui/screen/preferences/BugReportActivity.java | 128 ++++ .../preferences/DownloadsPreferencesFragment.java | 106 +++ .../ImportExportPreferencesFragment.java | 412 +++++++++++ .../preferences/MainPreferencesFragment.java | 155 ++++ .../preferences/PlaybackPreferencesFragment.java | 127 ++++ .../ui/screen/preferences/PreferenceActivity.java | 188 +++++ .../screen/preferences/PreferenceListDialog.java | 49 ++ .../screen/preferences/PreferenceSwitchDialog.java | 57 ++ .../ui/screen/preferences/ProxyDialog.java | 316 ++++++++ .../preferences/SwipePreferencesFragment.java | 58 ++ .../UserInterfacePreferencesFragment.java | 170 +++++ .../antennapod/ui/screen/queue/QueueFragment.java | 639 ++++++++++++++++ .../ui/screen/queue/QueueRecyclerAdapter.java | 96 +++ .../ui/screen/rating/RatingDialogFragment.java | 79 ++ .../ui/screen/rating/RatingDialogManager.java | 94 +++ .../ui/screen/subscriptions/FeedMenuHandler.java | 63 ++ .../FeedMultiSelectActionHandler.java | 138 ++++ .../ui/screen/subscriptions/FeedSortDialog.java | 39 + .../subscriptions/HorizontalFeedListAdapter.java | 143 ++++ .../screen/subscriptions/SubscriptionFragment.java | 364 +++++++++ .../subscriptions/SubscriptionsFilterDialog.java | 132 ++++ .../subscriptions/SubscriptionsFilterGroup.java | 33 + .../SubscriptionsRecyclerAdapter.java | 309 ++++++++ .../de/danoeh/antennapod/ui/share/ShareDialog.java | 99 +++ .../de/danoeh/antennapod/ui/share/ShareUtils.java | 92 +++ .../ui/swipeactions/AddToQueueSwipeAction.java | 47 ++ .../ui/swipeactions/DeleteSwipeAction.java | 50 ++ .../ui/swipeactions/MarkFavoriteSwipeAction.java | 43 ++ .../swipeactions/RemoveFromHistorySwipeAction.java | 58 ++ .../swipeactions/RemoveFromInboxSwipeAction.java | 45 ++ .../swipeactions/RemoveFromQueueSwipeAction.java | 57 ++ .../swipeactions/ShowFirstSwipeDialogAction.java | 42 ++ .../ui/swipeactions/StartDownloadSwipeAction.java | 44 ++ .../antennapod/ui/swipeactions/SwipeAction.java | 36 + .../antennapod/ui/swipeactions/SwipeActions.java | 267 +++++++ .../ui/swipeactions/SwipeActionsDialog.java | 213 ++++++ .../TogglePlaybackStateSwipeAction.java | 48 ++ .../antennapod/ui/view/EmptyViewHandler.java | 152 ++++ .../antennapod/ui/view/ItemOffsetDecoration.java | 24 + .../antennapod/ui/view/LiftOnScrollListener.java | 59 ++ .../antennapod/ui/view/LocalDeleteModal.java | 32 + .../ui/view/LockableBottomSheetBehavior.java | 86 +++ .../antennapod/ui/view/NestedScrollableHost.java | 197 +++++ .../antennapod/ui/view/ShownotesWebView.java | 189 +++++ .../ui/view/SimpleAdapterDataObserver.java | 41 ++ .../antennapod/view/AspectRatioVideoView.java | 115 --- .../de/danoeh/antennapod/view/ChapterSeekBar.java | 148 ---- .../danoeh/antennapod/view/EmptyViewHandler.java | 152 ---- .../view/EpisodeItemListRecyclerView.java | 84 --- .../antennapod/view/ItemOffsetDecoration.java | 24 - .../antennapod/view/LiftOnScrollListener.java | 59 -- .../danoeh/antennapod/view/LocalDeleteModal.java | 32 - .../view/LockableBottomSheetBehavior.java | 86 --- .../antennapod/view/NestedScrollableHost.java | 197 ----- .../danoeh/antennapod/view/NoRelayoutTextView.java | 42 -- .../java/de/danoeh/antennapod/view/PlayButton.java | 52 -- .../antennapod/view/PlaybackSpeedSeekBar.java | 76 -- .../danoeh/antennapod/view/ShownotesWebView.java | 189 ----- .../antennapod/view/SimpleAdapterDataObserver.java | 41 -- .../antennapod/view/ToolbarIconTintManager.java | 59 -- .../view/viewholder/DownloadLogItemViewHolder.java | 40 - .../view/viewholder/EpisodeItemViewHolder.java | 280 ------- .../view/viewholder/HorizontalItemViewHolder.java | 130 ---- app/src/main/res/layout-sw720dp/main.xml | 2 +- app/src/main/res/layout/audioplayer_fragment.xml | 8 +- app/src/main/res/layout/episodes_list_fragment.xml | 2 +- .../main/res/layout/external_player_fragment.xml | 2 +- .../main/res/layout/feed_item_list_fragment.xml | 2 +- app/src/main/res/layout/feeditem_fragment.xml | 6 +- .../main/res/layout/item_description_fragment.xml | 15 +- app/src/main/res/layout/main.xml | 2 +- .../layout/playback_speed_feed_setting_dialog.xml | 2 +- app/src/main/res/layout/queue_fragment.xml | 2 +- app/src/main/res/layout/search_fragment.xml | 2 +- app/src/main/res/layout/simple_list_fragment.xml | 2 +- app/src/main/res/layout/speed_select_dialog.xml | 2 +- app/src/main/res/layout/videoplayer_activity.xml | 19 +- app/src/main/res/xml/feed_settings.xml | 2 +- .../ui/cleaner/ShownotesCleanerTest.java | 236 ++++++ .../screen/onlinefeedview/FeedDiscovererTest.java | 128 ++++ config/spotbugs/exclude.xml | 6 +- .../antennapod/core/dialog/ConfirmationDialog.java | 56 -- .../danoeh/antennapod/core/feed/ChapterMerger.java | 70 -- .../antennapod/core/feed/FeedItemFilterGroup.java | 37 - .../core/feed/SubscriptionsFilterGroup.java | 33 - .../antennapod/core/menuhandler/MenuItemUtils.java | 28 - .../core/storage/AutomaticDownloadAlgorithm.java | 21 +- .../danoeh/antennapod/core/util/ChapterMerger.java | 70 ++ .../danoeh/antennapod/core/util/ChapterUtils.java | 1 - .../antennapod/core/util/ConfirmationDialog.java | 56 ++ .../antennapod/core/util/DownloadErrorLabel.java | 46 -- .../de/danoeh/antennapod/core/util/PowerUtils.java | 30 - .../de/danoeh/antennapod/core/util/ShareUtils.java | 91 --- .../core/util/download/MediaSizeLoader.java | 78 -- .../download/NetworkConnectionChangeHandler.java | 29 - .../core/util/gui/MoreContentListFooterUtil.java | 53 -- .../core/util/gui/PictureInPictureUtil.java | 27 - .../antennapod/core/util/gui/ShownotesCleaner.java | 208 ------ .../core/util/syndication/FeedDiscoverer.java | 79 -- .../core/util/syndication/HtmlToPlainText.java | 123 ---- core/src/test/java/android/text/TextUtils.java | 42 -- .../core/feed/VolumeAdaptionSettingTest.java | 129 ---- .../core/storage/mapper/FeedCursorMapperTest.java | 101 --- .../danoeh/antennapod/core/util/ConverterTest.java | 40 - .../core/util/FeedItemPermutorsTest.java | 231 ------ .../core/util/FilenameGeneratorTest.java | 99 --- .../core/util/gui/ShownotesCleanerTest.java | 236 ------ .../core/util/syndication/FeedDiscovererTest.java | 128 ---- model/build.gradle | 4 +- .../java/de/danoeh/antennapod/model/feed/Feed.java | 9 +- .../antennapod/model/playback/RemoteMedia.java | 16 +- .../model/VolumeAdaptionSettingTest.java | 129 ++++ .../serviceinterface/FilenameGeneratorTest.java | 98 +++ net/download/service/src/main/AndroidManifest.xml | 18 + .../service/ConnectivityActionReceiver.java | 34 + .../download/service/PowerConnectionReceiver.java | 46 ++ storage/database/build.gradle | 3 + .../storage/database/FeedItemDuplicateGuesser.java | 8 +- .../storage/database/FeedItemPermutorsTest.java | 231 ++++++ .../database/mapper/FeedCursorMapperTest.java | 100 +++ ui/common/build.gradle | 2 + .../danoeh/antennapod/ui/common/ConverterTest.java | 40 + .../ui/statistics/StatisticsFragment.java | 2 +- 347 files changed, 22481 insertions(+), 22588 deletions(-) create mode 100644 app/src/main/java/de/danoeh/antennapod/CrashReportWriter.java create mode 100644 app/src/main/java/de/danoeh/antennapod/PreferenceUpgrader.java create mode 100644 app/src/main/java/de/danoeh/antennapod/RxJavaErrorHandlerSetup.java create mode 100644 app/src/main/java/de/danoeh/antennapod/actionbutton/CancelDownloadActionButton.java create mode 100644 app/src/main/java/de/danoeh/antennapod/actionbutton/DeleteActionButton.java create mode 100644 app/src/main/java/de/danoeh/antennapod/actionbutton/DownloadActionButton.java create mode 100644 app/src/main/java/de/danoeh/antennapod/actionbutton/ItemActionButton.java create mode 100644 app/src/main/java/de/danoeh/antennapod/actionbutton/MarkAsPlayedActionButton.java create mode 100644 app/src/main/java/de/danoeh/antennapod/actionbutton/PauseActionButton.java create mode 100644 app/src/main/java/de/danoeh/antennapod/actionbutton/PlayActionButton.java create mode 100644 app/src/main/java/de/danoeh/antennapod/actionbutton/PlayLocalActionButton.java create mode 100644 app/src/main/java/de/danoeh/antennapod/actionbutton/StreamActionButton.java create mode 100644 app/src/main/java/de/danoeh/antennapod/actionbutton/VisitWebsiteActionButton.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/activity/BugReportActivity.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/activity/PlaybackSpeedDialogActivity.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/activity/PreferenceActivity.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/activity/VideoplayerActivity.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/adapter/ChaptersListAdapter.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/adapter/CoverLoader.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/adapter/DownloadLogAdapter.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/adapter/EpisodeItemListAdapter.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/adapter/FeedItemlistDescriptionAdapter.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/adapter/HorizontalFeedListAdapter.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/adapter/HorizontalItemListAdapter.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/adapter/NavListAdapter.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/adapter/QueueRecyclerAdapter.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/adapter/SelectableAdapter.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/adapter/SimpleChipAdapter.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/adapter/SubscriptionsRecyclerAdapter.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/CancelDownloadActionButton.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/DeleteActionButton.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/DownloadActionButton.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/ItemActionButton.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/MarkAsPlayedActionButton.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/PauseActionButton.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/PlayActionButton.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/PlayLocalActionButton.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/StreamActionButton.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/VisitWebsiteActionButton.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/dialog/AllEpisodesFilterDialog.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/dialog/DownloadLogDetailsDialog.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/dialog/DrawerPreferencesDialog.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/dialog/EditUrlSettingsDialog.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/dialog/EpisodeFilterDialog.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/dialog/FeedItemFilterDialog.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/dialog/FeedPreferenceSkipDialog.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/dialog/FeedSortDialog.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/dialog/ItemFilterDialog.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/dialog/ItemSortDialog.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/dialog/MediaPlayerErrorDialog.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/dialog/PlaybackControlsDialog.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/dialog/ProxyDialog.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/dialog/RemoveFeedDialog.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/dialog/RenameItemDialog.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/dialog/ShareDialog.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/dialog/SkipPreferenceDialog.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/dialog/SleepTimerDialog.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/dialog/StreamingConfirmationDialog.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/dialog/SubscriptionsFilterDialog.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/dialog/SwipeActionsDialog.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/dialog/TagSettingsDialog.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/dialog/TimeRangeDialog.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/dialog/VariableSpeedDialog.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/dialog/rating/RatingDialogFragment.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/dialog/rating/RatingDialogManager.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/error/CrashReportWriter.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/error/RxJavaErrorHandlerSetup.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/fragment/AddFeedFragment.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/fragment/AllEpisodesFragment.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/fragment/AudioPlayerFragment.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/fragment/ChaptersFragment.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/fragment/CompletedDownloadsFragment.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/fragment/CoverFragment.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/fragment/DownloadLogFragment.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/fragment/EpisodesListFragment.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/fragment/ExternalPlayerFragment.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/fragment/FeedInfoFragment.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/fragment/FeedItemlistFragment.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/fragment/FeedSettingsFragment.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/fragment/InboxFragment.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/fragment/ItemDescriptionFragment.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/fragment/ItemFragment.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/fragment/ItemPagerFragment.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/fragment/NavDrawerFragment.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/fragment/PlaybackHistoryFragment.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/fragment/SearchFragment.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/fragment/SubscriptionFragment.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/fragment/TransitionEffect.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/fragment/actions/EpisodeMultiSelectActionHandler.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/fragment/actions/FeedMultiSelectActionHandler.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/fragment/preferences/DownloadsPreferencesFragment.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/fragment/preferences/ImportExportPreferencesFragment.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/fragment/preferences/MainPreferencesFragment.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/fragment/preferences/PlaybackPreferencesFragment.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/fragment/preferences/SwipePreferencesFragment.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/fragment/preferences/UserInterfacePreferencesFragment.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/fragment/preferences/dialog/PreferenceListDialog.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/fragment/preferences/dialog/PreferenceSwitchDialog.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/AddToQueueSwipeAction.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/DeleteSwipeAction.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/MarkFavoriteSwipeAction.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/RemoveFromHistorySwipeAction.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/RemoveFromInboxSwipeAction.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/RemoveFromQueueSwipeAction.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/ShowFirstSwipeDialogAction.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/StartDownloadSwipeAction.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/SwipeAction.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/SwipeActions.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/TogglePlaybackStateSwipeAction.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/menuhandler/FeedItemMenuHandler.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/menuhandler/FeedMenuHandler.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/preferences/PreferenceUpgrader.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/preferences/VolumeAdaptationPreference.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/receiver/ConnectivityActionReceiver.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/receiver/PowerConnectionReceiver.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/receiver/SPAReceiver.java create mode 100644 app/src/main/java/de/danoeh/antennapod/spa/SPAReceiver.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/AllEpisodesFilterDialog.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/CoverLoader.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/FeedItemFilterDialog.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/MenuItemUtils.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/SelectableAdapter.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/SimpleChipAdapter.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/StreamingConfirmationDialog.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/TransitionEffect.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/cleaner/HtmlToPlainText.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/cleaner/ShownotesCleaner.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/episodeslist/EpisodeItemListAdapter.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/episodeslist/EpisodeItemListRecyclerView.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/episodeslist/EpisodeItemViewHolder.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/episodeslist/EpisodeMultiSelectActionHandler.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/episodeslist/EpisodesListFragment.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/episodeslist/FeedItemMenuHandler.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/episodeslist/HorizontalItemListAdapter.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/episodeslist/HorizontalItemViewHolder.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/episodeslist/MediaSizeLoader.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/episodeslist/MoreContentListFooterUtil.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/ui/home/HomeFragment.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/ui/home/HomeSection.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/ui/home/HomeSectionsSettingsDialog.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/ui/home/sections/AllowNotificationsSection.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/ui/home/sections/DownloadsSection.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/ui/home/sections/EchoSection.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/ui/home/sections/EpisodesSurpriseSection.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/ui/home/sections/InboxSection.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/ui/home/sections/QueueSection.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/ui/home/sections/SubscriptionsSection.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/screen/AddFeedFragment.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/screen/AllEpisodesFragment.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/screen/InboxFragment.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/screen/PlaybackHistoryFragment.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/screen/SearchFragment.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/screen/chapter/ChaptersFragment.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/screen/chapter/ChaptersListAdapter.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/screen/download/CompletedDownloadsFragment.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/screen/download/DownloadErrorLabel.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/screen/download/DownloadLogAdapter.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/screen/download/DownloadLogDetailsDialog.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/screen/download/DownloadLogFragment.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/screen/download/DownloadLogItemViewHolder.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/screen/drawer/DrawerPreferencesDialog.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/screen/drawer/NavDrawerFragment.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/screen/drawer/NavListAdapter.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/screen/episode/ItemFragment.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/screen/episode/ItemPagerFragment.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/screen/feed/FeedInfoFragment.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/screen/feed/FeedItemFilterGroup.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/screen/feed/FeedItemlistFragment.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/screen/feed/ItemFilterDialog.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/screen/feed/ItemSortDialog.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/screen/feed/RemoveFeedDialog.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/screen/feed/RenameFeedDialog.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/screen/feed/ToolbarIconTintManager.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/screen/feed/preferences/EditUrlSettingsDialog.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/screen/feed/preferences/EpisodeFilterDialog.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/screen/feed/preferences/FeedPreferenceSkipDialog.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/screen/feed/preferences/FeedSettingsFragment.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/screen/feed/preferences/SkipPreferenceDialog.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/screen/feed/preferences/TagSettingsDialog.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/screen/feed/preferences/VolumeAdaptationPreference.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/screen/home/HomeFragment.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/screen/home/HomeSection.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/screen/home/HomeSectionsSettingsDialog.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/screen/home/sections/AllowNotificationsSection.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/screen/home/sections/DownloadsSection.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/screen/home/sections/EchoSection.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/screen/home/sections/EpisodesSurpriseSection.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/screen/home/sections/InboxSection.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/screen/home/sections/QueueSection.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/screen/home/sections/SubscriptionsSection.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/screen/onlinefeedview/FeedDiscoverer.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/screen/onlinefeedview/FeedItemlistDescriptionAdapter.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/screen/onlinefeedview/OnlineFeedViewActivity.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/screen/playback/MediaPlayerErrorDialog.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/screen/playback/PlayButton.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/screen/playback/PlaybackControlsDialog.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/screen/playback/PlaybackSpeedDialogActivity.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/screen/playback/PlaybackSpeedSeekBar.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/screen/playback/SleepTimerDialog.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/screen/playback/TimeRangeDialog.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/screen/playback/VariableSpeedDialog.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/screen/playback/audio/AudioPlayerFragment.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/screen/playback/audio/ChapterSeekBar.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/screen/playback/audio/CoverFragment.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/screen/playback/audio/ExternalPlayerFragment.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/screen/playback/audio/ItemDescriptionFragment.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/screen/playback/audio/NoRelayoutTextView.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/screen/playback/video/AspectRatioVideoView.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/screen/playback/video/PictureInPictureUtil.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/screen/playback/video/VideoplayerActivity.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/screen/preferences/BugReportActivity.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/screen/preferences/DownloadsPreferencesFragment.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/screen/preferences/ImportExportPreferencesFragment.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/screen/preferences/MainPreferencesFragment.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/screen/preferences/PlaybackPreferencesFragment.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/screen/preferences/PreferenceActivity.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/screen/preferences/PreferenceListDialog.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/screen/preferences/PreferenceSwitchDialog.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/screen/preferences/ProxyDialog.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/screen/preferences/SwipePreferencesFragment.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/screen/preferences/UserInterfacePreferencesFragment.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/screen/queue/QueueFragment.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/screen/queue/QueueRecyclerAdapter.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/screen/rating/RatingDialogFragment.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/screen/rating/RatingDialogManager.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/screen/subscriptions/FeedMenuHandler.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/screen/subscriptions/FeedMultiSelectActionHandler.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/screen/subscriptions/FeedSortDialog.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/screen/subscriptions/HorizontalFeedListAdapter.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/screen/subscriptions/SubscriptionFragment.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/screen/subscriptions/SubscriptionsFilterDialog.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/screen/subscriptions/SubscriptionsFilterGroup.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/screen/subscriptions/SubscriptionsRecyclerAdapter.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/share/ShareDialog.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/share/ShareUtils.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/swipeactions/AddToQueueSwipeAction.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/swipeactions/DeleteSwipeAction.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/swipeactions/MarkFavoriteSwipeAction.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/swipeactions/RemoveFromHistorySwipeAction.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/swipeactions/RemoveFromInboxSwipeAction.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/swipeactions/RemoveFromQueueSwipeAction.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/swipeactions/ShowFirstSwipeDialogAction.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/swipeactions/StartDownloadSwipeAction.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/swipeactions/SwipeAction.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/swipeactions/SwipeActions.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/swipeactions/SwipeActionsDialog.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/swipeactions/TogglePlaybackStateSwipeAction.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/view/EmptyViewHandler.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/view/ItemOffsetDecoration.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/view/LiftOnScrollListener.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/view/LocalDeleteModal.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/view/LockableBottomSheetBehavior.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/view/NestedScrollableHost.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/view/ShownotesWebView.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/view/SimpleAdapterDataObserver.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/view/AspectRatioVideoView.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/view/ChapterSeekBar.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/view/EmptyViewHandler.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/view/EpisodeItemListRecyclerView.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/view/ItemOffsetDecoration.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/view/LiftOnScrollListener.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/view/LocalDeleteModal.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/view/LockableBottomSheetBehavior.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/view/NestedScrollableHost.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/view/NoRelayoutTextView.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/view/PlayButton.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/view/PlaybackSpeedSeekBar.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/view/ShownotesWebView.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/view/SimpleAdapterDataObserver.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/view/ToolbarIconTintManager.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/view/viewholder/DownloadLogItemViewHolder.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/view/viewholder/EpisodeItemViewHolder.java delete mode 100644 app/src/main/java/de/danoeh/antennapod/view/viewholder/HorizontalItemViewHolder.java create mode 100644 app/src/test/java/de/danoeh/antennapod/ui/cleaner/ShownotesCleanerTest.java create mode 100644 app/src/test/java/de/danoeh/antennapod/ui/screen/onlinefeedview/FeedDiscovererTest.java delete mode 100644 core/src/main/java/de/danoeh/antennapod/core/dialog/ConfirmationDialog.java delete mode 100644 core/src/main/java/de/danoeh/antennapod/core/feed/ChapterMerger.java delete mode 100644 core/src/main/java/de/danoeh/antennapod/core/feed/FeedItemFilterGroup.java delete mode 100644 core/src/main/java/de/danoeh/antennapod/core/feed/SubscriptionsFilterGroup.java delete mode 100644 core/src/main/java/de/danoeh/antennapod/core/menuhandler/MenuItemUtils.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/util/ChapterMerger.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/util/ConfirmationDialog.java delete mode 100644 core/src/main/java/de/danoeh/antennapod/core/util/DownloadErrorLabel.java delete mode 100644 core/src/main/java/de/danoeh/antennapod/core/util/PowerUtils.java delete mode 100644 core/src/main/java/de/danoeh/antennapod/core/util/ShareUtils.java delete mode 100644 core/src/main/java/de/danoeh/antennapod/core/util/download/MediaSizeLoader.java delete mode 100644 core/src/main/java/de/danoeh/antennapod/core/util/download/NetworkConnectionChangeHandler.java delete mode 100644 core/src/main/java/de/danoeh/antennapod/core/util/gui/MoreContentListFooterUtil.java delete mode 100644 core/src/main/java/de/danoeh/antennapod/core/util/gui/PictureInPictureUtil.java delete mode 100644 core/src/main/java/de/danoeh/antennapod/core/util/gui/ShownotesCleaner.java delete mode 100644 core/src/main/java/de/danoeh/antennapod/core/util/syndication/FeedDiscoverer.java delete mode 100644 core/src/main/java/de/danoeh/antennapod/core/util/syndication/HtmlToPlainText.java delete mode 100644 core/src/test/java/android/text/TextUtils.java delete mode 100644 core/src/test/java/de/danoeh/antennapod/core/feed/VolumeAdaptionSettingTest.java delete mode 100644 core/src/test/java/de/danoeh/antennapod/core/storage/mapper/FeedCursorMapperTest.java delete mode 100644 core/src/test/java/de/danoeh/antennapod/core/util/ConverterTest.java delete mode 100644 core/src/test/java/de/danoeh/antennapod/core/util/FeedItemPermutorsTest.java delete mode 100644 core/src/test/java/de/danoeh/antennapod/core/util/FilenameGeneratorTest.java delete mode 100644 core/src/test/java/de/danoeh/antennapod/core/util/gui/ShownotesCleanerTest.java delete mode 100644 core/src/test/java/de/danoeh/antennapod/core/util/syndication/FeedDiscovererTest.java create mode 100644 model/src/test/java/de/danoeh/antennapod/model/VolumeAdaptionSettingTest.java create mode 100644 net/download/service-interface/src/test/java/de/danoeh/antennapod/net/download/serviceinterface/FilenameGeneratorTest.java create mode 100644 net/download/service/src/main/java/de/danoeh/antennapod/net/download/service/ConnectivityActionReceiver.java create mode 100644 net/download/service/src/main/java/de/danoeh/antennapod/net/download/service/PowerConnectionReceiver.java create mode 100644 storage/database/src/test/java/de/danoeh/antennapod/storage/database/FeedItemPermutorsTest.java create mode 100644 storage/database/src/test/java/de/danoeh/antennapod/storage/database/mapper/FeedCursorMapperTest.java create mode 100644 ui/common/src/test/java/de/danoeh/antennapod/ui/common/ConverterTest.java diff --git a/app/build.gradle b/app/build.gradle index a07d0db4a..449488f58 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -122,6 +122,10 @@ dependencies { implementation 'com.github.skydoves:balloon:1.5.3' implementation 'com.github.xabaras:RecyclerViewSwipeDecorator:1.3' + testImplementation "androidx.test:core:$testCoreVersion" + testImplementation "junit:junit:$junitVersion" + testImplementation "org.robolectric:robolectric:$robolectricVersion" + androidTestImplementation "org.awaitility:awaitility:$awaitilityVersion" androidTestImplementation 'com.nanohttpd:nanohttpd:2.1.1' androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion" diff --git a/app/src/androidTest/java/de/test/antennapod/EspressoTestUtils.java b/app/src/androidTest/java/de/test/antennapod/EspressoTestUtils.java index 041106017..c52df7f3e 100644 --- a/app/src/androidTest/java/de/test/antennapod/EspressoTestUtils.java +++ b/app/src/androidTest/java/de/test/antennapod/EspressoTestUtils.java @@ -25,7 +25,7 @@ import junit.framework.AssertionFailedError; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.storage.preferences.UserPreferences; -import de.danoeh.antennapod.fragment.NavDrawerFragment; +import de.danoeh.antennapod.ui.screen.drawer.NavDrawerFragment; import org.awaitility.Awaitility; import org.awaitility.core.ConditionTimeoutException; import org.hamcrest.Matcher; diff --git a/app/src/androidTest/java/de/test/antennapod/dialogs/ShareDialogTest.java b/app/src/androidTest/java/de/test/antennapod/dialogs/ShareDialogTest.java index 58a4a0bb6..2afe196b1 100644 --- a/app/src/androidTest/java/de/test/antennapod/dialogs/ShareDialogTest.java +++ b/app/src/androidTest/java/de/test/antennapod/dialogs/ShareDialogTest.java @@ -8,7 +8,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.platform.app.InstrumentationRegistry; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; -import de.danoeh.antennapod.fragment.AllEpisodesFragment; +import de.danoeh.antennapod.ui.screen.AllEpisodesFragment; import de.test.antennapod.EspressoTestUtils; import de.test.antennapod.ui.UITestUtils; import org.hamcrest.Matcher; diff --git a/app/src/androidTest/java/de/test/antennapod/ui/NavigationDrawerTest.java b/app/src/androidTest/java/de/test/antennapod/ui/NavigationDrawerTest.java index 5ccb35228..0246cf890 100644 --- a/app/src/androidTest/java/de/test/antennapod/ui/NavigationDrawerTest.java +++ b/app/src/androidTest/java/de/test/antennapod/ui/NavigationDrawerTest.java @@ -7,14 +7,14 @@ import androidx.test.espresso.intent.rule.IntentsTestRule; import androidx.test.ext.junit.runners.AndroidJUnit4; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; -import de.danoeh.antennapod.activity.PreferenceActivity; -import de.danoeh.antennapod.fragment.CompletedDownloadsFragment; +import de.danoeh.antennapod.ui.screen.preferences.PreferenceActivity; +import de.danoeh.antennapod.ui.screen.download.CompletedDownloadsFragment; import de.danoeh.antennapod.model.feed.Feed; import de.danoeh.antennapod.storage.preferences.UserPreferences; -import de.danoeh.antennapod.fragment.AllEpisodesFragment; -import de.danoeh.antennapod.fragment.NavDrawerFragment; -import de.danoeh.antennapod.fragment.PlaybackHistoryFragment; -import de.danoeh.antennapod.fragment.QueueFragment; +import de.danoeh.antennapod.ui.screen.AllEpisodesFragment; +import de.danoeh.antennapod.ui.screen.drawer.NavDrawerFragment; +import de.danoeh.antennapod.ui.screen.PlaybackHistoryFragment; +import de.danoeh.antennapod.ui.screen.queue.QueueFragment; import de.test.antennapod.EspressoTestUtils; import org.junit.After; import org.junit.Before; diff --git a/app/src/androidTest/java/de/test/antennapod/ui/PreferencesTest.java b/app/src/androidTest/java/de/test/antennapod/ui/PreferencesTest.java index 9ba4276be..8d956e3e4 100644 --- a/app/src/androidTest/java/de/test/antennapod/ui/PreferencesTest.java +++ b/app/src/androidTest/java/de/test/antennapod/ui/PreferencesTest.java @@ -9,7 +9,7 @@ import androidx.preference.PreferenceManager; import androidx.test.filters.LargeTest; import androidx.test.rule.ActivityTestRule; import de.danoeh.antennapod.R; -import de.danoeh.antennapod.activity.PreferenceActivity; +import de.danoeh.antennapod.ui.screen.preferences.PreferenceActivity; import de.danoeh.antennapod.core.storage.APCleanupAlgorithm; import de.danoeh.antennapod.core.storage.APNullCleanupAlgorithm; import de.danoeh.antennapod.core.storage.APQueueCleanupAlgorithm; diff --git a/app/src/androidTest/java/de/test/antennapod/ui/QueueFragmentTest.java b/app/src/androidTest/java/de/test/antennapod/ui/QueueFragmentTest.java index da323af76..0f472b699 100644 --- a/app/src/androidTest/java/de/test/antennapod/ui/QueueFragmentTest.java +++ b/app/src/androidTest/java/de/test/antennapod/ui/QueueFragmentTest.java @@ -5,7 +5,7 @@ import androidx.test.espresso.intent.rule.IntentsTestRule; import androidx.test.ext.junit.runners.AndroidJUnit4; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; -import de.danoeh.antennapod.fragment.QueueFragment; +import de.danoeh.antennapod.ui.screen.queue.QueueFragment; import de.test.antennapod.EspressoTestUtils; import org.junit.Before; import org.junit.Rule; diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 66f0b8766..3d608133f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -44,7 +44,7 @@ android:networkSecurityConfig="@xml/network_security_config"> @@ -167,15 +167,15 @@ + android:value="de.danoeh.antennapod.ui.screen.preferences.PreferenceActivity"/> - - - - - - - - - - - - - diff --git a/app/src/main/java/de/danoeh/antennapod/ClientConfigurator.java b/app/src/main/java/de/danoeh/antennapod/ClientConfigurator.java index a0cf4bfc9..43be25afa 100644 --- a/app/src/main/java/de/danoeh/antennapod/ClientConfigurator.java +++ b/app/src/main/java/de/danoeh/antennapod/ClientConfigurator.java @@ -20,7 +20,6 @@ import de.danoeh.antennapod.net.common.AntennapodHttpClient; import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterface; import de.danoeh.antennapod.net.download.service.feed.DownloadServiceInterfaceImpl; import de.danoeh.antennapod.net.common.NetworkUtils; -import de.danoeh.antennapod.core.util.download.NetworkConnectionChangeHandler; import de.danoeh.antennapod.net.ssl.SslProviderInstaller; import de.danoeh.antennapod.storage.database.PodDBAdapter; @@ -48,7 +47,6 @@ public class ClientConfigurator { PlaybackPreferences.init(context); SslProviderInstaller.install(context); NetworkUtils.init(context); - NetworkConnectionChangeHandler.init(context); DownloadServiceInterface.setImpl(new DownloadServiceInterfaceImpl()); FeedUpdateManager.setInstance(new FeedUpdateManagerImpl()); AutoDownloadManager.setInstance(new AutoDownloadManagerImpl()); diff --git a/app/src/main/java/de/danoeh/antennapod/CrashReportWriter.java b/app/src/main/java/de/danoeh/antennapod/CrashReportWriter.java new file mode 100644 index 000000000..92d6785ab --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/CrashReportWriter.java @@ -0,0 +1,67 @@ +package de.danoeh.antennapod; + +import android.os.Build; +import android.util.Log; + +import de.danoeh.antennapod.BuildConfig; +import org.apache.commons.io.IOUtils; + +import java.io.File; +import java.io.IOException; +import java.io.PrintWriter; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; + +import de.danoeh.antennapod.storage.preferences.UserPreferences; + +public class CrashReportWriter implements Thread.UncaughtExceptionHandler { + + private static final String TAG = "CrashReportWriter"; + + private final Thread.UncaughtExceptionHandler defaultHandler; + + public CrashReportWriter() { + defaultHandler = Thread.getDefaultUncaughtExceptionHandler(); + } + + public static File getFile() { + return new File(UserPreferences.getDataFolder(null), "crash-report.log"); + } + + @Override + public void uncaughtException(Thread thread, Throwable ex) { + write(ex); + defaultHandler.uncaughtException(thread, ex); + } + + public static void write(Throwable exception) { + File path = getFile(); + PrintWriter out = null; + try { + out = new PrintWriter(path, "UTF-8"); + out.println("## Crash info"); + out.println("Time: " + new SimpleDateFormat("dd-MM-yyyy HH:mm:ss", Locale.getDefault()).format(new Date())); + out.println("AntennaPod version: " + BuildConfig.VERSION_NAME); + out.println(); + out.println("## StackTrace"); + out.println("```"); + exception.printStackTrace(out); + out.println("```"); + } catch (IOException e) { + Log.e(TAG, Log.getStackTraceString(e)); + } finally { + IOUtils.closeQuietly(out); + } + } + + public static String getSystemInfo() { + return "## Environment" + + "\nAndroid version: " + Build.VERSION.RELEASE + + "\nOS version: " + System.getProperty("os.version") + + "\nAntennaPod version: " + BuildConfig.VERSION_NAME + + "\nModel: " + Build.MODEL + + "\nDevice: " + Build.DEVICE + + "\nProduct: " + Build.PRODUCT; + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/PodcastApp.java b/app/src/main/java/de/danoeh/antennapod/PodcastApp.java index a83e97d26..fb8c21883 100644 --- a/app/src/main/java/de/danoeh/antennapod/PodcastApp.java +++ b/app/src/main/java/de/danoeh/antennapod/PodcastApp.java @@ -5,11 +5,9 @@ import android.os.StrictMode; import com.google.android.material.color.DynamicColors; -import de.danoeh.antennapod.error.CrashReportWriter; -import de.danoeh.antennapod.error.RxJavaErrorHandlerSetup; -import de.danoeh.antennapod.preferences.PreferenceUpgrader; import de.danoeh.antennapod.spa.SPAUtil; import org.greenrobot.eventbus.EventBus; +import org.greenrobot.eventbus.EventBusException; /** Main application class. */ public class PodcastApp extends Application { @@ -30,16 +28,20 @@ public class PodcastApp extends Application { StrictMode.setVmPolicy(builder.build()); } + try { + // Robolectric calls onCreate for every test, which causes problems with static members + EventBus.builder() + .addIndex(new ApEventBusIndex()) + .logNoSubscriberMessages(false) + .sendNoSubscriberEvent(false) + .installDefaultEventBus(); + } catch (EventBusException e) { + e.printStackTrace(); + } + + DynamicColors.applyToActivitiesIfAvailable(this); ClientConfigurator.initialize(this); PreferenceUpgrader.checkUpgrades(this); - SPAUtil.sendSPAppsQueryFeedsIntent(this); - EventBus.builder() - .addIndex(new ApEventBusIndex()) - .logNoSubscriberMessages(false) - .sendNoSubscriberEvent(false) - .installDefaultEventBus(); - - DynamicColors.applyToActivitiesIfAvailable(this); } } diff --git a/app/src/main/java/de/danoeh/antennapod/PreferenceUpgrader.java b/app/src/main/java/de/danoeh/antennapod/PreferenceUpgrader.java new file mode 100644 index 000000000..02abc23fb --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/PreferenceUpgrader.java @@ -0,0 +1,170 @@ +package de.danoeh.antennapod; + +import android.content.Context; +import android.content.SharedPreferences; +import android.view.KeyEvent; +import androidx.core.app.NotificationManagerCompat; +import androidx.preference.PreferenceManager; + +import org.apache.commons.lang3.StringUtils; + +import java.util.concurrent.TimeUnit; + +import de.danoeh.antennapod.BuildConfig; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.storage.preferences.SleepTimerPreferences; +import de.danoeh.antennapod.CrashReportWriter; +import de.danoeh.antennapod.ui.screen.AllEpisodesFragment; +import de.danoeh.antennapod.storage.preferences.UserPreferences; +import de.danoeh.antennapod.storage.preferences.UserPreferences.EnqueueLocation; +import de.danoeh.antennapod.ui.screen.queue.QueueFragment; +import de.danoeh.antennapod.ui.swipeactions.SwipeAction; +import de.danoeh.antennapod.ui.swipeactions.SwipeActions; + +public class PreferenceUpgrader { + private static final String PREF_CONFIGURED_VERSION = "version_code"; + private static final String PREF_NAME = "app_version"; + + private static SharedPreferences prefs; + + public static void checkUpgrades(Context context) { + prefs = PreferenceManager.getDefaultSharedPreferences(context); + SharedPreferences upgraderPrefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); + int oldVersion = upgraderPrefs.getInt(PREF_CONFIGURED_VERSION, -1); + int newVersion = BuildConfig.VERSION_CODE; + + if (oldVersion != newVersion) { + CrashReportWriter.getFile().delete(); + + upgrade(oldVersion, context); + upgraderPrefs.edit().putInt(PREF_CONFIGURED_VERSION, newVersion).apply(); + } + } + + private static void upgrade(int oldVersion, Context context) { + if (oldVersion == -1) { + //New installation + return; + } + if (oldVersion < 1070196) { + // migrate episode cleanup value (unit changed from days to hours) + int oldValueInDays = UserPreferences.getEpisodeCleanupValue(); + if (oldValueInDays > 0) { + UserPreferences.setEpisodeCleanupValue(oldValueInDays * 24); + } // else 0 or special negative values, no change needed + } + if (oldVersion < 1070197) { + if (prefs.getBoolean("prefMobileUpdate", false)) { + prefs.edit().putString("prefMobileUpdateAllowed", "everything").apply(); + } + } + if (oldVersion < 1070300) { + if (prefs.getBoolean("prefEnableAutoDownloadOnMobile", false)) { + UserPreferences.setAllowMobileAutoDownload(true); + } + switch (prefs.getString("prefMobileUpdateAllowed", "images")) { + case "everything": + UserPreferences.setAllowMobileFeedRefresh(true); + UserPreferences.setAllowMobileEpisodeDownload(true); + UserPreferences.setAllowMobileImages(true); + break; + case "images": + UserPreferences.setAllowMobileImages(true); + break; + case "nothing": + UserPreferences.setAllowMobileImages(false); + break; + } + } + if (oldVersion < 1070400) { + UserPreferences.ThemePreference theme = UserPreferences.getTheme(); + if (theme == UserPreferences.ThemePreference.LIGHT) { + prefs.edit().putString(UserPreferences.PREF_THEME, "system").apply(); + } + + UserPreferences.setQueueLocked(false); + UserPreferences.setStreamOverDownload(false); + + if (!prefs.contains(UserPreferences.PREF_ENQUEUE_LOCATION)) { + final String keyOldPrefEnqueueFront = "prefQueueAddToFront"; + boolean enqueueAtFront = prefs.getBoolean(keyOldPrefEnqueueFront, false); + EnqueueLocation enqueueLocation = enqueueAtFront ? EnqueueLocation.FRONT : EnqueueLocation.BACK; + UserPreferences.setEnqueueLocation(enqueueLocation); + } + } + if (oldVersion < 2010300) { + // Migrate hardware button preferences + if (prefs.getBoolean("prefHardwareForwardButtonSkips", false)) { + prefs.edit().putString(UserPreferences.PREF_HARDWARE_FORWARD_BUTTON, + String.valueOf(KeyEvent.KEYCODE_MEDIA_NEXT)).apply(); + } + if (prefs.getBoolean("prefHardwarePreviousButtonRestarts", false)) { + prefs.edit().putString(UserPreferences.PREF_HARDWARE_PREVIOUS_BUTTON, + String.valueOf(KeyEvent.KEYCODE_MEDIA_PREVIOUS)).apply(); + } + } + if (oldVersion < 2040000) { + SharedPreferences swipePrefs = context.getSharedPreferences(SwipeActions.PREF_NAME, Context.MODE_PRIVATE); + swipePrefs.edit().putString(SwipeActions.KEY_PREFIX_SWIPEACTIONS + QueueFragment.TAG, + SwipeAction.REMOVE_FROM_QUEUE + "," + SwipeAction.REMOVE_FROM_QUEUE).apply(); + } + if (oldVersion < 2050000) { + prefs.edit().putBoolean(UserPreferences.PREF_PAUSE_PLAYBACK_FOR_FOCUS_LOSS, true).apply(); + } + if (oldVersion < 2080000) { + // Migrate drawer feed counter setting to reflect removal of + // "unplayed and in inbox" (0), by changing it to "unplayed" (2) + String feedCounterSetting = prefs.getString(UserPreferences.PREF_DRAWER_FEED_COUNTER, "1"); + if (feedCounterSetting.equals("0")) { + prefs.edit().putString(UserPreferences.PREF_DRAWER_FEED_COUNTER, "2").apply(); + } + + SharedPreferences sleepTimerPreferences = + context.getSharedPreferences(SleepTimerPreferences.PREF_NAME, Context.MODE_PRIVATE); + TimeUnit[] timeUnits = { TimeUnit.SECONDS, TimeUnit.MINUTES, TimeUnit.HOURS }; + long value = Long.parseLong(SleepTimerPreferences.lastTimerValue()); + TimeUnit unit = timeUnits[sleepTimerPreferences.getInt("LastTimeUnit", 1)]; + SleepTimerPreferences.setLastTimer(String.valueOf(unit.toMinutes(value))); + + if (prefs.getString(UserPreferences.PREF_EPISODE_CACHE_SIZE, "20") + .equals(context.getString(R.string.pref_episode_cache_unlimited))) { + prefs.edit().putString(UserPreferences.PREF_EPISODE_CACHE_SIZE, + "" + UserPreferences.EPISODE_CACHE_SIZE_UNLIMITED).apply(); + } + } + if (oldVersion < 3000007) { + if (prefs.getString("prefBackButtonBehavior", "").equals("drawer")) { + prefs.edit().putBoolean(UserPreferences.PREF_BACK_OPENS_DRAWER, true).apply(); + } + } + if (oldVersion < 3010000) { + if (prefs.getString(UserPreferences.PREF_THEME, "system").equals("2")) { + prefs.edit() + .putString(UserPreferences.PREF_THEME, "1") + .putBoolean(UserPreferences.PREF_THEME_BLACK, true) + .apply(); + } + UserPreferences.setAllowMobileSync(true); + if (prefs.getString(UserPreferences.PREF_UPDATE_INTERVAL, ":").contains(":")) { // Unset or "time of day" + prefs.edit().putString(UserPreferences.PREF_UPDATE_INTERVAL, "12").apply(); + } + } + if (oldVersion < 3020000) { + NotificationManagerCompat.from(context).deleteNotificationChannel("auto_download"); + } + + if (oldVersion < 3030000) { + SharedPreferences allEpisodesPreferences = + context.getSharedPreferences(AllEpisodesFragment.PREF_NAME, Context.MODE_PRIVATE); + String oldEpisodeSort = allEpisodesPreferences.getString(UserPreferences.PREF_SORT_ALL_EPISODES, ""); + if (!StringUtils.isAllEmpty(oldEpisodeSort)) { + prefs.edit().putString(UserPreferences.PREF_SORT_ALL_EPISODES, oldEpisodeSort).apply(); + } + + String oldEpisodeFilter = allEpisodesPreferences.getString("filter", ""); + if (!StringUtils.isAllEmpty(oldEpisodeFilter)) { + prefs.edit().putString(UserPreferences.PREF_FILTER_ALL_EPISODES, oldEpisodeFilter).apply(); + } + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/RxJavaErrorHandlerSetup.java b/app/src/main/java/de/danoeh/antennapod/RxJavaErrorHandlerSetup.java new file mode 100644 index 000000000..e909702ce --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/RxJavaErrorHandlerSetup.java @@ -0,0 +1,35 @@ +package de.danoeh.antennapod; + +import android.util.Log; +import io.reactivex.exceptions.UndeliverableException; +import io.reactivex.plugins.RxJavaPlugins; + +public class RxJavaErrorHandlerSetup { + private static final String TAG = "RxJavaErrorHandler"; + + private RxJavaErrorHandlerSetup() { + + } + + public static void setupRxJavaErrorHandler() { + RxJavaPlugins.setErrorHandler(exception -> { + if (exception instanceof UndeliverableException) { + // Probably just disposed because the fragment was left + Log.d(TAG, "Ignored exception: " + Log.getStackTraceString(exception)); + return; + } + + // Usually, undeliverable exceptions are wrapped in an UndeliverableException. + // If an undeliverable exception is a NPE (or some others), wrapping does not happen. + // AntennaPod threads might throw NPEs after disposing because we set controllers to null. + // Just swallow all exceptions here. + Log.e(TAG, Log.getStackTraceString(exception)); + CrashReportWriter.write(exception); + + if (BuildConfig.DEBUG) { + Thread.currentThread().getUncaughtExceptionHandler() + .uncaughtException(Thread.currentThread(), exception); + } + }); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/actionbutton/CancelDownloadActionButton.java b/app/src/main/java/de/danoeh/antennapod/actionbutton/CancelDownloadActionButton.java new file mode 100644 index 000000000..3e8e65200 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/actionbutton/CancelDownloadActionButton.java @@ -0,0 +1,41 @@ +package de.danoeh.antennapod.actionbutton; + +import android.content.Context; +import androidx.annotation.DrawableRes; +import androidx.annotation.StringRes; + +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterface; +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedMedia; +import de.danoeh.antennapod.storage.preferences.UserPreferences; +import de.danoeh.antennapod.storage.database.DBWriter; + +public class CancelDownloadActionButton extends ItemActionButton { + + public CancelDownloadActionButton(FeedItem item) { + super(item); + } + + @Override + @StringRes + public int getLabel() { + return R.string.cancel_download_label; + } + + @Override + @DrawableRes + public int getDrawable() { + return R.drawable.ic_cancel; + } + + @Override + public void onClick(Context context) { + FeedMedia media = item.getMedia(); + DownloadServiceInterface.get().cancel(context, media); + if (UserPreferences.isEnableAutodownload()) { + item.disableAutoDownload(); + DBWriter.setFeedItem(item); + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/actionbutton/DeleteActionButton.java b/app/src/main/java/de/danoeh/antennapod/actionbutton/DeleteActionButton.java new file mode 100644 index 000000000..3e88270da --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/actionbutton/DeleteActionButton.java @@ -0,0 +1,53 @@ +package de.danoeh.antennapod.actionbutton; + +import android.content.Context; +import android.view.View; +import androidx.annotation.DrawableRes; +import androidx.annotation.StringRes; + +import java.util.Collections; + +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedMedia; +import de.danoeh.antennapod.storage.database.DBWriter; +import de.danoeh.antennapod.ui.view.LocalDeleteModal; + +public class DeleteActionButton extends ItemActionButton { + + public DeleteActionButton(FeedItem item) { + super(item); + } + + @Override + @StringRes + public int getLabel() { + return R.string.delete_label; + } + + @Override + @DrawableRes + public int getDrawable() { + return R.drawable.ic_delete; + } + + @Override + public void onClick(Context context) { + final FeedMedia media = item.getMedia(); + if (media == null) { + return; + } + + LocalDeleteModal.showLocalFeedDeleteWarningIfNecessary(context, Collections.singletonList(item), + () -> DBWriter.deleteFeedMediaOfItem(context, media)); + } + + @Override + public int getVisibility() { + if (item.getMedia() != null && (item.getMedia().isDownloaded() || item.getFeed().isLocalFeed())) { + return View.VISIBLE; + } + + return View.INVISIBLE; + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/actionbutton/DownloadActionButton.java b/app/src/main/java/de/danoeh/antennapod/actionbutton/DownloadActionButton.java new file mode 100644 index 000000000..15f07d207 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/actionbutton/DownloadActionButton.java @@ -0,0 +1,74 @@ +package de.danoeh.antennapod.actionbutton; + +import android.content.Context; +import android.view.View; + +import androidx.annotation.DrawableRes; +import androidx.annotation.NonNull; +import androidx.annotation.StringRes; + +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterface; +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedMedia; +import de.danoeh.antennapod.storage.preferences.UsageStatistics; +import de.danoeh.antennapod.net.common.NetworkUtils; + +public class DownloadActionButton extends ItemActionButton { + + public DownloadActionButton(FeedItem item) { + super(item); + } + + @Override + @StringRes + public int getLabel() { + return R.string.download_label; + } + + @Override + @DrawableRes + public int getDrawable() { + return R.drawable.ic_download; + } + + @Override + public int getVisibility() { + return item.getFeed().isLocalFeed() ? View.INVISIBLE : View.VISIBLE; + } + + @Override + public void onClick(Context context) { + final FeedMedia media = item.getMedia(); + if (media == null || shouldNotDownload(media)) { + return; + } + + UsageStatistics.logAction(UsageStatistics.ACTION_DOWNLOAD); + + if (NetworkUtils.isEpisodeDownloadAllowed()) { + DownloadServiceInterface.get().downloadNow(context, item, false); + } else { + MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(context) + .setTitle(R.string.confirm_mobile_download_dialog_title) + .setPositiveButton(R.string.confirm_mobile_download_dialog_download_later, + (d, w) -> DownloadServiceInterface.get().downloadNow(context, item, false)) + .setNeutralButton(R.string.confirm_mobile_download_dialog_allow_this_time, + (d, w) -> DownloadServiceInterface.get().downloadNow(context, item, true)) + .setNegativeButton(R.string.cancel_label, null); + if (NetworkUtils.isNetworkRestricted() && NetworkUtils.isVpnOverWifi()) { + builder.setMessage(R.string.confirm_mobile_download_dialog_message_vpn); + } else { + builder.setMessage(R.string.confirm_mobile_download_dialog_message); + } + + builder.show(); + } + } + + private boolean shouldNotDownload(@NonNull FeedMedia media) { + boolean isDownloading = DownloadServiceInterface.get().isDownloadingEpisode(media.getDownloadUrl()); + return isDownloading || media.isDownloaded(); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/actionbutton/ItemActionButton.java b/app/src/main/java/de/danoeh/antennapod/actionbutton/ItemActionButton.java new file mode 100644 index 000000000..84b246167 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/actionbutton/ItemActionButton.java @@ -0,0 +1,64 @@ +package de.danoeh.antennapod.actionbutton; + +import android.content.Context; +import android.widget.ImageView; +import androidx.annotation.DrawableRes; +import androidx.annotation.NonNull; +import androidx.annotation.StringRes; +import android.view.View; + +import de.danoeh.antennapod.playback.service.PlaybackStatus; +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedMedia; +import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterface; +import de.danoeh.antennapod.storage.preferences.UserPreferences; + +public abstract class ItemActionButton { + FeedItem item; + + ItemActionButton(FeedItem item) { + this.item = item; + } + + @StringRes + public abstract int getLabel(); + + @DrawableRes + public abstract int getDrawable(); + + public abstract void onClick(Context context); + + public int getVisibility() { + return View.VISIBLE; + } + + @NonNull + public static ItemActionButton forItem(@NonNull FeedItem item) { + final FeedMedia media = item.getMedia(); + if (media == null) { + return new MarkAsPlayedActionButton(item); + } + + final boolean isDownloadingMedia = DownloadServiceInterface.get().isDownloadingEpisode(media.getDownloadUrl()); + if (PlaybackStatus.isCurrentlyPlaying(media)) { + return new PauseActionButton(item); + } else if (item.getFeed().isLocalFeed()) { + return new PlayLocalActionButton(item); + } else if (media.isDownloaded()) { + return new PlayActionButton(item); + } else if (isDownloadingMedia) { + return new CancelDownloadActionButton(item); + } else if (UserPreferences.isStreamOverDownload()) { + return new StreamActionButton(item); + } else { + return new DownloadActionButton(item); + } + } + + public void configure(@NonNull View button, @NonNull ImageView icon, Context context) { + button.setVisibility(getVisibility()); + button.setContentDescription(context.getString(getLabel())); + button.setOnClickListener((view) -> onClick(context)); + icon.setImageResource(getDrawable()); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/actionbutton/MarkAsPlayedActionButton.java b/app/src/main/java/de/danoeh/antennapod/actionbutton/MarkAsPlayedActionButton.java new file mode 100644 index 000000000..673a0c6ff --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/actionbutton/MarkAsPlayedActionButton.java @@ -0,0 +1,41 @@ +package de.danoeh.antennapod.actionbutton; + +import android.content.Context; +import androidx.annotation.DrawableRes; +import androidx.annotation.StringRes; +import android.view.View; + +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.storage.database.DBWriter; + +public class MarkAsPlayedActionButton extends ItemActionButton { + + public MarkAsPlayedActionButton(FeedItem item) { + super(item); + } + + @Override + @StringRes + public int getLabel() { + return (item.hasMedia() ? R.string.mark_read_label : R.string.mark_read_no_media_label); + } + + @Override + @DrawableRes + public int getDrawable() { + return R.drawable.ic_check; + } + + @Override + public void onClick(Context context) { + if (!item.isPlayed()) { + DBWriter.markItemPlayed(item, FeedItem.PLAYED, true); + } + } + + @Override + public int getVisibility() { + return (item.isPlayed()) ? View.INVISIBLE : View.VISIBLE; + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/actionbutton/PauseActionButton.java b/app/src/main/java/de/danoeh/antennapod/actionbutton/PauseActionButton.java new file mode 100644 index 000000000..effb33e78 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/actionbutton/PauseActionButton.java @@ -0,0 +1,42 @@ +package de.danoeh.antennapod.actionbutton; + +import android.content.Context; +import android.view.KeyEvent; +import androidx.annotation.DrawableRes; +import androidx.annotation.StringRes; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.playback.service.PlaybackStatus; +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedMedia; +import de.danoeh.antennapod.ui.appstartintent.MediaButtonStarter; + +public class PauseActionButton extends ItemActionButton { + + public PauseActionButton(FeedItem item) { + super(item); + } + + @Override + @StringRes + public int getLabel() { + return R.string.pause_label; + } + + @Override + @DrawableRes + public int getDrawable() { + return R.drawable.ic_pause; + } + + @Override + public void onClick(Context context) { + FeedMedia media = item.getMedia(); + if (media == null) { + return; + } + + if (PlaybackStatus.isCurrentlyPlaying(media)) { + context.sendBroadcast(MediaButtonStarter.createIntent(context, KeyEvent.KEYCODE_MEDIA_PAUSE)); + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/actionbutton/PlayActionButton.java b/app/src/main/java/de/danoeh/antennapod/actionbutton/PlayActionButton.java new file mode 100644 index 000000000..6eeb42065 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/actionbutton/PlayActionButton.java @@ -0,0 +1,60 @@ +package de.danoeh.antennapod.actionbutton; + +import android.content.Context; +import android.util.Log; +import androidx.annotation.DrawableRes; +import androidx.annotation.StringRes; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.playback.service.PlaybackService; +import de.danoeh.antennapod.playback.service.PlaybackServiceStarter; +import de.danoeh.antennapod.storage.database.DBWriter; +import de.danoeh.antennapod.event.FeedItemEvent; +import de.danoeh.antennapod.event.MessageEvent; +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedMedia; +import de.danoeh.antennapod.model.playback.MediaType; +import org.greenrobot.eventbus.EventBus; + +public class PlayActionButton extends ItemActionButton { + private static final String TAG = "PlayActionButton"; + + public PlayActionButton(FeedItem item) { + super(item); + } + + @Override + @StringRes + public int getLabel() { + return R.string.play_label; + } + + @Override + @DrawableRes + public int getDrawable() { + return R.drawable.ic_play_24dp; + } + + @Override + public void onClick(Context context) { + FeedMedia media = item.getMedia(); + if (media == null) { + return; + } + if (!media.fileExists()) { + Log.i(TAG, "Missing episode. Will update the database now."); + media.setDownloaded(false); + media.setLocalFileUrl(null); + DBWriter.setFeedMedia(media); + EventBus.getDefault().post(FeedItemEvent.updated(media.getItem())); + EventBus.getDefault().post(new MessageEvent(context.getString(R.string.error_file_not_found))); + return; + } + new PlaybackServiceStarter(context, media) + .callEvenIfRunning(true) + .start(); + + if (media.getMediaType() == MediaType.VIDEO) { + context.startActivity(PlaybackService.getPlayerActivityIntent(context, media)); + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/actionbutton/PlayLocalActionButton.java b/app/src/main/java/de/danoeh/antennapod/actionbutton/PlayLocalActionButton.java new file mode 100644 index 000000000..62a965494 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/actionbutton/PlayLocalActionButton.java @@ -0,0 +1,46 @@ +package de.danoeh.antennapod.actionbutton; + +import android.content.Context; +import androidx.annotation.DrawableRes; +import androidx.annotation.StringRes; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedMedia; +import de.danoeh.antennapod.model.playback.MediaType; +import de.danoeh.antennapod.playback.service.PlaybackService; +import de.danoeh.antennapod.playback.service.PlaybackServiceStarter; + +public class PlayLocalActionButton extends ItemActionButton { + + public PlayLocalActionButton(FeedItem item) { + super(item); + } + + @Override + @StringRes + public int getLabel() { + return R.string.play_label; + } + + @Override + @DrawableRes + public int getDrawable() { + return R.drawable.ic_play_24dp; + } + + @Override + public void onClick(Context context) { + final FeedMedia media = item.getMedia(); + if (media == null) { + return; + } + + new PlaybackServiceStarter(context, media) + .callEvenIfRunning(true) + .start(); + + if (media.getMediaType() == MediaType.VIDEO) { + context.startActivity(PlaybackService.getPlayerActivityIntent(context, media)); + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/actionbutton/StreamActionButton.java b/app/src/main/java/de/danoeh/antennapod/actionbutton/StreamActionButton.java new file mode 100644 index 000000000..afe1fcce8 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/actionbutton/StreamActionButton.java @@ -0,0 +1,56 @@ +package de.danoeh.antennapod.actionbutton; + +import android.content.Context; + +import androidx.annotation.DrawableRes; +import androidx.annotation.StringRes; + +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedMedia; +import de.danoeh.antennapod.model.playback.MediaType; +import de.danoeh.antennapod.playback.service.PlaybackService; +import de.danoeh.antennapod.playback.service.PlaybackServiceStarter; +import de.danoeh.antennapod.storage.preferences.UsageStatistics; +import de.danoeh.antennapod.net.common.NetworkUtils; +import de.danoeh.antennapod.ui.StreamingConfirmationDialog; + +public class StreamActionButton extends ItemActionButton { + + public StreamActionButton(FeedItem item) { + super(item); + } + + @Override + @StringRes + public int getLabel() { + return R.string.stream_label; + } + + @Override + @DrawableRes + public int getDrawable() { + return R.drawable.ic_stream; + } + + @Override + public void onClick(Context context) { + final FeedMedia media = item.getMedia(); + if (media == null) { + return; + } + UsageStatistics.logAction(UsageStatistics.ACTION_STREAM); + + if (!NetworkUtils.isStreamingAllowed()) { + new StreamingConfirmationDialog(context, media).show(); + return; + } + new PlaybackServiceStarter(context, media) + .callEvenIfRunning(true) + .start(); + + if (media.getMediaType() == MediaType.VIDEO) { + context.startActivity(PlaybackService.getPlayerActivityIntent(context, media)); + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/actionbutton/VisitWebsiteActionButton.java b/app/src/main/java/de/danoeh/antennapod/actionbutton/VisitWebsiteActionButton.java new file mode 100644 index 000000000..dbe937c14 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/actionbutton/VisitWebsiteActionButton.java @@ -0,0 +1,38 @@ +package de.danoeh.antennapod.actionbutton; + +import android.content.Context; +import android.view.View; +import androidx.annotation.DrawableRes; +import androidx.annotation.StringRes; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.core.util.IntentUtils; + +public class VisitWebsiteActionButton extends ItemActionButton { + + public VisitWebsiteActionButton(FeedItem item) { + super(item); + } + + @Override + @StringRes + public int getLabel() { + return R.string.visit_website_label; + } + + @Override + @DrawableRes + public int getDrawable() { + return R.drawable.ic_web; + } + + @Override + public void onClick(Context context) { + IntentUtils.openInBrowser(context, item.getLink()); + } + + @Override + public int getVisibility() { + return (item.getLink() == null) ? View.INVISIBLE : View.VISIBLE; + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/activity/BugReportActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/BugReportActivity.java deleted file mode 100644 index e379b5eb2..000000000 --- a/app/src/main/java/de/danoeh/antennapod/activity/BugReportActivity.java +++ /dev/null @@ -1,128 +0,0 @@ -package de.danoeh.antennapod.activity; - -import android.content.ClipData; -import android.content.ClipboardManager; -import android.content.Context; -import android.net.Uri; -import android.os.Build; -import android.os.Bundle; -import android.util.Log; -import com.google.android.material.snackbar.Snackbar; - -import androidx.annotation.NonNull; -import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import androidx.appcompat.app.AppCompatActivity; -import androidx.core.app.ShareCompat; -import androidx.core.content.FileProvider; - - -import android.view.Menu; -import android.view.MenuItem; -import android.widget.TextView; - - -import de.danoeh.antennapod.ui.common.ThemeSwitcher; -import de.danoeh.antennapod.error.CrashReportWriter; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.storage.preferences.UserPreferences; -import de.danoeh.antennapod.core.util.IntentUtils; -import org.apache.commons.io.IOUtils; - -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.nio.charset.Charset; - -/** - * Displays the 'crash report' screen - */ -public class BugReportActivity extends AppCompatActivity { - private static final String TAG = "BugReportActivity"; - - @Override - protected void onCreate(Bundle savedInstanceState) { - setTheme(ThemeSwitcher.getTheme(this)); - super.onCreate(savedInstanceState); - getSupportActionBar().setDisplayShowHomeEnabled(true); - setContentView(R.layout.bug_report); - - String stacktrace = "No crash report recorded"; - try { - File crashFile = CrashReportWriter.getFile(); - if (crashFile.exists()) { - stacktrace = IOUtils.toString(new FileInputStream(crashFile), Charset.forName("UTF-8")); - } else { - Log.d(TAG, stacktrace); - } - } catch (IOException e) { - e.printStackTrace(); - } - - TextView crashDetailsTextView = findViewById(R.id.crash_report_logs); - crashDetailsTextView.setText(CrashReportWriter.getSystemInfo() + "\n\n" + stacktrace); - - findViewById(R.id.btn_open_bug_tracker).setOnClickListener(v -> IntentUtils.openInBrowser( - BugReportActivity.this, "https://github.com/AntennaPod/AntennaPod/issues")); - - findViewById(R.id.btn_copy_log).setOnClickListener(v -> { - ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); - ClipData clip = ClipData.newPlainText(getString(R.string.bug_report_title), crashDetailsTextView.getText()); - clipboard.setPrimaryClip(clip); - if (Build.VERSION.SDK_INT < 32) { - Snackbar.make(findViewById(android.R.id.content), R.string.copied_to_clipboard, - Snackbar.LENGTH_SHORT).show(); - } - }); - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - getMenuInflater().inflate(R.menu.bug_report_options, menu); - return super.onCreateOptionsMenu(menu); - } - - @Override - public boolean onOptionsItemSelected(@NonNull MenuItem item) { - if (item.getItemId() == R.id.export_logcat) { - MaterialAlertDialogBuilder alertBuilder = new MaterialAlertDialogBuilder(this); - alertBuilder.setMessage(R.string.confirm_export_log_dialog_message); - alertBuilder.setPositiveButton(R.string.confirm_label, (dialog, which) -> { - exportLog(); - dialog.dismiss(); - }); - alertBuilder.setNegativeButton(R.string.cancel_label, null); - alertBuilder.show(); - return true; - } - return super.onOptionsItemSelected(item); - } - - private void exportLog() { - try { - File filename = new File(UserPreferences.getDataFolder(null), "full-logs.txt"); - String cmd = "logcat -d -f " + filename.getAbsolutePath(); - Runtime.getRuntime().exec(cmd); - //share file - try { - String authority = getString(R.string.provider_authority); - Uri fileUri = FileProvider.getUriForFile(this, authority, filename); - - new ShareCompat.IntentBuilder(this) - .setType("text/*") - .addStream(fileUri) - .setChooserTitle(R.string.share_file_label) - .startChooser(); - } catch (Exception e) { - e.printStackTrace(); - int strResId = R.string.log_file_share_exception; - Snackbar.make(findViewById(android.R.id.content), strResId, Snackbar.LENGTH_LONG) - .show(); - } - } catch (IOException e) { - e.printStackTrace(); - Snackbar.make(findViewById(android.R.id.content), e.getMessage(), Snackbar.LENGTH_LONG).show(); - } - } - - -} 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 32884c9c9..fd4007736 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java @@ -40,23 +40,23 @@ import de.danoeh.antennapod.net.download.serviceinterface.FeedUpdateManager; import de.danoeh.antennapod.net.sync.serviceinterface.SynchronizationQueueSink; import de.danoeh.antennapod.ui.appstartintent.MediaButtonStarter; import de.danoeh.antennapod.ui.common.ThemeSwitcher; -import de.danoeh.antennapod.dialog.rating.RatingDialogManager; +import de.danoeh.antennapod.ui.screen.rating.RatingDialogManager; import de.danoeh.antennapod.event.EpisodeDownloadEvent; import de.danoeh.antennapod.event.FeedUpdateRunningEvent; import de.danoeh.antennapod.event.MessageEvent; -import de.danoeh.antennapod.fragment.AddFeedFragment; -import de.danoeh.antennapod.fragment.AllEpisodesFragment; -import de.danoeh.antennapod.fragment.AudioPlayerFragment; -import de.danoeh.antennapod.fragment.CompletedDownloadsFragment; -import de.danoeh.antennapod.fragment.DownloadLogFragment; -import de.danoeh.antennapod.fragment.FeedItemlistFragment; -import de.danoeh.antennapod.fragment.InboxFragment; -import de.danoeh.antennapod.fragment.NavDrawerFragment; -import de.danoeh.antennapod.fragment.PlaybackHistoryFragment; -import de.danoeh.antennapod.fragment.QueueFragment; -import de.danoeh.antennapod.fragment.SearchFragment; -import de.danoeh.antennapod.fragment.SubscriptionFragment; -import de.danoeh.antennapod.fragment.TransitionEffect; +import de.danoeh.antennapod.ui.screen.AddFeedFragment; +import de.danoeh.antennapod.ui.screen.AllEpisodesFragment; +import de.danoeh.antennapod.ui.screen.playback.audio.AudioPlayerFragment; +import de.danoeh.antennapod.ui.screen.download.CompletedDownloadsFragment; +import de.danoeh.antennapod.ui.screen.download.DownloadLogFragment; +import de.danoeh.antennapod.ui.screen.feed.FeedItemlistFragment; +import de.danoeh.antennapod.ui.screen.InboxFragment; +import de.danoeh.antennapod.ui.screen.drawer.NavDrawerFragment; +import de.danoeh.antennapod.ui.screen.PlaybackHistoryFragment; +import de.danoeh.antennapod.ui.screen.queue.QueueFragment; +import de.danoeh.antennapod.ui.screen.SearchFragment; +import de.danoeh.antennapod.ui.screen.subscriptions.SubscriptionFragment; +import de.danoeh.antennapod.ui.TransitionEffect; import de.danoeh.antennapod.model.download.DownloadStatus; import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterface; import de.danoeh.antennapod.playback.cast.CastEnabledActivity; @@ -65,8 +65,8 @@ import de.danoeh.antennapod.storage.preferences.UserPreferences; import de.danoeh.antennapod.ui.appstartintent.MainActivityStarter; import de.danoeh.antennapod.ui.common.ThemeUtils; import de.danoeh.antennapod.ui.discovery.DiscoveryFragment; -import de.danoeh.antennapod.ui.home.HomeFragment; -import de.danoeh.antennapod.view.LockableBottomSheetBehavior; +import de.danoeh.antennapod.ui.screen.home.HomeFragment; +import de.danoeh.antennapod.ui.view.LockableBottomSheetBehavior; import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.Validate; import org.greenrobot.eventbus.EventBus; diff --git a/app/src/main/java/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java deleted file mode 100644 index 1f3963d46..000000000 --- a/app/src/main/java/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java +++ /dev/null @@ -1,713 +0,0 @@ -package de.danoeh.antennapod.activity; - -import android.app.Dialog; -import android.content.Context; -import android.content.DialogInterface; -import android.content.Intent; -import android.content.SharedPreferences; -import android.graphics.LightingColorFilter; -import android.os.Bundle; -import android.text.Spannable; -import android.text.SpannableString; -import android.text.TextUtils; -import android.text.style.ForegroundColorSpan; -import android.util.Log; -import android.view.View; -import android.view.ViewGroup; -import android.widget.AdapterView; -import android.widget.ArrayAdapter; -import android.widget.Toast; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.UiThread; -import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import androidx.appcompat.app.AppCompatActivity; -import com.bumptech.glide.Glide; -import com.bumptech.glide.request.RequestOptions; -import com.google.android.material.snackbar.Snackbar; - -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.adapter.FeedItemlistDescriptionAdapter; -import de.danoeh.antennapod.net.download.service.feed.remote.Downloader; -import de.danoeh.antennapod.net.download.service.feed.remote.HttpDownloader; -import de.danoeh.antennapod.ui.appstartintent.MainActivityStarter; -import de.danoeh.antennapod.ui.common.ThemeSwitcher; -import de.danoeh.antennapod.net.download.serviceinterface.DownloadRequestCreator; -import de.danoeh.antennapod.net.discovery.FeedUrlNotFoundException; -import de.danoeh.antennapod.storage.database.FeedDatabaseWriter; -import de.danoeh.antennapod.playback.service.PlaybackServiceInterface; -import de.danoeh.antennapod.core.util.DownloadErrorLabel; -import de.danoeh.antennapod.databinding.EditTextDialogBinding; -import de.danoeh.antennapod.databinding.OnlinefeedviewHeaderBinding; -import de.danoeh.antennapod.event.EpisodeDownloadEvent; -import de.danoeh.antennapod.event.FeedListUpdateEvent; -import de.danoeh.antennapod.event.PlayerStatusEvent; -import de.danoeh.antennapod.storage.preferences.PlaybackPreferences; -import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterface; -import de.danoeh.antennapod.storage.preferences.UserPreferences; -import de.danoeh.antennapod.model.download.DownloadRequest; -import de.danoeh.antennapod.model.download.DownloadResult; -import de.danoeh.antennapod.storage.database.DBReader; -import de.danoeh.antennapod.storage.database.DBWriter; -import de.danoeh.antennapod.net.discovery.CombinedSearcher; -import de.danoeh.antennapod.net.discovery.PodcastSearchResult; -import de.danoeh.antennapod.net.discovery.PodcastSearcherRegistry; -import de.danoeh.antennapod.parser.feed.FeedHandler; -import de.danoeh.antennapod.parser.feed.FeedHandlerResult; -import de.danoeh.antennapod.model.download.DownloadError; -import de.danoeh.antennapod.core.util.IntentUtils; -import de.danoeh.antennapod.net.common.UrlChecker; -import de.danoeh.antennapod.core.util.syndication.FeedDiscoverer; -import de.danoeh.antennapod.core.util.syndication.HtmlToPlainText; -import de.danoeh.antennapod.databinding.OnlinefeedviewActivityBinding; -import de.danoeh.antennapod.model.feed.Feed; -import de.danoeh.antennapod.model.feed.FeedPreferences; -import de.danoeh.antennapod.model.playback.RemoteMedia; -import de.danoeh.antennapod.parser.feed.UnsupportedFeedtypeException; -import de.danoeh.antennapod.ui.common.ThemeUtils; -import de.danoeh.antennapod.ui.glide.FastBlurTransformation; -import de.danoeh.antennapod.ui.preferences.screen.synchronization.AuthenticationDialog; -import io.reactivex.Maybe; -import io.reactivex.Observable; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.Disposable; -import io.reactivex.observers.DisposableMaybeObserver; -import io.reactivex.schedulers.Schedulers; -import org.apache.commons.lang3.StringUtils; -import org.greenrobot.eventbus.EventBus; -import org.greenrobot.eventbus.Subscribe; -import org.greenrobot.eventbus.ThreadMode; - -import java.io.File; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import static de.danoeh.antennapod.ui.appstartintent.OnlineFeedviewActivityStarter.ARG_FEEDURL; -import static de.danoeh.antennapod.ui.appstartintent.OnlineFeedviewActivityStarter.ARG_STARTED_FROM_SEARCH; -import static de.danoeh.antennapod.ui.appstartintent.OnlineFeedviewActivityStarter.ARG_WAS_MANUAL_URL; - -/** - * Downloads a feed from a feed URL and parses it. Subclasses can display the - * feed object that was parsed. This activity MUST be started with a given URL - * or an Exception will be thrown. - *

- * If the feed cannot be downloaded or parsed, an error dialog will be displayed - * and the activity will finish as soon as the error dialog is closed. - */ -public class OnlineFeedViewActivity extends AppCompatActivity { - - private static final int RESULT_ERROR = 2; - private static final String TAG = "OnlineFeedViewActivity"; - private static final String PREFS = "OnlineFeedViewActivityPreferences"; - private static final String PREF_LAST_AUTO_DOWNLOAD = "lastAutoDownload"; - private static final int DESCRIPTION_MAX_LINES_COLLAPSED = 4; - - private volatile List feeds; - private String selectedDownloadUrl; - private Downloader downloader; - private String username = null; - private String password = null; - - private boolean isPaused; - private boolean didPressSubscribe = false; - private boolean isFeedFoundBySearch = false; - - private Dialog dialog; - - private Disposable download; - private Disposable parser; - private Disposable updater; - - private OnlinefeedviewHeaderBinding headerBinding; - private OnlinefeedviewActivityBinding viewBinding; - - @Override - protected void onCreate(Bundle savedInstanceState) { - setTheme(ThemeSwitcher.getTranslucentTheme(this)); - super.onCreate(savedInstanceState); - - viewBinding = OnlinefeedviewActivityBinding.inflate(getLayoutInflater()); - setContentView(viewBinding.getRoot()); - - viewBinding.transparentBackground.setOnClickListener(v -> finish()); - viewBinding.closeButton.setOnClickListener(view -> finish()); - viewBinding.card.setOnClickListener(null); - viewBinding.card.setCardBackgroundColor(ThemeUtils.getColorFromAttr(this, R.attr.colorSurface)); - headerBinding = OnlinefeedviewHeaderBinding.inflate(getLayoutInflater()); - - String feedUrl = null; - if (getIntent().hasExtra(ARG_FEEDURL)) { - feedUrl = getIntent().getStringExtra(ARG_FEEDURL); - } else if (TextUtils.equals(getIntent().getAction(), Intent.ACTION_SEND)) { - feedUrl = getIntent().getStringExtra(Intent.EXTRA_TEXT); - } else if (TextUtils.equals(getIntent().getAction(), Intent.ACTION_VIEW)) { - feedUrl = getIntent().getDataString(); - } - - if (feedUrl == null) { - Log.e(TAG, "feedUrl is null."); - showNoPodcastFoundError(); - } else { - Log.d(TAG, "Activity was started with url " + feedUrl); - setLoadingLayout(); - // Remove subscribeonandroid.com from feed URL in order to subscribe to the actual feed URL - if (feedUrl.contains("subscribeonandroid.com")) { - feedUrl = feedUrl.replaceFirst("((www.)?(subscribeonandroid.com/))", ""); - } - if (savedInstanceState != null) { - username = savedInstanceState.getString("username"); - password = savedInstanceState.getString("password"); - } - lookupUrlAndDownload(feedUrl); - } - } - - private void showNoPodcastFoundError() { - runOnUiThread(() -> new MaterialAlertDialogBuilder(OnlineFeedViewActivity.this) - .setNeutralButton(android.R.string.ok, (dialog, which) -> finish()) - .setTitle(R.string.error_label) - .setMessage(R.string.null_value_podcast_error) - .setOnDismissListener(dialog1 -> { - setResult(RESULT_ERROR); - finish(); - }) - .show()); - } - - /** - * Displays a progress indicator. - */ - private void setLoadingLayout() { - viewBinding.progressBar.setVisibility(View.VISIBLE); - viewBinding.feedDisplayContainer.setVisibility(View.GONE); - } - - @Override - protected void onStart() { - super.onStart(); - isPaused = false; - EventBus.getDefault().register(this); - } - - @Override - protected void onStop() { - super.onStop(); - isPaused = true; - EventBus.getDefault().unregister(this); - if (downloader != null && !downloader.isFinished()) { - downloader.cancel(); - } - if(dialog != null && dialog.isShowing()) { - dialog.dismiss(); - } - } - - @Override - public void onDestroy() { - super.onDestroy(); - if(updater != null) { - updater.dispose(); - } - if(download != null) { - download.dispose(); - } - if(parser != null) { - parser.dispose(); - } - } - - @Override - protected void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - outState.putString("username", username); - outState.putString("password", password); - } - - private void resetIntent(String url) { - Intent intent = new Intent(); - intent.putExtra(ARG_FEEDURL, url); - setIntent(intent); - } - - @Override - public void finish() { - super.finish(); - overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out); - } - - private void lookupUrlAndDownload(String url) { - download = PodcastSearcherRegistry.lookupUrl(url) - .subscribeOn(Schedulers.io()) - .observeOn(Schedulers.io()) - .subscribe(this::startFeedDownload, - error -> { - if (error instanceof FeedUrlNotFoundException) { - tryToRetrieveFeedUrlBySearch((FeedUrlNotFoundException) error); - } else { - showNoPodcastFoundError(); - Log.e(TAG, Log.getStackTraceString(error)); - } - }); - } - - private void tryToRetrieveFeedUrlBySearch(FeedUrlNotFoundException error) { - Log.d(TAG, "Unable to retrieve feed url, trying to retrieve feed url from search"); - String url = searchFeedUrlByTrackName(error.getTrackName(), error.getArtistName()); - if (url != null) { - Log.d(TAG, "Successfully retrieve feed url"); - isFeedFoundBySearch = true; - startFeedDownload(url); - } else { - showNoPodcastFoundError(); - Log.d(TAG, "Failed to retrieve feed url"); - } - } - - private String searchFeedUrlByTrackName(String trackName, String artistName) { - CombinedSearcher searcher = new CombinedSearcher(); - String query = trackName + " " + artistName; - List results = searcher.search(query).blockingGet(); - for (PodcastSearchResult result : results) { - if (result.feedUrl != null && result.author != null - && result.author.equalsIgnoreCase(artistName) && result.title.equalsIgnoreCase(trackName)) { - return result.feedUrl; - - } - } - return null; - } - - private void startFeedDownload(String url) { - Log.d(TAG, "Starting feed download"); - selectedDownloadUrl = UrlChecker.prepareUrl(url); - DownloadRequest request = DownloadRequestCreator.create(new Feed(selectedDownloadUrl, null)) - .withAuthentication(username, password) - .withInitiatedByUser(true) - .build(); - - download = Observable.fromCallable(() -> { - feeds = DBReader.getFeedList(); - downloader = new HttpDownloader(request); - downloader.call(); - return downloader.getResult(); - }) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(status -> checkDownloadResult(status, request.getDestination()), - error -> Log.e(TAG, Log.getStackTraceString(error))); - } - - private void checkDownloadResult(@NonNull DownloadResult status, String destination) { - if (status.isSuccessful()) { - parseFeed(destination); - } else if (status.getReason() == DownloadError.ERROR_UNAUTHORIZED) { - if (!isFinishing() && !isPaused) { - if (username != null && password != null) { - Toast.makeText(this, R.string.download_error_unauthorized, Toast.LENGTH_LONG).show(); - } - dialog = new FeedViewAuthenticationDialog(OnlineFeedViewActivity.this, - R.string.authentication_notification_title, - downloader.getDownloadRequest().getSource()).create(); - dialog.show(); - } - } else { - showErrorDialog(getString(DownloadErrorLabel.from(status.getReason())), status.getReasonDetailed()); - } - } - - @Subscribe - public void onFeedListChanged(FeedListUpdateEvent event) { - updater = Observable.fromCallable(DBReader::getFeedList) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - feeds -> { - OnlineFeedViewActivity.this.feeds = feeds; - handleUpdatedFeedStatus(); - }, error -> Log.e(TAG, Log.getStackTraceString(error)) - ); - } - - @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) - public void onEventMainThread(EpisodeDownloadEvent event) { - handleUpdatedFeedStatus(); - } - - private void parseFeed(String destination) { - Log.d(TAG, "Parsing feed"); - parser = Maybe.fromCallable(() -> doParseFeed(destination)) - .subscribeOn(Schedulers.computation()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribeWith(new DisposableMaybeObserver() { - @Override - public void onSuccess(@NonNull FeedHandlerResult result) { - showFeedInformation(result.feed, result.alternateFeedUrls); - } - - @Override - public void onComplete() { - // Ignore null result: We showed the discovery dialog. - } - - @Override - public void onError(@NonNull Throwable error) { - showErrorDialog(error.getMessage(), ""); - Log.d(TAG, "Feed parser exception: " + Log.getStackTraceString(error)); - } - }); - } - - /** - * Try to parse the feed. - * @return The FeedHandlerResult if successful. - * Null if unsuccessful but we started another attempt. - * @throws Exception If unsuccessful but we do not know a resolution. - */ - @Nullable - private FeedHandlerResult doParseFeed(String destination) throws Exception { - FeedHandler handler = new FeedHandler(); - Feed feed = new Feed(selectedDownloadUrl, null); - feed.setLocalFileUrl(destination); - File destinationFile = new File(destination); - try { - return handler.parseFeed(feed); - } catch (UnsupportedFeedtypeException e) { - Log.d(TAG, "Unsupported feed type detected"); - if ("html".equalsIgnoreCase(e.getRootElement())) { - boolean dialogShown = showFeedDiscoveryDialog(destinationFile, selectedDownloadUrl); - if (dialogShown) { - return null; // Should not display an error message - } else { - throw new UnsupportedFeedtypeException(getString(R.string.download_error_unsupported_type_html)); - } - } else { - throw e; - } - } catch (Exception e) { - Log.e(TAG, Log.getStackTraceString(e)); - throw e; - } finally { - boolean rc = destinationFile.delete(); - Log.d(TAG, "Deleted feed source file. Result: " + rc); - } - } - - /** - * Called when feed parsed successfully. - * This method is executed on the GUI thread. - */ - private void showFeedInformation(final Feed feed, Map alternateFeedUrls) { - viewBinding.progressBar.setVisibility(View.GONE); - viewBinding.feedDisplayContainer.setVisibility(View.VISIBLE); - if (isFeedFoundBySearch) { - int resId = R.string.no_feed_url_podcast_found_by_search; - Snackbar.make(findViewById(android.R.id.content), resId, Snackbar.LENGTH_LONG).show(); - } - - viewBinding.backgroundImage.setColorFilter(new LightingColorFilter(0xff828282, 0x000000)); - - viewBinding.listView.addHeaderView(headerBinding.getRoot()); - viewBinding.listView.setSelector(android.R.color.transparent); - viewBinding.listView.setAdapter(new FeedItemlistDescriptionAdapter(this, 0, feed.getItems())); - - if (StringUtils.isNotBlank(feed.getImageUrl())) { - Glide.with(this) - .load(feed.getImageUrl()) - .apply(new RequestOptions() - .placeholder(R.color.light_gray) - .error(R.color.light_gray) - .fitCenter() - .dontAnimate()) - .into(viewBinding.coverImage); - Glide.with(this) - .load(feed.getImageUrl()) - .apply(new RequestOptions() - .placeholder(R.color.image_readability_tint) - .error(R.color.image_readability_tint) - .transform(new FastBlurTransformation()) - .dontAnimate()) - .into(viewBinding.backgroundImage); - } - - viewBinding.titleLabel.setText(feed.getTitle()); - viewBinding.authorLabel.setText(feed.getAuthor()); - headerBinding.txtvDescription.setText(HtmlToPlainText.getPlainText(feed.getDescription())); - - viewBinding.subscribeButton.setOnClickListener(v -> { - if (feedInFeedlist()) { - openFeed(); - } else { - FeedDatabaseWriter.updateFeed(this, feed, false); - didPressSubscribe = true; - handleUpdatedFeedStatus(); - } - }); - - viewBinding.stopPreviewButton.setOnClickListener(v -> { - PlaybackPreferences.writeNoMediaPlaying(); - IntentUtils.sendLocalBroadcast(this, PlaybackServiceInterface.ACTION_SHUTDOWN_PLAYBACK_SERVICE); - }); - - if (UserPreferences.isEnableAutodownload()) { - SharedPreferences preferences = getSharedPreferences(PREFS, MODE_PRIVATE); - viewBinding.autoDownloadCheckBox.setChecked(preferences.getBoolean(PREF_LAST_AUTO_DOWNLOAD, true)); - } - - headerBinding.txtvDescription.setMaxLines(DESCRIPTION_MAX_LINES_COLLAPSED); - headerBinding.txtvDescription.setOnClickListener(v -> { - if (headerBinding.txtvDescription.getMaxLines() > DESCRIPTION_MAX_LINES_COLLAPSED) { - headerBinding.txtvDescription.setMaxLines(DESCRIPTION_MAX_LINES_COLLAPSED); - } else { - headerBinding.txtvDescription.setMaxLines(2000); - } - }); - - if (alternateFeedUrls.isEmpty()) { - viewBinding.alternateUrlsSpinner.setVisibility(View.GONE); - } else { - viewBinding.alternateUrlsSpinner.setVisibility(View.VISIBLE); - - final List alternateUrlsList = new ArrayList<>(); - final List alternateUrlsTitleList = new ArrayList<>(); - - alternateUrlsList.add(feed.getDownloadUrl()); - alternateUrlsTitleList.add(feed.getTitle()); - - - alternateUrlsList.addAll(alternateFeedUrls.keySet()); - for (String url : alternateFeedUrls.keySet()) { - alternateUrlsTitleList.add(alternateFeedUrls.get(url)); - } - - ArrayAdapter adapter = new ArrayAdapter(this, - R.layout.alternate_urls_item, alternateUrlsTitleList) { - @Override - public View getDropDownView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { - // reusing the old view causes a visual bug on Android <= 10 - return super.getDropDownView(position, null, parent); - } - }; - - adapter.setDropDownViewResource(R.layout.alternate_urls_dropdown_item); - viewBinding.alternateUrlsSpinner.setAdapter(adapter); - viewBinding.alternateUrlsSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { - @Override - public void onItemSelected(AdapterView parent, View view, int position, long id) { - selectedDownloadUrl = alternateUrlsList.get(position); - } - - @Override - public void onNothingSelected(AdapterView parent) { - - } - }); - } - handleUpdatedFeedStatus(); - } - - private void openFeed() { - // feed.getId() is always 0, we have to retrieve the id from the feed list from the database - MainActivityStarter mainActivityStarter = new MainActivityStarter(this); - mainActivityStarter.withOpenFeed(getFeedId()); - if (getIntent().getBooleanExtra(ARG_STARTED_FROM_SEARCH, false)) { - mainActivityStarter.withAddToBackStack(); - } - finish(); - startActivity(mainActivityStarter.getIntent()); - } - - private void handleUpdatedFeedStatus() { - if (DownloadServiceInterface.get().isDownloadingEpisode(selectedDownloadUrl)) { - viewBinding.subscribeButton.setEnabled(false); - viewBinding.subscribeButton.setText(R.string.subscribing_label); - } else if (feedInFeedlist()) { - viewBinding.subscribeButton.setEnabled(true); - viewBinding.subscribeButton.setText(R.string.open_podcast); - if (didPressSubscribe) { - didPressSubscribe = false; - - Feed feed1 = DBReader.getFeed(getFeedId()); - FeedPreferences feedPreferences = feed1.getPreferences(); - if (UserPreferences.isEnableAutodownload()) { - boolean autoDownload = viewBinding.autoDownloadCheckBox.isChecked(); - feedPreferences.setAutoDownload(autoDownload); - - SharedPreferences preferences = getSharedPreferences(PREFS, MODE_PRIVATE); - SharedPreferences.Editor editor = preferences.edit(); - editor.putBoolean(PREF_LAST_AUTO_DOWNLOAD, autoDownload); - editor.apply(); - } - if (username != null) { - feedPreferences.setUsername(username); - feedPreferences.setPassword(password); - } - DBWriter.setFeedPreferences(feedPreferences); - - openFeed(); - } - } else { - viewBinding.subscribeButton.setEnabled(true); - viewBinding.subscribeButton.setText(R.string.subscribe_label); - if (UserPreferences.isEnableAutodownload()) { - viewBinding.autoDownloadCheckBox.setVisibility(View.VISIBLE); - } - } - } - - private boolean feedInFeedlist() { - return getFeedId() != 0; - } - - private long getFeedId() { - if (feeds == null) { - return 0; - } - for (Feed f : feeds) { - if (f.getDownloadUrl().equals(selectedDownloadUrl)) { - return f.getId(); - } - } - return 0; - } - - @UiThread - private void showErrorDialog(String errorMsg, String details) { - if (!isFinishing() && !isPaused) { - MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this); - builder.setTitle(R.string.error_label); - if (errorMsg != null) { - String total = errorMsg + "\n\n" + details; - SpannableString errorMessage = new SpannableString(total); - errorMessage.setSpan(new ForegroundColorSpan(0x88888888), - errorMsg.length(), total.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - builder.setMessage(errorMessage); - } else { - builder.setMessage(R.string.download_error_error_unknown); - } - builder.setPositiveButton(android.R.string.ok, (dialog, which) -> dialog.cancel()); - if (getIntent().getBooleanExtra(ARG_WAS_MANUAL_URL, false)) { - builder.setNeutralButton(R.string.edit_url_menu, (dialog, which) -> editUrl()); - } - builder.setOnCancelListener(dialog -> { - setResult(RESULT_ERROR); - finish(); - }); - if (dialog != null && dialog.isShowing()) { - dialog.dismiss(); - } - dialog = builder.show(); - } - } - - private void editUrl() { - MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this); - builder.setTitle(R.string.edit_url_menu); - final EditTextDialogBinding dialogBinding = EditTextDialogBinding.inflate(getLayoutInflater()); - if (downloader != null) { - dialogBinding.urlEditText.setText(downloader.getDownloadRequest().getSource()); - } - builder.setView(dialogBinding.getRoot()); - builder.setPositiveButton(R.string.confirm_label, (dialog, which) -> { - setLoadingLayout(); - lookupUrlAndDownload(dialogBinding.urlEditText.getText().toString()); - }); - builder.setNegativeButton(R.string.cancel_label, (dialog1, which) -> dialog1.cancel()); - builder.setOnCancelListener(dialog1 -> { - setResult(RESULT_ERROR); - finish(); - }); - builder.show(); - } - - @Subscribe(threadMode = ThreadMode.MAIN) - public void playbackStateChanged(PlayerStatusEvent event) { - boolean isPlayingPreview = - PlaybackPreferences.getCurrentlyPlayingMediaType() == RemoteMedia.PLAYABLE_TYPE_REMOTE_MEDIA; - viewBinding.stopPreviewButton.setVisibility(isPlayingPreview ? View.VISIBLE : View.GONE); - } - - /** - * - * @return true if a FeedDiscoveryDialog is shown, false otherwise (e.g., due to no feed found). - */ - private boolean showFeedDiscoveryDialog(File feedFile, String baseUrl) { - FeedDiscoverer fd = new FeedDiscoverer(); - final Map urlsMap; - try { - urlsMap = fd.findLinks(feedFile, baseUrl); - if (urlsMap == null || urlsMap.isEmpty()) { - return false; - } - } catch (IOException e) { - e.printStackTrace(); - return false; - } - - if (isPaused || isFinishing()) { - return false; - } - - final List titles = new ArrayList<>(); - - final List urls = new ArrayList<>(urlsMap.keySet()); - for (String url : urls) { - titles.add(urlsMap.get(url)); - } - - if (urls.size() == 1) { - // Skip dialog and display the item directly - resetIntent(urls.get(0)); - startFeedDownload(urls.get(0)); - return true; - } - - final ArrayAdapter adapter = new ArrayAdapter<>(OnlineFeedViewActivity.this, - R.layout.ellipsize_start_listitem, R.id.txtvTitle, titles); - DialogInterface.OnClickListener onClickListener = (dialog, which) -> { - String selectedUrl = urls.get(which); - dialog.dismiss(); - resetIntent(selectedUrl); - startFeedDownload(selectedUrl); - }; - - MaterialAlertDialogBuilder ab = new MaterialAlertDialogBuilder(OnlineFeedViewActivity.this) - .setTitle(R.string.feeds_label) - .setCancelable(true) - .setOnCancelListener(dialog -> finish()) - .setAdapter(adapter, onClickListener); - - runOnUiThread(() -> { - if(dialog != null && dialog.isShowing()) { - dialog.dismiss(); - } - dialog = ab.show(); - }); - return true; - } - - private class FeedViewAuthenticationDialog extends AuthenticationDialog { - - private final String feedUrl; - - FeedViewAuthenticationDialog(Context context, int titleRes, String feedUrl) { - super(context, titleRes, true, username, password); - this.feedUrl = feedUrl; - } - - @Override - protected void onCancelled() { - super.onCancelled(); - finish(); - } - - @Override - protected void onConfirmed(String username, String password) { - OnlineFeedViewActivity.this.username = username; - OnlineFeedViewActivity.this.password = password; - startFeedDownload(feedUrl); - } - } - -} diff --git a/app/src/main/java/de/danoeh/antennapod/activity/PlaybackSpeedDialogActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/PlaybackSpeedDialogActivity.java deleted file mode 100644 index 37f13272a..000000000 --- a/app/src/main/java/de/danoeh/antennapod/activity/PlaybackSpeedDialogActivity.java +++ /dev/null @@ -1,29 +0,0 @@ -package de.danoeh.antennapod.activity; - -import androidx.annotation.NonNull; -import androidx.appcompat.app.AppCompatActivity; - -import android.content.DialogInterface; -import android.os.Bundle; - -import de.danoeh.antennapod.ui.common.ThemeSwitcher; -import de.danoeh.antennapod.dialog.VariableSpeedDialog; - -public class PlaybackSpeedDialogActivity extends AppCompatActivity { - - @Override - protected void onCreate(Bundle savedInstanceState) { - setTheme(ThemeSwitcher.getTranslucentTheme(this)); - super.onCreate(savedInstanceState); - VariableSpeedDialog speedDialog = new InnerVariableSpeedDialog(); - speedDialog.show(getSupportFragmentManager(), null); - } - - public static class InnerVariableSpeedDialog extends VariableSpeedDialog { - @Override - public void onDismiss(@NonNull DialogInterface dialog) { - super.onDismiss(dialog); - getActivity().finish(); - } - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/activity/PreferenceActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/PreferenceActivity.java deleted file mode 100644 index aa3b05715..000000000 --- a/app/src/main/java/de/danoeh/antennapod/activity/PreferenceActivity.java +++ /dev/null @@ -1,194 +0,0 @@ -package de.danoeh.antennapod.activity; - -import android.content.Intent; -import android.os.Build; -import android.os.Bundle; -import android.provider.Settings; -import android.util.Log; -import android.view.MenuItem; - -import android.view.View; -import android.view.inputmethod.InputMethodManager; - -import androidx.appcompat.app.ActionBar; -import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import androidx.appcompat.app.AppCompatActivity; -import androidx.preference.PreferenceFragmentCompat; - -import com.bytehamster.lib.preferencesearch.SearchPreferenceResult; -import com.bytehamster.lib.preferencesearch.SearchPreferenceResultListener; - -import com.google.android.material.snackbar.Snackbar; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.ui.common.ThemeSwitcher; - -import de.danoeh.antennapod.event.MessageEvent; -import de.danoeh.antennapod.fragment.preferences.ImportExportPreferencesFragment; -import de.danoeh.antennapod.fragment.preferences.MainPreferencesFragment; -import de.danoeh.antennapod.fragment.preferences.DownloadsPreferencesFragment; -import de.danoeh.antennapod.fragment.preferences.PlaybackPreferencesFragment; -import de.danoeh.antennapod.fragment.preferences.SwipePreferencesFragment; -import de.danoeh.antennapod.fragment.preferences.UserInterfacePreferencesFragment; -import de.danoeh.antennapod.ui.preferences.screen.AutoDownloadPreferencesFragment; -import de.danoeh.antennapod.ui.preferences.screen.NotificationPreferencesFragment; -import de.danoeh.antennapod.ui.preferences.screen.synchronization.SynchronizationPreferencesFragment; -import de.danoeh.antennapod.ui.preferences.databinding.SettingsActivityBinding; -import org.greenrobot.eventbus.EventBus; -import org.greenrobot.eventbus.Subscribe; -import org.greenrobot.eventbus.ThreadMode; - -/** - * PreferenceActivity for API 11+. In order to change the behavior of the preference UI, see - * PreferenceController. - */ -public class PreferenceActivity extends AppCompatActivity implements SearchPreferenceResultListener { - private static final String FRAGMENT_TAG = "tag_preferences"; - public static final String OPEN_AUTO_DOWNLOAD_SETTINGS = "OpenAutoDownloadSettings"; - private SettingsActivityBinding binding; - - @Override - protected void onCreate(Bundle savedInstanceState) { - setTheme(ThemeSwitcher.getTheme(this)); - super.onCreate(savedInstanceState); - - ActionBar ab = getSupportActionBar(); - if (ab != null) { - ab.setDisplayHomeAsUpEnabled(true); - } - - binding = SettingsActivityBinding.inflate(getLayoutInflater()); - setContentView(binding.getRoot()); - - if (getSupportFragmentManager().findFragmentByTag(FRAGMENT_TAG) == null) { - getSupportFragmentManager().beginTransaction() - .replace(binding.settingsContainer.getId(), new MainPreferencesFragment(), FRAGMENT_TAG) - .commit(); - } - Intent intent = getIntent(); - if (intent.getBooleanExtra(OPEN_AUTO_DOWNLOAD_SETTINGS, false)) { - openScreen(R.xml.preferences_autodownload); - } - } - - private PreferenceFragmentCompat getPreferenceScreen(int screen) { - PreferenceFragmentCompat prefFragment = null; - - if (screen == R.xml.preferences_user_interface) { - prefFragment = new UserInterfacePreferencesFragment(); - } else if (screen == R.xml.preferences_downloads) { - prefFragment = new DownloadsPreferencesFragment(); - } else if (screen == R.xml.preferences_import_export) { - prefFragment = new ImportExportPreferencesFragment(); - } else if (screen == R.xml.preferences_autodownload) { - prefFragment = new AutoDownloadPreferencesFragment(); - } else if (screen == R.xml.preferences_synchronization) { - prefFragment = new SynchronizationPreferencesFragment(); - } else if (screen == R.xml.preferences_playback) { - prefFragment = new PlaybackPreferencesFragment(); - } else if (screen == R.xml.preferences_notifications) { - prefFragment = new NotificationPreferencesFragment(); - } else if (screen == R.xml.preferences_swipe) { - prefFragment = new SwipePreferencesFragment(); - } - return prefFragment; - } - - public static int getTitleOfPage(int preferences) { - if (preferences == R.xml.preferences_downloads) { - return R.string.downloads_pref; - } else if (preferences == R.xml.preferences_autodownload) { - return R.string.pref_automatic_download_title; - } else if (preferences == R.xml.preferences_playback) { - return R.string.playback_pref; - } else if (preferences == R.xml.preferences_import_export) { - return R.string.import_export_pref; - } else if (preferences == R.xml.preferences_user_interface) { - return R.string.user_interface_label; - } else if (preferences == R.xml.preferences_synchronization) { - return R.string.synchronization_pref; - } else if (preferences == R.xml.preferences_notifications) { - return R.string.notification_pref_fragment; - } else if (preferences == R.xml.feed_settings) { - return R.string.feed_settings_label; - } else if (preferences == R.xml.preferences_swipe) { - return R.string.swipeactions_label; - } - return R.string.settings_label; - } - - public PreferenceFragmentCompat openScreen(int screen) { - PreferenceFragmentCompat fragment = getPreferenceScreen(screen); - if (screen == R.xml.preferences_notifications && Build.VERSION.SDK_INT >= 26) { - Intent intent = new Intent(); - intent.setAction(Settings.ACTION_APP_NOTIFICATION_SETTINGS); - intent.putExtra(Settings.EXTRA_APP_PACKAGE, getPackageName()); - startActivity(intent); - } else { - getSupportFragmentManager().beginTransaction() - .replace(binding.settingsContainer.getId(), fragment) - .addToBackStack(getString(getTitleOfPage(screen))).commit(); - } - - - return fragment; - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - if (item.getItemId() == android.R.id.home) { - if (getSupportFragmentManager().getBackStackEntryCount() == 0) { - finish(); - } else { - InputMethodManager imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE); - View view = getCurrentFocus(); - //If no view currently has focus, create a new one, just so we can grab a window token from it - if (view == null) { - view = new View(this); - } - imm.hideSoftInputFromWindow(view.getWindowToken(), 0); - getSupportFragmentManager().popBackStack(); - } - return true; - } - return false; - } - - @Override - public void onSearchResultClicked(SearchPreferenceResult result) { - int screen = result.getResourceFile(); - if (screen == R.xml.feed_settings) { - MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this); - builder.setTitle(R.string.feed_settings_label); - builder.setMessage(R.string.pref_feed_settings_dialog_msg); - builder.setPositiveButton(android.R.string.ok, null); - builder.show(); - } else if (screen == R.xml.preferences_notifications) { - openScreen(screen); - } else { - PreferenceFragmentCompat fragment = openScreen(result.getResourceFile()); - result.highlight(fragment); - } - } - - @Override - protected void onStart() { - super.onStart(); - EventBus.getDefault().register(this); - } - - @Override - protected void onStop() { - super.onStop(); - EventBus.getDefault().unregister(this); - } - - @Subscribe(threadMode = ThreadMode.MAIN) - public void onEventMainThread(MessageEvent event) { - Log.d(FRAGMENT_TAG, "onEvent(" + event + ")"); - Snackbar s = Snackbar.make(binding.getRoot(), event.message, Snackbar.LENGTH_LONG); - if (event.action != null) { - s.setAction(event.actionText, v -> event.action.accept(this)); - } - s.show(); - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/activity/SplashActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/SplashActivity.java index 43da309d7..4ea33ca3e 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/SplashActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/SplashActivity.java @@ -7,7 +7,7 @@ import android.os.Bundle; import android.view.View; import android.widget.Toast; import androidx.annotation.Nullable; -import de.danoeh.antennapod.error.CrashReportWriter; +import de.danoeh.antennapod.CrashReportWriter; import de.danoeh.antennapod.storage.database.PodDBAdapter; import io.reactivex.Completable; import io.reactivex.android.schedulers.AndroidSchedulers; diff --git a/app/src/main/java/de/danoeh/antennapod/activity/VideoplayerActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/VideoplayerActivity.java deleted file mode 100644 index 313d97248..000000000 --- a/app/src/main/java/de/danoeh/antennapod/activity/VideoplayerActivity.java +++ /dev/null @@ -1,815 +0,0 @@ -package de.danoeh.antennapod.activity; - -import android.content.Intent; -import android.graphics.PixelFormat; -import android.graphics.drawable.ColorDrawable; -import android.media.AudioManager; -import android.os.Build; -import android.os.Bundle; -import android.os.Handler; -import android.os.Looper; -import android.util.Log; -import android.util.Pair; -import android.view.Gravity; -import android.view.KeyEvent; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.MotionEvent; -import android.view.SurfaceHolder; -import android.view.View; -import android.view.Window; -import android.view.WindowManager; -import android.view.animation.AlphaAnimation; -import android.view.animation.Animation; -import android.view.animation.AnimationSet; -import android.view.animation.AnimationUtils; -import android.view.animation.ScaleAnimation; -import android.widget.EditText; -import android.widget.FrameLayout; -import android.widget.SeekBar; -import androidx.annotation.Nullable; -import androidx.interpolator.view.animation.FastOutSlowInInterpolator; -import com.bumptech.glide.Glide; -import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.dialog.MediaPlayerErrorDialog; -import de.danoeh.antennapod.dialog.VariableSpeedDialog; -import de.danoeh.antennapod.event.MessageEvent; -import de.danoeh.antennapod.event.playback.BufferUpdateEvent; -import de.danoeh.antennapod.event.playback.PlaybackPositionEvent; -import de.danoeh.antennapod.event.PlayerErrorEvent; -import de.danoeh.antennapod.event.playback.PlaybackServiceEvent; -import de.danoeh.antennapod.event.playback.SleepTimerUpdatedEvent; -import de.danoeh.antennapod.fragment.ChaptersFragment; -import de.danoeh.antennapod.playback.service.PlaybackController; -import de.danoeh.antennapod.playback.service.PlaybackService; -import de.danoeh.antennapod.storage.preferences.UserPreferences; -import de.danoeh.antennapod.storage.database.DBReader; -import de.danoeh.antennapod.storage.database.DBWriter; -import de.danoeh.antennapod.ui.common.Converter; -import de.danoeh.antennapod.core.util.FeedItemUtil; -import de.danoeh.antennapod.core.util.IntentUtils; -import de.danoeh.antennapod.core.util.ShareUtils; -import de.danoeh.antennapod.core.util.gui.PictureInPictureUtil; -import de.danoeh.antennapod.databinding.VideoplayerActivityBinding; -import de.danoeh.antennapod.dialog.PlaybackControlsDialog; -import de.danoeh.antennapod.dialog.ShareDialog; -import de.danoeh.antennapod.dialog.SkipPreferenceDialog; -import de.danoeh.antennapod.dialog.SleepTimerDialog; -import de.danoeh.antennapod.model.feed.FeedItem; -import de.danoeh.antennapod.model.feed.FeedMedia; -import de.danoeh.antennapod.model.playback.Playable; -import de.danoeh.antennapod.playback.base.PlayerStatus; -import de.danoeh.antennapod.playback.cast.CastEnabledActivity; -import de.danoeh.antennapod.ui.appstartintent.MainActivityStarter; -import de.danoeh.antennapod.ui.episodes.TimeSpeedConverter; -import io.reactivex.Observable; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.Disposable; -import io.reactivex.schedulers.Schedulers; -import org.apache.commons.lang3.StringUtils; -import org.greenrobot.eventbus.EventBus; -import org.greenrobot.eventbus.Subscribe; -import org.greenrobot.eventbus.ThreadMode; - -/** - * Activity for playing video files. - */ -public class VideoplayerActivity extends CastEnabledActivity implements SeekBar.OnSeekBarChangeListener { - private static final String TAG = "VideoplayerActivity"; - - /** - * True if video controls are currently visible. - */ - private boolean videoControlsShowing = true; - private boolean videoSurfaceCreated = false; - private boolean destroyingDueToReload = false; - private long lastScreenTap = 0; - private final Handler videoControlsHider = new Handler(Looper.getMainLooper()); - private VideoplayerActivityBinding viewBinding; - private PlaybackController controller; - private boolean showTimeLeft = false; - private boolean isFavorite = false; - private boolean switchToAudioOnly = false; - private Disposable disposable; - private float prog; - - @Override - protected void onCreate(Bundle savedInstanceState) { - getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - getWindow().addFlags(WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN); - // has to be called before setting layout content - supportRequestWindowFeature(Window.FEATURE_ACTION_BAR_OVERLAY); - setTheme(R.style.Theme_AntennaPod_VideoPlayer); - super.onCreate(savedInstanceState); - - Log.d(TAG, "onCreate()"); - - getWindow().setFormat(PixelFormat.TRANSPARENT); - viewBinding = VideoplayerActivityBinding.inflate(LayoutInflater.from(this)); - setContentView(viewBinding.getRoot()); - setupView(); - getSupportActionBar().setBackgroundDrawable(new ColorDrawable(0x80000000)); - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - } - - @Override - protected void onResume() { - super.onResume(); - switchToAudioOnly = false; - if (PlaybackService.isCasting()) { - Intent intent = PlaybackService.getPlayerActivityIntent(this); - if (!intent.getComponent().getClassName().equals(VideoplayerActivity.class.getName())) { - destroyingDueToReload = true; - finish(); - startActivity(intent); - } - } - } - - @Override - protected void onStop() { - if (controller != null) { - controller.release(); - controller = null; // prevent leak - } - if (disposable != null) { - disposable.dispose(); - } - EventBus.getDefault().unregister(this); - super.onStop(); - if (!PictureInPictureUtil.isInPictureInPictureMode(this)) { - videoControlsHider.removeCallbacks(hideVideoControls); - } - // Controller released; we will not receive buffering updates - viewBinding.progressBar.setVisibility(View.GONE); - } - - @Override - public void onUserLeaveHint() { - if (!PictureInPictureUtil.isInPictureInPictureMode(this)) { - compatEnterPictureInPicture(); - } - } - - @Override - protected void onStart() { - super.onStart(); - controller = newPlaybackController(); - controller.init(); - loadMediaInfo(); - onPositionObserverUpdate(); - EventBus.getDefault().register(this); - } - - @Override - protected void onPause() { - if (!PictureInPictureUtil.isInPictureInPictureMode(this)) { - if (controller != null && controller.getStatus() == PlayerStatus.PLAYING) { - controller.pause(); - } - } - super.onPause(); - } - - @Override - public void onTrimMemory(int level) { - super.onTrimMemory(level); - Glide.get(this).trimMemory(level); - } - - @Override - public void onLowMemory() { - super.onLowMemory(); - Glide.get(this).clearMemory(); - } - - private PlaybackController newPlaybackController() { - return new PlaybackController(this) { - @Override - protected void updatePlayButtonShowsPlay(boolean showPlay) { - viewBinding.playButton.setIsShowPlay(showPlay); - if (showPlay) { - getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - } else { - getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - setupVideoAspectRatio(); - if (videoSurfaceCreated && controller != null) { - Log.d(TAG, "Videosurface already created, setting videosurface now"); - controller.setVideoSurface(viewBinding.videoView.getHolder()); - } - } - } - - @Override - public void loadMediaInfo() { - VideoplayerActivity.this.loadMediaInfo(); - } - - @Override - public void onPlaybackEnd() { - finish(); - } - }; - } - - @Subscribe(threadMode = ThreadMode.MAIN) - @SuppressWarnings("unused") - public void bufferUpdate(BufferUpdateEvent event) { - if (event.hasStarted()) { - viewBinding.progressBar.setVisibility(View.VISIBLE); - } else if (event.hasEnded()) { - viewBinding.progressBar.setVisibility(View.INVISIBLE); - } else { - viewBinding.sbPosition.setSecondaryProgress((int) (event.getProgress() * viewBinding.sbPosition.getMax())); - } - } - - @Subscribe(threadMode = ThreadMode.MAIN) - @SuppressWarnings("unused") - public void sleepTimerUpdate(SleepTimerUpdatedEvent event) { - if (event.isCancelled() || event.wasJustEnabled()) { - supportInvalidateOptionsMenu(); - } - } - - protected void loadMediaInfo() { - Log.d(TAG, "loadMediaInfo()"); - if (controller == null || controller.getMedia() == null) { - return; - } - if (controller.getStatus() == PlayerStatus.PLAYING && !controller.isPlayingVideoLocally()) { - Log.d(TAG, "Closing, no longer video"); - destroyingDueToReload = true; - finish(); - new MainActivityStarter(this).withOpenPlayer().start(); - return; - } - showTimeLeft = UserPreferences.shouldShowRemainingTime(); - onPositionObserverUpdate(); - checkFavorite(); - Playable media = controller.getMedia(); - if (media != null) { - getSupportActionBar().setSubtitle(media.getEpisodeTitle()); - getSupportActionBar().setTitle(media.getFeedTitle()); - } - } - - protected void setupView() { - showTimeLeft = UserPreferences.shouldShowRemainingTime(); - Log.d("timeleft", showTimeLeft ? "true" : "false"); - viewBinding.durationLabel.setOnClickListener(v -> { - showTimeLeft = !showTimeLeft; - Playable media = controller.getMedia(); - if (media == null) { - return; - } - - TimeSpeedConverter converter = new TimeSpeedConverter(controller.getCurrentPlaybackSpeedMultiplier()); - String length; - if (showTimeLeft) { - int remainingTime = converter.convert(media.getDuration() - media.getPosition()); - length = "-" + Converter.getDurationStringLong(remainingTime); - } else { - int duration = converter.convert(media.getDuration()); - length = Converter.getDurationStringLong(duration); - } - viewBinding.durationLabel.setText(length); - - UserPreferences.setShowRemainTimeSetting(showTimeLeft); - Log.d("timeleft on click", showTimeLeft ? "true" : "false"); - }); - - viewBinding.sbPosition.setOnSeekBarChangeListener(this); - viewBinding.rewindButton.setOnClickListener(v -> onRewind()); - viewBinding.rewindButton.setOnLongClickListener(v -> { - SkipPreferenceDialog.showSkipPreference(VideoplayerActivity.this, - SkipPreferenceDialog.SkipDirection.SKIP_REWIND, null); - return true; - }); - viewBinding.playButton.setIsVideoScreen(true); - viewBinding.playButton.setOnClickListener(v -> onPlayPause()); - viewBinding.fastForwardButton.setOnClickListener(v -> onFastForward()); - viewBinding.fastForwardButton.setOnLongClickListener(v -> { - SkipPreferenceDialog.showSkipPreference(VideoplayerActivity.this, - SkipPreferenceDialog.SkipDirection.SKIP_FORWARD, null); - return false; - }); - // To suppress touches directly below the slider - viewBinding.bottomControlsContainer.setOnTouchListener((view, motionEvent) -> true); - viewBinding.bottomControlsContainer.setFitsSystemWindows(true); - viewBinding.videoView.getHolder().addCallback(surfaceHolderCallback); - viewBinding.videoView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION); - - setupVideoControlsToggler(); - getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, - WindowManager.LayoutParams.FLAG_FULLSCREEN); - - viewBinding.videoPlayerContainer.setOnTouchListener(onVideoviewTouched); - viewBinding.videoPlayerContainer.getViewTreeObserver().addOnGlobalLayoutListener(() -> - viewBinding.videoView.setAvailableSize( - viewBinding.videoPlayerContainer.getWidth(), viewBinding.videoPlayerContainer.getHeight())); - } - - private final Runnable hideVideoControls = () -> { - if (videoControlsShowing) { - Log.d(TAG, "Hiding video controls"); - getSupportActionBar().hide(); - hideVideoControls(true); - videoControlsShowing = false; - } - }; - - private final View.OnTouchListener onVideoviewTouched = (v, event) -> { - if (event.getAction() != MotionEvent.ACTION_DOWN) { - return false; - } - if (PictureInPictureUtil.isInPictureInPictureMode(this)) { - return true; - } - videoControlsHider.removeCallbacks(hideVideoControls); - - if (System.currentTimeMillis() - lastScreenTap < 300) { - if (event.getX() > v.getMeasuredWidth() / 2.0f) { - onFastForward(); - showSkipAnimation(true); - } else { - onRewind(); - showSkipAnimation(false); - } - if (videoControlsShowing) { - getSupportActionBar().hide(); - hideVideoControls(false); - videoControlsShowing = false; - } - return true; - } - - toggleVideoControlsVisibility(); - if (videoControlsShowing) { - setupVideoControlsToggler(); - } - - lastScreenTap = System.currentTimeMillis(); - return true; - }; - - private void showSkipAnimation(boolean isForward) { - AnimationSet skipAnimation = new AnimationSet(true); - skipAnimation.addAnimation(new ScaleAnimation(1f, 2f, 1f, 2f, - Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f)); - skipAnimation.addAnimation(new AlphaAnimation(1f, 0f)); - skipAnimation.setFillAfter(false); - skipAnimation.setDuration(800); - - FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) viewBinding.skipAnimationImage.getLayoutParams(); - if (isForward) { - viewBinding.skipAnimationImage.setImageResource(R.drawable.ic_fast_forward_video_white); - params.gravity = Gravity.RIGHT | Gravity.CENTER_VERTICAL; - } else { - viewBinding.skipAnimationImage.setImageResource(R.drawable.ic_fast_rewind_video_white); - params.gravity = Gravity.LEFT | Gravity.CENTER_VERTICAL; - } - - viewBinding.skipAnimationImage.setVisibility(View.VISIBLE); - viewBinding.skipAnimationImage.setLayoutParams(params); - viewBinding.skipAnimationImage.startAnimation(skipAnimation); - skipAnimation.setAnimationListener(new Animation.AnimationListener() { - @Override - public void onAnimationStart(Animation animation) { - } - - @Override - public void onAnimationEnd(Animation animation) { - viewBinding.skipAnimationImage.setVisibility(View.GONE); - } - - @Override - public void onAnimationRepeat(Animation animation) { - - } - }); - } - - private void setupVideoControlsToggler() { - videoControlsHider.removeCallbacks(hideVideoControls); - videoControlsHider.postDelayed(hideVideoControls, 2500); - } - - private void setupVideoAspectRatio() { - if (videoSurfaceCreated && controller != null) { - Pair videoSize = controller.getVideoSize(); - if (videoSize != null && videoSize.first > 0 && videoSize.second > 0) { - Log.d(TAG, "Width,height of video: " + videoSize.first + ", " + videoSize.second); - viewBinding.videoView.setVideoSize(videoSize.first, videoSize.second); - } else { - Log.e(TAG, "Could not determine video size"); - } - } - } - - private void toggleVideoControlsVisibility() { - if (videoControlsShowing) { - getSupportActionBar().hide(); - hideVideoControls(true); - } else { - getSupportActionBar().show(); - showVideoControls(); - } - videoControlsShowing = !videoControlsShowing; - } - - void onRewind() { - if (controller == null) { - return; - } - int curr = controller.getPosition(); - controller.seekTo(curr - UserPreferences.getRewindSecs() * 1000); - setupVideoControlsToggler(); - } - - void onPlayPause() { - if (controller == null) { - return; - } - controller.playPause(); - setupVideoControlsToggler(); - } - - void onFastForward() { - if (controller == null) { - return; - } - int curr = controller.getPosition(); - controller.seekTo(curr + UserPreferences.getFastForwardSecs() * 1000); - setupVideoControlsToggler(); - } - - private final SurfaceHolder.Callback surfaceHolderCallback = new SurfaceHolder.Callback() { - @Override - public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { - holder.setFixedSize(width, height); - } - - @Override - public void surfaceCreated(SurfaceHolder holder) { - Log.d(TAG, "Videoview holder created"); - videoSurfaceCreated = true; - if (controller != null && controller.getStatus() == PlayerStatus.PLAYING) { - controller.setVideoSurface(holder); - } - setupVideoAspectRatio(); - } - - @Override - public void surfaceDestroyed(SurfaceHolder holder) { - Log.d(TAG, "Videosurface was destroyed"); - videoSurfaceCreated = false; - if (controller != null && !destroyingDueToReload && !switchToAudioOnly) { - controller.notifyVideoSurfaceAbandoned(); - } - } - }; - - private void showVideoControls() { - viewBinding.bottomControlsContainer.setVisibility(View.VISIBLE); - viewBinding.controlsContainer.setVisibility(View.VISIBLE); - final Animation animation = AnimationUtils.loadAnimation(this, R.anim.fade_in); - if (animation != null) { - viewBinding.bottomControlsContainer.startAnimation(animation); - viewBinding.controlsContainer.startAnimation(animation); - } - viewBinding.videoView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_VISIBLE); - } - - private void hideVideoControls(boolean showAnimation) { - if (showAnimation) { - final Animation animation = AnimationUtils.loadAnimation(this, R.anim.fade_out); - if (animation != null) { - viewBinding.bottomControlsContainer.startAnimation(animation); - viewBinding.controlsContainer.startAnimation(animation); - } - } - getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE - | View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION - | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION); - viewBinding.bottomControlsContainer.setFitsSystemWindows(true); - - viewBinding.bottomControlsContainer.setVisibility(View.GONE); - viewBinding.controlsContainer.setVisibility(View.GONE); - } - - @Subscribe(threadMode = ThreadMode.MAIN) - public void onEventMainThread(PlaybackPositionEvent event) { - onPositionObserverUpdate(); - } - - @Subscribe(threadMode = ThreadMode.MAIN) - public void onPlaybackServiceChanged(PlaybackServiceEvent event) { - if (event.action == PlaybackServiceEvent.Action.SERVICE_SHUT_DOWN) { - finish(); - } - } - - @Subscribe(threadMode = ThreadMode.MAIN) - public void onMediaPlayerError(PlayerErrorEvent event) { - MediaPlayerErrorDialog.show(this, event); - } - - @Subscribe(threadMode = ThreadMode.MAIN) - public void onEventMainThread(MessageEvent event) { - Log.d(TAG, "onEvent(" + event + ")"); - final MaterialAlertDialogBuilder errorDialog = new MaterialAlertDialogBuilder(this); - errorDialog.setMessage(event.message); - if (event.action != null) { - errorDialog.setPositiveButton(event.actionText, (dialog, which) -> event.action.accept(this)); - } - errorDialog.show(); - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - super.onCreateOptionsMenu(menu); - requestCastButton(menu); - MenuInflater inflater = getMenuInflater(); - inflater.inflate(R.menu.mediaplayer, menu); - return true; - } - - @Override - public boolean onPrepareOptionsMenu(Menu menu) { - super.onPrepareOptionsMenu(menu); - if (controller == null) { - return false; - } - Playable media = controller.getMedia(); - boolean isFeedMedia = (media instanceof FeedMedia); - - menu.findItem(R.id.open_feed_item).setVisible(isFeedMedia); // FeedMedia implies it belongs to a Feed - - boolean hasWebsiteLink = getWebsiteLinkWithFallback(media) != null; - menu.findItem(R.id.visit_website_item).setVisible(hasWebsiteLink); - - boolean isItemAndHasLink = isFeedMedia && ShareUtils.hasLinkToShare(((FeedMedia) media).getItem()); - boolean isItemHasDownloadLink = isFeedMedia && ((FeedMedia) media).getDownloadUrl() != null; - menu.findItem(R.id.share_item).setVisible(hasWebsiteLink || isItemAndHasLink || isItemHasDownloadLink); - - menu.findItem(R.id.add_to_favorites_item).setVisible(false); - menu.findItem(R.id.remove_from_favorites_item).setVisible(false); - if (isFeedMedia) { - menu.findItem(R.id.add_to_favorites_item).setVisible(!isFavorite); - menu.findItem(R.id.remove_from_favorites_item).setVisible(isFavorite); - } - - menu.findItem(R.id.set_sleeptimer_item).setVisible(!controller.sleepTimerActive()); - menu.findItem(R.id.disable_sleeptimer_item).setVisible(controller.sleepTimerActive()); - - menu.findItem(R.id.player_switch_to_audio_only).setVisible(true); - - menu.findItem(R.id.audio_controls).setVisible(controller.getAudioTracks().size() >= 2); - menu.findItem(R.id.playback_speed).setVisible(true); - menu.findItem(R.id.player_show_chapters).setVisible(true); - return true; - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - if (item.getItemId() == R.id.player_switch_to_audio_only) { - switchToAudioOnly = true; - finish(); - return true; - } else if (item.getItemId() == android.R.id.home) { - Intent intent = new Intent(VideoplayerActivity.this, MainActivity.class); - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK); - startActivity(intent); - finish(); - return true; - } else if (item.getItemId() == R.id.player_show_chapters) { - new ChaptersFragment().show(getSupportFragmentManager(), ChaptersFragment.TAG); - return true; - } - - if (controller == null) { - return false; - } - - Playable media = controller.getMedia(); - if (media == null) { - return false; - } - final @Nullable FeedItem feedItem = getFeedItem(media); // some options option requires FeedItem - if (item.getItemId() == R.id.add_to_favorites_item && feedItem != null) { - DBWriter.addFavoriteItem(feedItem); - isFavorite = true; - invalidateOptionsMenu(); - } else if (item.getItemId() == R.id.remove_from_favorites_item && feedItem != null) { - DBWriter.removeFavoriteItem(feedItem); - isFavorite = false; - invalidateOptionsMenu(); - } else if (item.getItemId() == R.id.disable_sleeptimer_item - || item.getItemId() == R.id.set_sleeptimer_item) { - new SleepTimerDialog().show(getSupportFragmentManager(), "SleepTimerDialog"); - } else if (item.getItemId() == R.id.audio_controls) { - PlaybackControlsDialog dialog = PlaybackControlsDialog.newInstance(); - dialog.show(getSupportFragmentManager(), "playback_controls"); - } else if (item.getItemId() == R.id.open_feed_item && feedItem != null) { - Intent intent = MainActivity.getIntentToOpenFeed(this, feedItem.getFeedId()); - startActivity(intent); - } else if (item.getItemId() == R.id.visit_website_item) { - IntentUtils.openInBrowser(VideoplayerActivity.this, getWebsiteLinkWithFallback(media)); - } else if (item.getItemId() == R.id.share_item && feedItem != null) { - ShareDialog shareDialog = ShareDialog.newInstance(feedItem); - shareDialog.show(getSupportFragmentManager(), "ShareEpisodeDialog"); - } else if (item.getItemId() == R.id.playback_speed) { - new VariableSpeedDialog().show(getSupportFragmentManager(), null); - } else { - return false; - } - return true; - } - - private static String getWebsiteLinkWithFallback(Playable media) { - if (media == null) { - return null; - } else if (StringUtils.isNotBlank(media.getWebsiteLink())) { - return media.getWebsiteLink(); - } else if (media instanceof FeedMedia) { - return FeedItemUtil.getLinkWithFallback(((FeedMedia) media).getItem()); - } - return null; - } - - void onPositionObserverUpdate() { - if (controller == null) { - return; - } - - TimeSpeedConverter converter = new TimeSpeedConverter(controller.getCurrentPlaybackSpeedMultiplier()); - int currentPosition = converter.convert(controller.getPosition()); - int duration = converter.convert(controller.getDuration()); - int remainingTime = converter.convert( - controller.getDuration() - controller.getPosition()); - Log.d(TAG, "currentPosition " + Converter.getDurationStringLong(currentPosition)); - if (currentPosition == Playable.INVALID_TIME - || duration == Playable.INVALID_TIME) { - Log.w(TAG, "Could not react to position observer update because of invalid time"); - return; - } - viewBinding.positionLabel.setText(Converter.getDurationStringLong(currentPosition)); - if (showTimeLeft) { - viewBinding.durationLabel.setText("-" + Converter.getDurationStringLong(remainingTime)); - } else { - viewBinding.durationLabel.setText(Converter.getDurationStringLong(duration)); - } - updateProgressbarPosition(currentPosition, duration); - } - - private void updateProgressbarPosition(int position, int duration) { - Log.d(TAG, "updateProgressbarPosition(" + position + ", " + duration + ")"); - float progress = ((float) position) / duration; - viewBinding.sbPosition.setProgress((int) (progress * viewBinding.sbPosition.getMax())); - } - - @Override - public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { - if (controller == null) { - return; - } - if (fromUser) { - prog = progress / ((float) seekBar.getMax()); - TimeSpeedConverter converter = new TimeSpeedConverter(controller.getCurrentPlaybackSpeedMultiplier()); - int position = converter.convert((int) (prog * controller.getDuration())); - viewBinding.seekPositionLabel.setText(Converter.getDurationStringLong(position)); - } - } - - @Override - public void onStartTrackingTouch(SeekBar seekBar) { - viewBinding.seekCardView.setScaleX(.8f); - viewBinding.seekCardView.setScaleY(.8f); - viewBinding.seekCardView.animate() - .setInterpolator(new FastOutSlowInInterpolator()) - .alpha(1f).scaleX(1f).scaleY(1f) - .setDuration(200) - .start(); - videoControlsHider.removeCallbacks(hideVideoControls); - } - - @Override - public void onStopTrackingTouch(SeekBar seekBar) { - if (controller != null) { - controller.seekTo((int) (prog * controller.getDuration())); - } - viewBinding.seekCardView.setScaleX(1f); - viewBinding.seekCardView.setScaleY(1f); - viewBinding.seekCardView.animate() - .setInterpolator(new FastOutSlowInInterpolator()) - .alpha(0f).scaleX(.8f).scaleY(.8f) - .setDuration(200) - .start(); - setupVideoControlsToggler(); - } - - private void checkFavorite() { - FeedItem feedItem = getFeedItem(controller.getMedia()); - if (feedItem == null) { - return; - } - if (disposable != null) { - disposable.dispose(); - } - disposable = Observable.fromCallable(() -> DBReader.getFeedItem(feedItem.getId())) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - item -> { - boolean isFav = item.isTagged(FeedItem.TAG_FAVORITE); - if (isFavorite != isFav) { - isFavorite = isFav; - invalidateOptionsMenu(); - } - }, error -> Log.e(TAG, Log.getStackTraceString(error))); - } - - @Nullable - private static FeedItem getFeedItem(@Nullable Playable playable) { - if (playable instanceof FeedMedia) { - return ((FeedMedia) playable).getItem(); - } else { - return null; - } - } - - private void compatEnterPictureInPicture() { - if (PictureInPictureUtil.supportsPictureInPicture(this) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - getSupportActionBar().hide(); - hideVideoControls(false); - enterPictureInPictureMode(); - } - } - - //Hardware keyboard support - @Override - public boolean onKeyUp(int keyCode, KeyEvent event) { - View currentFocus = getCurrentFocus(); - if (currentFocus instanceof EditText) { - return super.onKeyUp(keyCode, event); - } - - AudioManager audioManager = (AudioManager) getSystemService(AUDIO_SERVICE); - - switch (keyCode) { - case KeyEvent.KEYCODE_P: //Fallthrough - case KeyEvent.KEYCODE_SPACE: - onPlayPause(); - toggleVideoControlsVisibility(); - return true; - case KeyEvent.KEYCODE_J: //Fallthrough - case KeyEvent.KEYCODE_A: - case KeyEvent.KEYCODE_COMMA: - onRewind(); - showSkipAnimation(false); - return true; - case KeyEvent.KEYCODE_K: //Fallthrough - case KeyEvent.KEYCODE_D: - case KeyEvent.KEYCODE_PERIOD: - onFastForward(); - showSkipAnimation(true); - return true; - case KeyEvent.KEYCODE_F: //Fallthrough - case KeyEvent.KEYCODE_ESCAPE: - //Exit fullscreen mode - onBackPressed(); - return true; - case KeyEvent.KEYCODE_I: - compatEnterPictureInPicture(); - return true; - case KeyEvent.KEYCODE_PLUS: //Fallthrough - case KeyEvent.KEYCODE_W: - audioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC, - AudioManager.ADJUST_RAISE, AudioManager.FLAG_SHOW_UI); - return true; - case KeyEvent.KEYCODE_MINUS: //Fallthrough - case KeyEvent.KEYCODE_S: - audioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC, - AudioManager.ADJUST_LOWER, AudioManager.FLAG_SHOW_UI); - return true; - case KeyEvent.KEYCODE_M: - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - audioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC, - AudioManager.ADJUST_TOGGLE_MUTE, AudioManager.FLAG_SHOW_UI); - return true; - } - break; - } - - //Go to x% of video: - if (keyCode >= KeyEvent.KEYCODE_0 && keyCode <= KeyEvent.KEYCODE_9) { - controller.seekTo((int) (0.1f * (keyCode - KeyEvent.KEYCODE_0) * controller.getDuration())); - return true; - } - return super.onKeyUp(keyCode, event); - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/ChaptersListAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/ChaptersListAdapter.java deleted file mode 100644 index 8f9a77f76..000000000 --- a/app/src/main/java/de/danoeh/antennapod/adapter/ChaptersListAdapter.java +++ /dev/null @@ -1,177 +0,0 @@ -package de.danoeh.antennapod.adapter; - -import android.content.Context; -import android.text.TextUtils; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.TextView; -import androidx.annotation.NonNull; -import androidx.core.content.ContextCompat; -import androidx.recyclerview.widget.RecyclerView; -import com.bumptech.glide.Glide; -import com.bumptech.glide.load.resource.bitmap.FitCenter; -import com.bumptech.glide.load.resource.bitmap.RoundedCorners; -import com.bumptech.glide.request.RequestOptions; -import com.google.android.material.elevation.SurfaceColors; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.model.feed.Chapter; -import de.danoeh.antennapod.ui.common.Converter; -import de.danoeh.antennapod.model.feed.EmbeddedChapterImage; -import de.danoeh.antennapod.core.util.IntentUtils; -import de.danoeh.antennapod.model.playback.Playable; -import de.danoeh.antennapod.ui.common.CircularProgressBar; - -public class ChaptersListAdapter extends RecyclerView.Adapter { - private Playable media; - private final Callback callback; - private final Context context; - private int currentChapterIndex = -1; - private long currentChapterPosition = -1; - private boolean hasImages = false; - - public ChaptersListAdapter(Context context, Callback callback) { - this.callback = callback; - this.context = context; - } - - public void setMedia(Playable media) { - this.media = media; - hasImages = false; - if (media.getChapters() != null) { - for (Chapter chapter : media.getChapters()) { - if (!TextUtils.isEmpty(chapter.getImageUrl())) { - hasImages = true; - } - } - } - notifyDataSetChanged(); - } - - @Override - public void onBindViewHolder(@NonNull ChapterHolder holder, int position) { - Chapter sc = getItem(position); - if (sc == null) { - holder.title.setText("Error"); - return; - } - holder.title.setText(sc.getTitle()); - holder.start.setText(Converter.getDurationStringLong((int) sc - .getStart())); - - long duration; - if (position + 1 < media.getChapters().size()) { - duration = media.getChapters().get(position + 1).getStart() - sc.getStart(); - } else { - duration = media.getDuration() - sc.getStart(); - } - holder.duration.setText(context.getString(R.string.chapter_duration, - Converter.getDurationStringLocalized(context, (int) duration))); - - if (TextUtils.isEmpty(sc.getLink())) { - holder.link.setVisibility(View.GONE); - } else { - holder.link.setVisibility(View.VISIBLE); - holder.link.setText(sc.getLink()); - holder.link.setOnClickListener(v -> IntentUtils.openInBrowser(context, sc.getLink())); - } - holder.secondaryActionIcon.setImageResource(R.drawable.ic_play_48dp); - holder.secondaryActionButton.setContentDescription(context.getString(R.string.play_chapter)); - holder.secondaryActionButton.setOnClickListener(v -> { - if (callback != null) { - callback.onPlayChapterButtonClicked(position); - } - }); - - if (position == currentChapterIndex) { - float density = context.getResources().getDisplayMetrics().density; - holder.itemView.setBackgroundColor(SurfaceColors.getColorForElevation(context, 32 * density)); - float progress = ((float) (currentChapterPosition - sc.getStart())) / duration; - progress = Math.max(progress, CircularProgressBar.MINIMUM_PERCENTAGE); - progress = Math.min(progress, CircularProgressBar.MAXIMUM_PERCENTAGE); - holder.progressBar.setPercentage(progress, position); - holder.secondaryActionIcon.setImageResource(R.drawable.ic_replay); - } else { - holder.itemView.setBackgroundColor(ContextCompat.getColor(context, android.R.color.transparent)); - holder.progressBar.setPercentage(0, null); - } - - if (hasImages) { - holder.image.setVisibility(View.VISIBLE); - if (TextUtils.isEmpty(sc.getImageUrl())) { - Glide.with(context).clear(holder.image); - } else { - Glide.with(context) - .load(EmbeddedChapterImage.getModelFor(media, position)) - .apply(new RequestOptions() - .dontAnimate() - .transform(new FitCenter(), new RoundedCorners((int) - (4 * context.getResources().getDisplayMetrics().density)))) - .into(holder.image); - } - } else { - holder.image.setVisibility(View.GONE); - } - } - - @NonNull - @Override - public ChapterHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - LayoutInflater inflater = LayoutInflater.from(context); - return new ChapterHolder(inflater.inflate(R.layout.simplechapter_item, parent, false)); - } - - @Override - public int getItemCount() { - if (media == null || media.getChapters() == null) { - return 0; - } - return media.getChapters().size(); - } - - static class ChapterHolder extends RecyclerView.ViewHolder { - final TextView title; - final TextView start; - final TextView link; - final TextView duration; - final ImageView image; - final View secondaryActionButton; - final ImageView secondaryActionIcon; - final CircularProgressBar progressBar; - - public ChapterHolder(@NonNull View itemView) { - super(itemView); - title = itemView.findViewById(R.id.txtvTitle); - start = itemView.findViewById(R.id.txtvStart); - link = itemView.findViewById(R.id.txtvLink); - image = itemView.findViewById(R.id.imgvCover); - duration = itemView.findViewById(R.id.txtvDuration); - secondaryActionButton = itemView.findViewById(R.id.secondaryActionButton); - secondaryActionIcon = itemView.findViewById(R.id.secondaryActionIcon); - progressBar = itemView.findViewById(R.id.secondaryActionProgress); - } - } - - public void notifyChapterChanged(int newChapterIndex) { - currentChapterIndex = newChapterIndex; - currentChapterPosition = getItem(newChapterIndex).getStart(); - notifyDataSetChanged(); - } - - public void notifyTimeChanged(long timeMs) { - currentChapterPosition = timeMs; - // Passing an argument prevents flickering. - // See EpisodeItemListAdapter.notifyItemChangedCompat. - notifyItemChanged(currentChapterIndex, "foo"); - } - - public Chapter getItem(int position) { - return media.getChapters().get(position); - } - - public interface Callback { - void onPlayChapterButtonClicked(int position); - } - -} diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/CoverLoader.java b/app/src/main/java/de/danoeh/antennapod/adapter/CoverLoader.java deleted file mode 100644 index c87228cdd..000000000 --- a/app/src/main/java/de/danoeh/antennapod/adapter/CoverLoader.java +++ /dev/null @@ -1,132 +0,0 @@ -package de.danoeh.antennapod.adapter; - -import android.graphics.drawable.Drawable; -import android.view.View; -import android.widget.ImageView; -import android.widget.TextView; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import com.bumptech.glide.Glide; -import com.bumptech.glide.RequestBuilder; -import com.bumptech.glide.request.RequestOptions; -import com.bumptech.glide.request.target.CustomViewTarget; -import com.bumptech.glide.request.transition.Transition; - -import java.lang.ref.WeakReference; - -public class CoverLoader { - private int resource = 0; - private String uri; - private String fallbackUri; - private ImageView imgvCover; - private boolean textAndImageCombined; - private TextView fallbackTitle; - - public CoverLoader() { - } - - public CoverLoader withUri(String uri) { - this.uri = uri; - return this; - } - - public CoverLoader withResource(int resource) { - this.resource = resource; - return this; - } - - public CoverLoader withFallbackUri(String uri) { - fallbackUri = uri; - return this; - } - - public CoverLoader withCoverView(ImageView coverView) { - imgvCover = coverView; - return this; - } - - public CoverLoader withPlaceholderView(TextView title) { - this.fallbackTitle = title; - return this; - } - - /** - * Set cover text and if it should be shown even if there is a cover image. - * @param fallbackTitle Fallback title text - * @param textAndImageCombined Show cover text even if there is a cover image? - */ - @NonNull - public CoverLoader withPlaceholderView(TextView fallbackTitle, boolean textAndImageCombined) { - this.fallbackTitle = fallbackTitle; - this.textAndImageCombined = textAndImageCombined; - return this; - } - - public void load() { - CoverTarget coverTarget = new CoverTarget(fallbackTitle, imgvCover, textAndImageCombined); - - if (resource != 0) { - Glide.with(imgvCover).clear(coverTarget); - imgvCover.setImageResource(resource); - CoverTarget.setTitleVisibility(fallbackTitle, textAndImageCombined); - return; - } - - RequestOptions options = new RequestOptions() - .fitCenter() - .dontAnimate(); - - RequestBuilder builder = Glide.with(imgvCover) - .as(Drawable.class) - .load(uri) - .apply(options); - - if (fallbackUri != null) { - builder = builder.error(Glide.with(imgvCover) - .as(Drawable.class) - .load(fallbackUri) - .apply(options)); - } - - builder.into(coverTarget); - } - - static class CoverTarget extends CustomViewTarget { - private final WeakReference fallbackTitle; - private final WeakReference cover; - private final boolean textAndImageCombined; - - public CoverTarget(TextView fallbackTitle, ImageView coverImage, boolean textAndImageCombined) { - super(coverImage); - this.fallbackTitle = new WeakReference<>(fallbackTitle); - this.cover = new WeakReference<>(coverImage); - this.textAndImageCombined = textAndImageCombined; - } - - @Override - public void onLoadFailed(Drawable errorDrawable) { - setTitleVisibility(fallbackTitle.get(), true); - } - - @Override - public void onResourceReady(@NonNull Drawable resource, - @Nullable Transition transition) { - ImageView ivCover = cover.get(); - ivCover.setImageDrawable(resource); - setTitleVisibility(fallbackTitle.get(), textAndImageCombined); - } - - @Override - protected void onResourceCleared(@Nullable Drawable placeholder) { - ImageView ivCover = cover.get(); - ivCover.setImageDrawable(placeholder); - setTitleVisibility(fallbackTitle.get(), textAndImageCombined); - } - - static void setTitleVisibility(TextView fallbackTitle, boolean textAndImageCombined) { - if (fallbackTitle != null) { - fallbackTitle.setVisibility(textAndImageCombined ? View.VISIBLE : View.GONE); - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/DownloadLogAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/DownloadLogAdapter.java deleted file mode 100644 index 84c9709bb..000000000 --- a/app/src/main/java/de/danoeh/antennapod/adapter/DownloadLogAdapter.java +++ /dev/null @@ -1,155 +0,0 @@ -package de.danoeh.antennapod.adapter; - -import android.app.Activity; -import android.text.format.DateUtils; -import android.util.Log; -import android.view.View; -import android.view.ViewGroup; -import android.widget.BaseAdapter; -import android.widget.Toast; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.activity.MainActivity; -import de.danoeh.antennapod.adapter.actionbutton.DownloadActionButton; -import de.danoeh.antennapod.net.download.serviceinterface.FeedUpdateManager; -import de.danoeh.antennapod.storage.database.DBReader; -import de.danoeh.antennapod.core.util.DownloadErrorLabel; -import de.danoeh.antennapod.model.download.DownloadError; -import de.danoeh.antennapod.model.download.DownloadResult; -import de.danoeh.antennapod.model.feed.Feed; -import de.danoeh.antennapod.model.feed.FeedMedia; -import de.danoeh.antennapod.view.viewholder.DownloadLogItemViewHolder; - -import java.util.ArrayList; -import java.util.List; - -/** - * Displays a list of DownloadStatus entries. - */ -public class DownloadLogAdapter extends BaseAdapter { - private static final String TAG = "DownloadLogAdapter"; - - private final Activity context; - private List downloadLog = new ArrayList<>(); - - public DownloadLogAdapter(Activity context) { - super(); - this.context = context; - } - - public void setDownloadLog(List downloadLog) { - this.downloadLog = downloadLog; - notifyDataSetChanged(); - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - DownloadLogItemViewHolder holder; - if (convertView == null) { - holder = new DownloadLogItemViewHolder(context, parent); - holder.itemView.setTag(holder); - } else { - holder = (DownloadLogItemViewHolder) convertView.getTag(); - } - bind(holder, getItem(position), position); - return holder.itemView; - } - - private void bind(DownloadLogItemViewHolder holder, DownloadResult status, int position) { - String statusText = ""; - if (status.getFeedfileType() == Feed.FEEDFILETYPE_FEED) { - statusText += context.getString(R.string.download_type_feed); - } else if (status.getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA) { - statusText += context.getString(R.string.download_type_media); - } - statusText += " · "; - statusText += DateUtils.getRelativeTimeSpanString(status.getCompletionDate().getTime(), - System.currentTimeMillis(), DateUtils.MINUTE_IN_MILLIS, 0); - holder.status.setText(statusText); - - if (status.getTitle() != null) { - holder.title.setText(status.getTitle()); - } else { - holder.title.setText(R.string.download_log_title_unknown); - } - - if (status.isSuccessful()) { - holder.icon.setImageResource(R.drawable.ic_check); - holder.icon.setContentDescription(context.getString(R.string.download_successful)); - holder.secondaryActionButton.setVisibility(View.INVISIBLE); - holder.reason.setVisibility(View.GONE); - holder.tapForDetails.setVisibility(View.GONE); - } else { - if (status.getReason() == DownloadError.ERROR_PARSER_EXCEPTION_DUPLICATE) { - holder.icon.setImageResource(R.drawable.ic_info); - } else { - holder.icon.setImageResource(R.drawable.ic_error); - } - holder.icon.setContentDescription(context.getString(R.string.error_label)); - holder.reason.setText(DownloadErrorLabel.from(status.getReason())); - holder.reason.setVisibility(View.VISIBLE); - holder.tapForDetails.setVisibility(View.VISIBLE); - - if (newerWasSuccessful(position, status.getFeedfileType(), status.getFeedfileId())) { - holder.secondaryActionButton.setVisibility(View.INVISIBLE); - holder.secondaryActionButton.setOnClickListener(null); - holder.secondaryActionButton.setTag(null); - } else { - holder.secondaryActionIcon.setImageResource(R.drawable.ic_refresh); - holder.secondaryActionButton.setVisibility(View.VISIBLE); - - if (status.getFeedfileType() == Feed.FEEDFILETYPE_FEED) { - holder.secondaryActionButton.setOnClickListener(v -> { - holder.secondaryActionButton.setVisibility(View.INVISIBLE); - Feed feed = DBReader.getFeed(status.getFeedfileId()); - if (feed == null) { - Log.e(TAG, "Could not find feed for feed id: " + status.getFeedfileId()); - return; - } - FeedUpdateManager.getInstance().runOnce(context, feed); - }); - } else if (status.getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA) { - holder.secondaryActionButton.setOnClickListener(v -> { - holder.secondaryActionButton.setVisibility(View.INVISIBLE); - FeedMedia media = DBReader.getFeedMedia(status.getFeedfileId()); - if (media == null) { - Log.e(TAG, "Could not find feed media for feed id: " + status.getFeedfileId()); - return; - } - new DownloadActionButton(media.getItem()).onClick(context); - ((MainActivity) context).showSnackbarAbovePlayer( - R.string.status_downloading_label, Toast.LENGTH_SHORT); - }); - } - } - } - } - - private boolean newerWasSuccessful(int downloadStatusIndex, int feedTypeId, long id) { - for (int i = 0; i < downloadStatusIndex; i++) { - DownloadResult status = downloadLog.get(i); - if (status.getFeedfileType() == feedTypeId && status.getFeedfileId() == id && status.isSuccessful()) { - return true; - } - } - return false; - } - - @Override - public int getCount() { - return downloadLog.size(); - } - - @Override - public DownloadResult getItem(int position) { - if (position < downloadLog.size()) { - return downloadLog.get(position); - } - return null; - } - - @Override - public long getItemId(int position) { - return position; - } - -} diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/EpisodeItemListAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/EpisodeItemListAdapter.java deleted file mode 100644 index 0ec7018a7..000000000 --- a/app/src/main/java/de/danoeh/antennapod/adapter/EpisodeItemListAdapter.java +++ /dev/null @@ -1,234 +0,0 @@ -package de.danoeh.antennapod.adapter; - -import android.app.Activity; -import android.os.Build; -import android.view.ContextMenu; -import android.view.InputDevice; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.MotionEvent; -import android.view.View; -import android.view.ViewGroup; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.recyclerview.widget.RecyclerView; - -import de.danoeh.antennapod.ui.common.ThemeUtils; -import org.apache.commons.lang3.ArrayUtils; - -import java.lang.ref.WeakReference; -import java.util.ArrayList; -import java.util.List; - -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.activity.MainActivity; -import de.danoeh.antennapod.model.feed.FeedItem; -import de.danoeh.antennapod.core.util.FeedItemUtil; -import de.danoeh.antennapod.fragment.ItemPagerFragment; -import de.danoeh.antennapod.menuhandler.FeedItemMenuHandler; -import de.danoeh.antennapod.view.viewholder.EpisodeItemViewHolder; - -/** - * List adapter for the list of new episodes. - */ -public class EpisodeItemListAdapter extends SelectableAdapter - implements View.OnCreateContextMenuListener { - - private final WeakReference mainActivityRef; - private List episodes = new ArrayList<>(); - private FeedItem longPressedItem; - int longPressedPosition = 0; // used to init actionMode - private int dummyViews = 0; - - public EpisodeItemListAdapter(MainActivity mainActivity) { - super(mainActivity); - this.mainActivityRef = new WeakReference<>(mainActivity); - setHasStableIds(true); - } - - public void setDummyViews(int dummyViews) { - this.dummyViews = dummyViews; - notifyDataSetChanged(); - } - - public void updateItems(List items) { - episodes = items; - notifyDataSetChanged(); - updateTitle(); - } - - @Override - public final int getItemViewType(int position) { - return R.id.view_type_episode_item; - } - - @NonNull - @Override - public final EpisodeItemViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - return new EpisodeItemViewHolder(mainActivityRef.get(), parent); - } - - @Override - public final void onBindViewHolder(EpisodeItemViewHolder holder, int pos) { - if (pos >= episodes.size()) { - beforeBindViewHolder(holder, pos); - holder.bindDummy(); - afterBindViewHolder(holder, pos); - holder.hideSeparatorIfNecessary(); - return; - } - - // Reset state of recycled views - holder.coverHolder.setVisibility(View.VISIBLE); - holder.dragHandle.setVisibility(View.GONE); - - beforeBindViewHolder(holder, pos); - - FeedItem item = episodes.get(pos); - holder.bind(item); - - holder.itemView.setOnClickListener(v -> { - MainActivity activity = mainActivityRef.get(); - if (activity != null && !inActionMode()) { - long[] ids = FeedItemUtil.getIds(episodes); - int position = ArrayUtils.indexOf(ids, item.getId()); - activity.loadChildFragment(ItemPagerFragment.newInstance(ids, position)); - } else { - toggleSelection(holder.getBindingAdapterPosition()); - } - }); - holder.itemView.setOnCreateContextMenuListener(this); - holder.itemView.setOnLongClickListener(v -> { - longPressedItem = item; - longPressedPosition = holder.getBindingAdapterPosition(); - return false; - }); - holder.itemView.setOnTouchListener((v, e) -> { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - if (e.isFromSource(InputDevice.SOURCE_MOUSE) - && e.getButtonState() == MotionEvent.BUTTON_SECONDARY) { - longPressedItem = item; - longPressedPosition = holder.getBindingAdapterPosition(); - return false; - } - } - return false; - }); - - if (inActionMode()) { - holder.secondaryActionButton.setOnClickListener(null); - if (isSelected(pos)) { - holder.itemView.setBackgroundColor(0x88000000 - + (0xffffff & ThemeUtils.getColorFromAttr(mainActivityRef.get(), R.attr.colorAccent))); - } else { - holder.itemView.setBackgroundResource(android.R.color.transparent); - } - } - - afterBindViewHolder(holder, pos); - holder.hideSeparatorIfNecessary(); - } - - protected void beforeBindViewHolder(EpisodeItemViewHolder holder, int pos) { - } - - protected void afterBindViewHolder(EpisodeItemViewHolder holder, int pos) { - } - - @Override - public void onViewRecycled(@NonNull EpisodeItemViewHolder holder) { - super.onViewRecycled(holder); - // Set all listeners to null. This is required to prevent leaking fragments that have set a listener. - // Activity -> recycledViewPool -> EpisodeItemViewHolder -> Listener -> Fragment (can not be garbage collected) - holder.itemView.setOnClickListener(null); - holder.itemView.setOnCreateContextMenuListener(null); - holder.itemView.setOnLongClickListener(null); - holder.itemView.setOnTouchListener(null); - holder.secondaryActionButton.setOnClickListener(null); - holder.dragHandle.setOnTouchListener(null); - holder.coverHolder.setOnTouchListener(null); - } - - /** - * {@link #notifyItemChanged(int)} is final, so we can not override. - * Calling {@link #notifyItemChanged(int)} may bind the item to a new ViewHolder and execute a transition. - * This causes flickering and breaks the download animation that stores the old progress in the View. - * Instead, we tell the adapter to use partial binding by calling {@link #notifyItemChanged(int, Object)}. - * We actually ignore the payload and always do a full bind but calling the partial bind method ensures - * that ViewHolders are always re-used. - * - * @param position Position of the item that has changed - */ - public void notifyItemChangedCompat(int position) { - notifyItemChanged(position, "foo"); - } - - @Nullable - public FeedItem getLongPressedItem() { - return longPressedItem; - } - - @Override - public long getItemId(int position) { - if (position >= episodes.size()) { - return RecyclerView.NO_ID; // Dummy views - } - FeedItem item = episodes.get(position); - return item != null ? item.getId() : RecyclerView.NO_POSITION; - } - - @Override - public int getItemCount() { - return dummyViews + episodes.size(); - } - - protected FeedItem getItem(int index) { - return episodes.get(index); - } - - protected Activity getActivity() { - return mainActivityRef.get(); - } - - @Override - public void onCreateContextMenu(final ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { - MenuInflater inflater = mainActivityRef.get().getMenuInflater(); - if (inActionMode()) { - inflater.inflate(R.menu.multi_select_context_popup, menu); - } else { - if (longPressedItem == null) { - return; - } - inflater.inflate(R.menu.feeditemlist_context, menu); - menu.setHeaderTitle(longPressedItem.getTitle()); - FeedItemMenuHandler.onPrepareMenu(menu, longPressedItem, R.id.skip_episode_item); - } - } - - public boolean onContextItemSelected(MenuItem item) { - if (item.getItemId() == R.id.multi_select) { - startSelectMode(longPressedPosition); - return true; - } else if (item.getItemId() == R.id.select_all_above) { - setSelected(0, longPressedPosition, true); - return true; - } else if (item.getItemId() == R.id.select_all_below) { - shouldSelectLazyLoadedItems = true; - setSelected(longPressedPosition + 1, getItemCount(), true); - return true; - } - return false; - } - - public List getSelectedItems() { - List items = new ArrayList<>(); - for (int i = 0; i < getItemCount(); i++) { - if (isSelected(i)) { - items.add(getItem(i)); - } - } - return items; - } - -} diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/FeedItemlistDescriptionAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/FeedItemlistDescriptionAdapter.java deleted file mode 100644 index b55f2a6bc..000000000 --- a/app/src/main/java/de/danoeh/antennapod/adapter/FeedItemlistDescriptionAdapter.java +++ /dev/null @@ -1,112 +0,0 @@ -package de.danoeh.antennapod.adapter; - -import android.content.Context; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ArrayAdapter; -import android.widget.Button; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.model.playback.MediaType; -import de.danoeh.antennapod.net.common.NetworkUtils; -import de.danoeh.antennapod.model.playback.RemoteMedia; -import de.danoeh.antennapod.model.feed.FeedItem; -import de.danoeh.antennapod.playback.service.PlaybackService; -import de.danoeh.antennapod.playback.service.PlaybackServiceStarter; -import de.danoeh.antennapod.ui.common.DateFormatter; -import de.danoeh.antennapod.model.playback.Playable; -import de.danoeh.antennapod.core.util.syndication.HtmlToPlainText; -import de.danoeh.antennapod.dialog.StreamingConfirmationDialog; - -import java.util.List; - -/** - * List adapter for showing a list of FeedItems with their title and description. - */ -public class FeedItemlistDescriptionAdapter extends ArrayAdapter { - private static final int MAX_LINES_COLLAPSED = 2; - - public FeedItemlistDescriptionAdapter(Context context, int resource, List objects) { - super(context, resource, objects); - } - - @NonNull - @Override - public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { - Holder holder; - - FeedItem item = getItem(position); - - // Inflate layout - if (convertView == null) { - holder = new Holder(); - LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE); - convertView = inflater.inflate(R.layout.itemdescription_listitem, parent, false); - holder.title = convertView.findViewById(R.id.txtvTitle); - holder.pubDate = convertView.findViewById(R.id.txtvPubDate); - holder.description = convertView.findViewById(R.id.txtvDescription); - holder.preview = convertView.findViewById(R.id.butPreview); - - convertView.setTag(holder); - } else { - holder = (Holder) convertView.getTag(); - } - - holder.title.setText(item.getTitle()); - holder.pubDate.setText(DateFormatter.formatAbbrev(getContext(), item.getPubDate())); - if (item.getDescription() != null) { - String description = HtmlToPlainText.getPlainText(item.getDescription()) - .replaceAll("\n", " ") - .replaceAll("\\s+", " ") - .trim(); - holder.description.setText(description); - holder.description.setMaxLines(MAX_LINES_COLLAPSED); - } - holder.description.setTag(Boolean.FALSE); // not expanded - holder.preview.setVisibility(View.GONE); - holder.preview.setOnClickListener(v -> { - if (item.getMedia() == null) { - return; - } - Playable playable = new RemoteMedia(item); - if (!NetworkUtils.isStreamingAllowed()) { - new StreamingConfirmationDialog(getContext(), playable).show(); - return; - } - - new PlaybackServiceStarter(getContext(), playable) - .callEvenIfRunning(true) - .start(); - - if (playable.getMediaType() == MediaType.VIDEO) { - getContext().startActivity(PlaybackService.getPlayerActivityIntent(getContext(), playable)); - } - }); - convertView.setOnClickListener(v -> { - if (holder.description.getTag() == Boolean.TRUE) { - holder.description.setMaxLines(MAX_LINES_COLLAPSED); - holder.preview.setVisibility(View.GONE); - holder.description.setTag(Boolean.FALSE); - } else { - holder.description.setMaxLines(30); - holder.description.setTag(Boolean.TRUE); - - holder.preview.setVisibility(item.getMedia() != null ? View.VISIBLE : View.GONE); - holder.preview.setText(R.string.preview_episode); - } - }); - return convertView; - } - - static class Holder { - TextView title; - TextView pubDate; - TextView description; - Button preview; - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/HorizontalFeedListAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/HorizontalFeedListAdapter.java deleted file mode 100644 index 304bf9f64..000000000 --- a/app/src/main/java/de/danoeh/antennapod/adapter/HorizontalFeedListAdapter.java +++ /dev/null @@ -1,143 +0,0 @@ -package de.danoeh.antennapod.adapter; - -import android.view.View; -import android.view.ViewGroup; -import android.widget.Button; -import androidx.annotation.NonNull; -import androidx.annotation.StringRes; -import androidx.cardview.widget.CardView; -import androidx.recyclerview.widget.RecyclerView; -import com.bumptech.glide.Glide; -import com.bumptech.glide.request.RequestOptions; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.activity.MainActivity; -import de.danoeh.antennapod.model.feed.Feed; -import de.danoeh.antennapod.fragment.FeedItemlistFragment; -import de.danoeh.antennapod.ui.common.SquareImageView; - -import java.lang.ref.WeakReference; -import java.util.ArrayList; -import java.util.List; - -import android.view.ContextMenu; -import android.view.MenuInflater; -import androidx.annotation.Nullable; - -public class HorizontalFeedListAdapter extends RecyclerView.Adapter - implements View.OnCreateContextMenuListener { - private final WeakReference mainActivityRef; - private final List data = new ArrayList<>(); - private int dummyViews = 0; - private Feed longPressedItem; - private @StringRes int endButtonText = 0; - private Runnable endButtonAction = null; - - public HorizontalFeedListAdapter(MainActivity mainActivity) { - this.mainActivityRef = new WeakReference<>(mainActivity); - } - - public void setDummyViews(int dummyViews) { - this.dummyViews = dummyViews; - } - - public void updateData(List newData) { - data.clear(); - data.addAll(newData); - notifyDataSetChanged(); - } - - @NonNull - @Override - public Holder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - View convertView = View.inflate(mainActivityRef.get(), R.layout.horizontal_feed_item, null); - return new Holder(convertView); - } - - @Override - public void onBindViewHolder(@NonNull Holder holder, int position) { - if (position == getItemCount() - 1 && endButtonAction != null) { - holder.cardView.setVisibility(View.GONE); - holder.actionButton.setVisibility(View.VISIBLE); - holder.actionButton.setText(endButtonText); - holder.actionButton.setOnClickListener(v -> endButtonAction.run()); - return; - } - holder.cardView.setVisibility(View.VISIBLE); - holder.actionButton.setVisibility(View.GONE); - if (position >= data.size()) { - holder.itemView.setAlpha(0.1f); - Glide.with(mainActivityRef.get()).clear(holder.imageView); - holder.imageView.setImageResource(R.color.medium_gray); - return; - } - - holder.itemView.setAlpha(1.0f); - final Feed podcast = data.get(position); - holder.imageView.setContentDescription(podcast.getTitle()); - holder.imageView.setOnClickListener(v -> - mainActivityRef.get().loadChildFragment(FeedItemlistFragment.newInstance(podcast.getId()))); - - holder.imageView.setOnCreateContextMenuListener(this); - holder.imageView.setOnLongClickListener(v -> { - int currentItemPosition = holder.getBindingAdapterPosition(); - longPressedItem = data.get(currentItemPosition); - return false; - }); - - Glide.with(mainActivityRef.get()) - .load(podcast.getImageUrl()) - .apply(new RequestOptions() - .placeholder(R.color.light_gray) - .fitCenter() - .dontAnimate()) - .into(holder.imageView); - } - - @Nullable - public Feed getLongPressedItem() { - return longPressedItem; - } - - @Override - public long getItemId(int position) { - if (position >= data.size()) { - return RecyclerView.NO_ID; // Dummy views - } - return data.get(position).getId(); - } - - @Override - public int getItemCount() { - return dummyViews + data.size() + ((endButtonAction == null) ? 0 : 1); - } - - @Override - public void onCreateContextMenu(ContextMenu contextMenu, View view, ContextMenu.ContextMenuInfo contextMenuInfo) { - MenuInflater inflater = mainActivityRef.get().getMenuInflater(); - if (longPressedItem == null) { - return; - } - inflater.inflate(R.menu.nav_feed_context, contextMenu); - contextMenu.setHeaderTitle(longPressedItem.getTitle()); - } - - public void setEndButton(@StringRes int text, Runnable action) { - endButtonAction = action; - endButtonText = text; - notifyDataSetChanged(); - } - - static class Holder extends RecyclerView.ViewHolder { - SquareImageView imageView; - CardView cardView; - Button actionButton; - - public Holder(@NonNull View itemView) { - super(itemView); - imageView = itemView.findViewById(R.id.discovery_cover); - imageView.setDirection(SquareImageView.DIRECTION_HEIGHT); - actionButton = itemView.findViewById(R.id.actionButton); - cardView = itemView.findViewById(R.id.cardView); - } - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/HorizontalItemListAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/HorizontalItemListAdapter.java deleted file mode 100644 index 4e8a2b05e..000000000 --- a/app/src/main/java/de/danoeh/antennapod/adapter/HorizontalItemListAdapter.java +++ /dev/null @@ -1,138 +0,0 @@ -package de.danoeh.antennapod.adapter; - -import android.view.ContextMenu; -import android.view.MenuInflater; -import android.view.View; -import android.view.ViewGroup; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.recyclerview.widget.RecyclerView; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.activity.MainActivity; -import de.danoeh.antennapod.core.util.FeedItemUtil; -import de.danoeh.antennapod.fragment.ItemPagerFragment; -import de.danoeh.antennapod.menuhandler.FeedItemMenuHandler; -import de.danoeh.antennapod.model.feed.FeedItem; -import de.danoeh.antennapod.view.viewholder.HorizontalItemViewHolder; -import org.apache.commons.lang3.ArrayUtils; - -import java.lang.ref.WeakReference; -import java.util.ArrayList; -import java.util.List; - -public class HorizontalItemListAdapter extends RecyclerView.Adapter - implements View.OnCreateContextMenuListener { - private final WeakReference mainActivityRef; - private List data = new ArrayList<>(); - private FeedItem longPressedItem; - private int dummyViews = 0; - - public HorizontalItemListAdapter(MainActivity mainActivity) { - this.mainActivityRef = new WeakReference<>(mainActivity); - setHasStableIds(true); - } - - public void setDummyViews(int dummyViews) { - this.dummyViews = dummyViews; - } - - public void updateData(List newData) { - data = newData; - notifyDataSetChanged(); - } - - @NonNull - @Override - public HorizontalItemViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - return new HorizontalItemViewHolder(mainActivityRef.get(), parent); - } - - @Override - public void onBindViewHolder(@NonNull HorizontalItemViewHolder holder, int position) { - if (position >= data.size()) { - holder.bindDummy(); - return; - } - - final FeedItem item = data.get(position); - holder.bind(item); - - holder.card.setOnCreateContextMenuListener(this); - holder.card.setOnLongClickListener(v -> { - longPressedItem = item; - return false; - }); - holder.secondaryActionIcon.setOnCreateContextMenuListener(this); - holder.secondaryActionIcon.setOnLongClickListener(v -> { - longPressedItem = item; - return false; - }); - holder.card.setOnClickListener(v -> { - MainActivity activity = mainActivityRef.get(); - if (activity != null) { - long[] ids = FeedItemUtil.getIds(data); - int clickPosition = ArrayUtils.indexOf(ids, item.getId()); - activity.loadChildFragment(ItemPagerFragment.newInstance(ids, clickPosition)); - } - }); - } - - @Override - public long getItemId(int position) { - if (position >= data.size()) { - return RecyclerView.NO_ID; // Dummy views - } - return data.get(position).getId(); - } - - @Override - public int getItemCount() { - return dummyViews + data.size(); - } - - @Override - public void onViewRecycled(@NonNull HorizontalItemViewHolder holder) { - super.onViewRecycled(holder); - // Set all listeners to null. This is required to prevent leaking fragments that have set a listener. - // Activity -> recycledViewPool -> ViewHolder -> Listener -> Fragment (can not be garbage collected) - holder.card.setOnClickListener(null); - holder.card.setOnCreateContextMenuListener(null); - holder.card.setOnLongClickListener(null); - holder.secondaryActionIcon.setOnClickListener(null); - holder.secondaryActionIcon.setOnCreateContextMenuListener(null); - holder.secondaryActionIcon.setOnLongClickListener(null); - } - - /** - * {@link #notifyItemChanged(int)} is final, so we can not override. - * Calling {@link #notifyItemChanged(int)} may bind the item to a new ViewHolder and execute a transition. - * This causes flickering and breaks the download animation that stores the old progress in the View. - * Instead, we tell the adapter to use partial binding by calling {@link #notifyItemChanged(int, Object)}. - * We actually ignore the payload and always do a full bind but calling the partial bind method ensures - * that ViewHolders are always re-used. - * - * @param position Position of the item that has changed - */ - public void notifyItemChangedCompat(int position) { - notifyItemChanged(position, "foo"); - } - - @Nullable - public FeedItem getLongPressedItem() { - return longPressedItem; - } - - @Override - public void onCreateContextMenu(final ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { - MenuInflater inflater = mainActivityRef.get().getMenuInflater(); - if (longPressedItem == null) { - return; - } - menu.clear(); - inflater.inflate(R.menu.feeditemlist_context, menu); - menu.setHeaderTitle(longPressedItem.getTitle()); - FeedItemMenuHandler.onPrepareMenu(menu, longPressedItem, R.id.skip_episode_item); - } - - -} diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/NavListAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/NavListAdapter.java deleted file mode 100644 index 4c8e074a1..000000000 --- a/app/src/main/java/de/danoeh/antennapod/adapter/NavListAdapter.java +++ /dev/null @@ -1,420 +0,0 @@ -package de.danoeh.antennapod.adapter; - -import android.app.Activity; -import android.content.Intent; -import android.content.SharedPreferences; -import android.os.Build; -import android.view.ContextMenu; -import android.view.InputDevice; -import android.view.LayoutInflater; -import androidx.annotation.DrawableRes; -import androidx.annotation.NonNull; -import androidx.preference.PreferenceManager; -import android.view.MotionEvent; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.RelativeLayout; -import android.widget.TextView; -import com.bumptech.glide.load.resource.bitmap.FitCenter; -import com.bumptech.glide.load.resource.bitmap.RoundedCorners; -import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import androidx.recyclerview.widget.RecyclerView; -import com.bumptech.glide.Glide; -import com.bumptech.glide.request.RequestOptions; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.activity.PreferenceActivity; -import de.danoeh.antennapod.fragment.AllEpisodesFragment; -import de.danoeh.antennapod.fragment.CompletedDownloadsFragment; -import de.danoeh.antennapod.fragment.InboxFragment; -import de.danoeh.antennapod.model.feed.Feed; -import de.danoeh.antennapod.storage.preferences.UserPreferences; -import de.danoeh.antennapod.storage.database.NavDrawerData; -import de.danoeh.antennapod.fragment.AddFeedFragment; -import de.danoeh.antennapod.fragment.NavDrawerFragment; -import de.danoeh.antennapod.fragment.PlaybackHistoryFragment; -import de.danoeh.antennapod.fragment.QueueFragment; -import de.danoeh.antennapod.fragment.SubscriptionFragment; -import de.danoeh.antennapod.ui.home.HomeFragment; -import org.apache.commons.lang3.ArrayUtils; - -import java.lang.ref.WeakReference; -import java.text.NumberFormat; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; - -/** - * BaseAdapter for the navigation drawer - */ -public class NavListAdapter extends RecyclerView.Adapter - implements SharedPreferences.OnSharedPreferenceChangeListener { - - public static final int VIEW_TYPE_NAV = 0; - public static final int VIEW_TYPE_SECTION_DIVIDER = 1; - private static final int VIEW_TYPE_SUBSCRIPTION = 2; - - /** - * a tag used as a placeholder to indicate if the subscription list should be displayed or not - * This tag doesn't correspond to any specific activity. - */ - public static final String SUBSCRIPTION_LIST_TAG = "SubscriptionList"; - - private final List fragmentTags = new ArrayList<>(); - private final String[] titles; - private final ItemAccess itemAccess; - private final WeakReference activity; - public boolean showSubscriptionList = true; - - public NavListAdapter(ItemAccess itemAccess, Activity context) { - this.itemAccess = itemAccess; - this.activity = new WeakReference<>(context); - - titles = context.getResources().getStringArray(R.array.nav_drawer_titles); - loadItems(); - - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); - prefs.registerOnSharedPreferenceChangeListener(this); - } - - public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { - if (UserPreferences.PREF_HIDDEN_DRAWER_ITEMS.equals(key)) { - loadItems(); - } - } - - private void loadItems() { - List newTags = new ArrayList<>(Arrays.asList(NavDrawerFragment.NAV_DRAWER_TAGS)); - List hiddenFragments = UserPreferences.getHiddenDrawerItems(); - newTags.removeAll(hiddenFragments); - - if (newTags.contains(SUBSCRIPTION_LIST_TAG)) { - // we never want SUBSCRIPTION_LIST_TAG to be in 'tags' - // since it doesn't actually correspond to a position in the list, but is - // a placeholder that indicates if we should show the subscription list in the - // nav drawer at all. - showSubscriptionList = true; - newTags.remove(SUBSCRIPTION_LIST_TAG); - } else { - showSubscriptionList = false; - } - - fragmentTags.clear(); - fragmentTags.addAll(newTags); - notifyDataSetChanged(); - } - - public String getLabel(String tag) { - int index = ArrayUtils.indexOf(NavDrawerFragment.NAV_DRAWER_TAGS, tag); - return titles[index]; - } - - private @DrawableRes int getDrawable(String tag) { - switch (tag) { - case HomeFragment.TAG: - return R.drawable.ic_home; - case QueueFragment.TAG: - return R.drawable.ic_playlist_play; - case InboxFragment.TAG: - return R.drawable.ic_inbox; - case AllEpisodesFragment.TAG: - return R.drawable.ic_feed; - case CompletedDownloadsFragment.TAG: - return R.drawable.ic_download; - case PlaybackHistoryFragment.TAG: - return R.drawable.ic_history; - case SubscriptionFragment.TAG: - return R.drawable.ic_subscriptions; - case AddFeedFragment.TAG: - return R.drawable.ic_add; - default: - return 0; - } - } - - public List getFragmentTags() { - return Collections.unmodifiableList(fragmentTags); - } - - @Override - public int getItemCount() { - int baseCount = getSubscriptionOffset(); - if (showSubscriptionList) { - baseCount += itemAccess.getCount(); - } - return baseCount; - } - - @Override - public long getItemId(int position) { - int viewType = getItemViewType(position); - if (viewType == VIEW_TYPE_SUBSCRIPTION) { - return itemAccess.getItem(position - getSubscriptionOffset()).id; - } else if (viewType == VIEW_TYPE_NAV) { - return -Math.abs((long) fragmentTags.get(position).hashCode()) - 1; // Folder IDs are >0 - } else { - return 0; - } - } - - @Override - public int getItemViewType(int position) { - if (0 <= position && position < fragmentTags.size()) { - return VIEW_TYPE_NAV; - } else if (position < getSubscriptionOffset()) { - return VIEW_TYPE_SECTION_DIVIDER; - } else { - return VIEW_TYPE_SUBSCRIPTION; - } - } - - public int getSubscriptionOffset() { - return fragmentTags.size() > 0 ? fragmentTags.size() + 1 : 0; - } - - @NonNull - @Override - public Holder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - LayoutInflater inflater = LayoutInflater.from(activity.get()); - if (viewType == VIEW_TYPE_NAV) { - return new NavHolder(inflater.inflate(R.layout.nav_listitem, parent, false)); - } else if (viewType == VIEW_TYPE_SECTION_DIVIDER) { - return new DividerHolder(inflater.inflate(R.layout.nav_section_item, parent, false)); - } else { - return new FeedHolder(inflater.inflate(R.layout.nav_listitem, parent, false)); - } - } - - @Override - public void onBindViewHolder(@NonNull Holder holder, int position) { - int viewType = getItemViewType(position); - - holder.itemView.setOnCreateContextMenuListener(null); - if (viewType == VIEW_TYPE_NAV) { - bindNavView(getLabel(fragmentTags.get(position)), position, (NavHolder) holder); - } else if (viewType == VIEW_TYPE_SECTION_DIVIDER) { - bindSectionDivider((DividerHolder) holder); - } else { - int itemPos = position - getSubscriptionOffset(); - NavDrawerData.DrawerItem item = itemAccess.getItem(itemPos); - bindListItem(item, (FeedHolder) holder); - if (item.type == NavDrawerData.DrawerItem.Type.FEED) { - bindFeedView((NavDrawerData.FeedDrawerItem) item, (FeedHolder) holder); - } else { - bindTagView((NavDrawerData.TagDrawerItem) item, (FeedHolder) holder); - } - holder.itemView.setOnCreateContextMenuListener(itemAccess); - } - if (viewType != VIEW_TYPE_SECTION_DIVIDER) { - holder.itemView.setSelected(itemAccess.isSelected(position)); - holder.itemView.setOnClickListener(v -> itemAccess.onItemClick(position)); - holder.itemView.setOnLongClickListener(v -> itemAccess.onItemLongClick(position)); - holder.itemView.setOnTouchListener((v, e) -> { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - if (e.isFromSource(InputDevice.SOURCE_MOUSE) - && e.getButtonState() == MotionEvent.BUTTON_SECONDARY) { - itemAccess.onItemLongClick(position); - return false; - } - } - return false; - }); - } - } - - private void bindNavView(String title, int position, NavHolder holder) { - Activity context = activity.get(); - if (context == null) { - return; - } - holder.title.setText(title); - - // reset for re-use - holder.count.setVisibility(View.GONE); - holder.count.setOnClickListener(null); - holder.count.setClickable(false); - - String tag = fragmentTags.get(position); - if (tag.equals(QueueFragment.TAG)) { - int queueSize = itemAccess.getQueueSize(); - if (queueSize > 0) { - holder.count.setText(NumberFormat.getInstance().format(queueSize)); - holder.count.setVisibility(View.VISIBLE); - } - } else if (tag.equals(InboxFragment.TAG)) { - int unreadItems = itemAccess.getNumberOfNewItems(); - if (unreadItems > 0) { - holder.count.setText(NumberFormat.getInstance().format(unreadItems)); - holder.count.setVisibility(View.VISIBLE); - } - } else if (tag.equals(SubscriptionFragment.TAG)) { - int sum = itemAccess.getFeedCounterSum(); - if (sum > 0) { - holder.count.setText(NumberFormat.getInstance().format(sum)); - holder.count.setVisibility(View.VISIBLE); - } - } else if (tag.equals(CompletedDownloadsFragment.TAG) && UserPreferences.isEnableAutodownload()) { - int epCacheSize = UserPreferences.getEpisodeCacheSize(); - // don't count episodes that can be reclaimed - int spaceUsed = itemAccess.getNumberOfDownloadedItems() - - itemAccess.getReclaimableItems(); - if (epCacheSize > 0 && spaceUsed >= epCacheSize) { - holder.count.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_disc_alert, 0); - holder.count.setVisibility(View.VISIBLE); - holder.count.setOnClickListener(v -> - new MaterialAlertDialogBuilder(context) - .setTitle(R.string.episode_cache_full_title) - .setMessage(R.string.episode_cache_full_message) - .setPositiveButton(android.R.string.ok, null) - .setNeutralButton(R.string.open_autodownload_settings, (dialog, which) -> { - Intent intent = new Intent(context, PreferenceActivity.class); - intent.putExtra(PreferenceActivity.OPEN_AUTO_DOWNLOAD_SETTINGS, true); - context.startActivity(intent); - }) - .show() - ); - } - } - - holder.image.setImageResource(getDrawable(fragmentTags.get(position))); - } - - private void bindSectionDivider(DividerHolder holder) { - Activity context = activity.get(); - if (context == null) { - return; - } - - if (UserPreferences.getSubscriptionsFilter().isEnabled() && showSubscriptionList) { - holder.itemView.setEnabled(true); - holder.feedsFilteredMsg.setVisibility(View.VISIBLE); - } else { - holder.itemView.setEnabled(false); - holder.feedsFilteredMsg.setVisibility(View.GONE); - } - } - - private void bindListItem(NavDrawerData.DrawerItem item, FeedHolder holder) { - if (item.getCounter() > 0) { - holder.count.setVisibility(View.VISIBLE); - holder.count.setText(NumberFormat.getInstance().format(item.getCounter())); - } else { - holder.count.setVisibility(View.GONE); - } - holder.title.setText(item.getTitle()); - int padding = (int) (activity.get().getResources().getDimension(R.dimen.thumbnail_length_navlist) / 2); - holder.itemView.setPadding(item.getLayer() * padding, 0, 0, 0); - } - - private void bindFeedView(NavDrawerData.FeedDrawerItem drawerItem, FeedHolder holder) { - Feed feed = drawerItem.feed; - Activity context = activity.get(); - if (context == null) { - return; - } - - Glide.with(context) - .load(feed.getImageUrl()) - .apply(new RequestOptions() - .placeholder(R.color.light_gray) - .error(R.color.light_gray) - .transform(new FitCenter(), - new RoundedCorners((int) (4 * context.getResources().getDisplayMetrics().density))) - .dontAnimate()) - .into(holder.image); - - if (feed.hasLastUpdateFailed()) { - RelativeLayout.LayoutParams p = (RelativeLayout.LayoutParams) holder.title.getLayoutParams(); - p.addRule(RelativeLayout.LEFT_OF, R.id.itxtvFailure); - holder.failure.setVisibility(View.VISIBLE); - } else { - RelativeLayout.LayoutParams p = (RelativeLayout.LayoutParams) holder.title.getLayoutParams(); - p.addRule(RelativeLayout.LEFT_OF, R.id.txtvCount); - holder.failure.setVisibility(View.GONE); - } - } - - private void bindTagView(NavDrawerData.TagDrawerItem tag, FeedHolder holder) { - Activity context = activity.get(); - if (context == null) { - return; - } - if (tag.isOpen()) { - holder.count.setVisibility(View.GONE); - } - Glide.with(context).clear(holder.image); - holder.image.setImageResource(R.drawable.ic_tag); - holder.failure.setVisibility(View.GONE); - } - - static class Holder extends RecyclerView.ViewHolder { - public Holder(@NonNull View itemView) { - super(itemView); - } - } - - static class DividerHolder extends Holder { - final LinearLayout feedsFilteredMsg; - - public DividerHolder(@NonNull View itemView) { - super(itemView); - feedsFilteredMsg = itemView.findViewById(R.id.nav_feeds_filtered_message); - } - } - - static class NavHolder extends Holder { - final ImageView image; - final TextView title; - final TextView count; - - public NavHolder(@NonNull View itemView) { - super(itemView); - image = itemView.findViewById(R.id.imgvCover); - title = itemView.findViewById(R.id.txtvTitle); - count = itemView.findViewById(R.id.txtvCount); - } - } - - static class FeedHolder extends Holder { - final ImageView image; - final TextView title; - final ImageView failure; - final TextView count; - - public FeedHolder(@NonNull View itemView) { - super(itemView); - image = itemView.findViewById(R.id.imgvCover); - title = itemView.findViewById(R.id.txtvTitle); - failure = itemView.findViewById(R.id.itxtvFailure); - count = itemView.findViewById(R.id.txtvCount); - } - } - - public interface ItemAccess extends View.OnCreateContextMenuListener { - int getCount(); - - NavDrawerData.DrawerItem getItem(int position); - - boolean isSelected(int position); - - int getQueueSize(); - - int getNumberOfNewItems(); - - int getNumberOfDownloadedItems(); - - int getReclaimableItems(); - - int getFeedCounterSum(); - - void onItemClick(int position); - - boolean onItemLongClick(int position); - - @Override - void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo); - } - -} diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/QueueRecyclerAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/QueueRecyclerAdapter.java deleted file mode 100644 index 1f4cfd0cf..000000000 --- a/app/src/main/java/de/danoeh/antennapod/adapter/QueueRecyclerAdapter.java +++ /dev/null @@ -1,95 +0,0 @@ -package de.danoeh.antennapod.adapter; - -import android.annotation.SuppressLint; -import android.util.Log; -import android.view.ContextMenu; -import android.view.MenuInflater; -import android.view.MotionEvent; -import android.view.View; - -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.activity.MainActivity; -import de.danoeh.antennapod.storage.preferences.UserPreferences; -import de.danoeh.antennapod.fragment.swipeactions.SwipeActions; -import de.danoeh.antennapod.view.viewholder.EpisodeItemViewHolder; - -/** - * List adapter for the queue. - */ -public class QueueRecyclerAdapter extends EpisodeItemListAdapter { - private static final String TAG = "QueueRecyclerAdapter"; - - private final SwipeActions swipeActions; - private boolean dragDropEnabled; - - - public QueueRecyclerAdapter(MainActivity mainActivity, SwipeActions swipeActions) { - super(mainActivity); - this.swipeActions = swipeActions; - dragDropEnabled = ! (UserPreferences.isQueueKeepSorted() || UserPreferences.isQueueLocked()); - } - - public void updateDragDropEnabled() { - dragDropEnabled = ! (UserPreferences.isQueueKeepSorted() || UserPreferences.isQueueLocked()); - notifyDataSetChanged(); - } - - @Override - @SuppressLint("ClickableViewAccessibility") - protected void afterBindViewHolder(EpisodeItemViewHolder holder, int pos) { - if (!dragDropEnabled) { - holder.dragHandle.setVisibility(View.GONE); - holder.dragHandle.setOnTouchListener(null); - holder.coverHolder.setOnTouchListener(null); - } else { - holder.dragHandle.setVisibility(View.VISIBLE); - holder.dragHandle.setOnTouchListener((v1, event) -> { - if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { - Log.d(TAG, "startDrag()"); - swipeActions.startDrag(holder); - } - return false; - }); - holder.coverHolder.setOnTouchListener((v1, event) -> { - if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { - boolean isLtr = holder.itemView.getLayoutDirection() == View.LAYOUT_DIRECTION_LTR; - float factor = isLtr ? 1 : -1; - if (factor * event.getX() < factor * 0.5 * v1.getWidth()) { - Log.d(TAG, "startDrag()"); - swipeActions.startDrag(holder); - } else { - Log.d(TAG, "Ignoring drag in right half of the image"); - } - } - return false; - }); - } - if (inActionMode()) { - holder.dragHandle.setOnTouchListener(null); - holder.coverHolder.setOnTouchListener(null); - } - - holder.isInQueue.setVisibility(View.GONE); - } - - @Override - public void onCreateContextMenu(final ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { - MenuInflater inflater = getActivity().getMenuInflater(); - inflater.inflate(R.menu.queue_context, menu); - super.onCreateContextMenu(menu, v, menuInfo); - - if (!inActionMode()) { - menu.findItem(R.id.multi_select).setVisible(true); - final boolean keepSorted = UserPreferences.isQueueKeepSorted(); - if (getItem(0).getId() == getLongPressedItem().getId() || keepSorted) { - menu.findItem(R.id.move_to_top_item).setVisible(false); - } - if (getItem(getItemCount() - 1).getId() == getLongPressedItem().getId() || keepSorted) { - menu.findItem(R.id.move_to_bottom_item).setVisible(false); - } - } else { - menu.findItem(R.id.move_to_top_item).setVisible(false); - menu.findItem(R.id.move_to_bottom_item).setVisible(false); - } - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/SelectableAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/SelectableAdapter.java deleted file mode 100644 index 70d00cbff..000000000 --- a/app/src/main/java/de/danoeh/antennapod/adapter/SelectableAdapter.java +++ /dev/null @@ -1,200 +0,0 @@ -package de.danoeh.antennapod.adapter; - -import android.app.Activity; -import android.view.ActionMode; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; - -import androidx.recyclerview.widget.RecyclerView; - -import de.danoeh.antennapod.R; - -import java.util.HashSet; - -/** - * Used by Recyclerviews that need to provide ability to select items. - */ -public abstract class SelectableAdapter extends RecyclerView.Adapter { - public static final int COUNT_AUTOMATICALLY = -1; - private ActionMode actionMode; - private final HashSet selectedIds = new HashSet<>(); - private final Activity activity; - private OnSelectModeListener onSelectModeListener; - boolean shouldSelectLazyLoadedItems = false; - private int totalNumberOfItems = COUNT_AUTOMATICALLY; - - public SelectableAdapter(Activity activity) { - this.activity = activity; - } - - public void startSelectMode(int pos) { - if (inActionMode()) { - endSelectMode(); - } - - if (onSelectModeListener != null) { - onSelectModeListener.onStartSelectMode(); - } - - shouldSelectLazyLoadedItems = false; - selectedIds.clear(); - selectedIds.add(getItemId(pos)); - notifyDataSetChanged(); - - actionMode = activity.startActionMode(new ActionMode.Callback() { - @Override - public boolean onCreateActionMode(ActionMode mode, Menu menu) { - MenuInflater inflater = mode.getMenuInflater(); - inflater.inflate(R.menu.multi_select_options, menu); - return true; - } - - @Override - public boolean onPrepareActionMode(ActionMode mode, Menu menu) { - updateTitle(); - toggleSelectAllIcon(menu.findItem(R.id.select_toggle), false); - return false; - } - - @Override - public boolean onActionItemClicked(ActionMode mode, MenuItem item) { - if (item.getItemId() == R.id.select_toggle) { - boolean selectAll = selectedIds.size() != getItemCount(); - shouldSelectLazyLoadedItems = selectAll; - setSelected(0, getItemCount(), selectAll); - toggleSelectAllIcon(item, selectAll); - updateTitle(); - return true; - } - return false; - } - - @Override - public void onDestroyActionMode(ActionMode mode) { - callOnEndSelectMode(); - actionMode = null; - shouldSelectLazyLoadedItems = false; - selectedIds.clear(); - notifyDataSetChanged(); - } - }); - updateTitle(); - } - - /** - * End action mode if currently in select mode, otherwise do nothing - */ - public void endSelectMode() { - if (inActionMode()) { - callOnEndSelectMode(); - actionMode.finish(); - } - } - - public boolean isSelected(int pos) { - return selectedIds.contains(getItemId(pos)); - } - - /** - * Set the selected state of item at given position - * - * @param pos the position to select - * @param selected true for selected state and false for unselected - */ - public void setSelected(int pos, boolean selected) { - if (selected) { - selectedIds.add(getItemId(pos)); - } else { - selectedIds.remove(getItemId(pos)); - } - updateTitle(); - } - - /** - * Set the selected state of item for a given range - * - * @param startPos start position of range, inclusive - * @param endPos end position of range, inclusive - * @param selected indicates the selection state - * @throws IllegalArgumentException if start and end positions are not valid - */ - public void setSelected(int startPos, int endPos, boolean selected) throws IllegalArgumentException { - for (int i = startPos; i < endPos && i < getItemCount(); i++) { - setSelected(i, selected); - } - notifyItemRangeChanged(startPos, (endPos - startPos)); - } - - protected void toggleSelection(int pos) { - setSelected(pos, !isSelected(pos)); - notifyItemChanged(pos); - - if (selectedIds.size() == 0) { - endSelectMode(); - } - } - - public boolean inActionMode() { - return actionMode != null; - } - - public int getSelectedCount() { - return selectedIds.size(); - } - - private void toggleSelectAllIcon(MenuItem selectAllItem, boolean allSelected) { - if (allSelected) { - selectAllItem.setIcon(R.drawable.ic_select_none); - selectAllItem.setTitle(R.string.deselect_all_label); - } else { - selectAllItem.setIcon(R.drawable.ic_select_all); - selectAllItem.setTitle(R.string.select_all_label); - } - } - - void updateTitle() { - if (actionMode == null) { - return; - } - int totalCount = getItemCount(); - int selectedCount = selectedIds.size(); - if (totalNumberOfItems != COUNT_AUTOMATICALLY) { - totalCount = totalNumberOfItems; - if (shouldSelectLazyLoadedItems) { - selectedCount += (totalNumberOfItems - getItemCount()); - } - } - actionMode.setTitle(activity.getResources() - .getQuantityString(R.plurals.num_selected_label, selectedIds.size(), - selectedCount, totalCount)); - } - - public void setOnSelectModeListener(OnSelectModeListener onSelectModeListener) { - this.onSelectModeListener = onSelectModeListener; - } - - private void callOnEndSelectMode() { - if (onSelectModeListener != null) { - onSelectModeListener.onEndSelectMode(); - } - } - - public boolean shouldSelectLazyLoadedItems() { - return shouldSelectLazyLoadedItems; - } - - /** - * Sets the total number of items that could be lazy-loaded. - * Can also be set to {@link #COUNT_AUTOMATICALLY} to simply use {@link #getItemCount} - */ - public void setTotalNumberOfItems(int totalNumberOfItems) { - this.totalNumberOfItems = totalNumberOfItems; - } - - public interface OnSelectModeListener { - void onStartSelectMode(); - - void onEndSelectMode(); - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/SimpleChipAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/SimpleChipAdapter.java deleted file mode 100644 index c7a04c3c7..000000000 --- a/app/src/main/java/de/danoeh/antennapod/adapter/SimpleChipAdapter.java +++ /dev/null @@ -1,57 +0,0 @@ -package de.danoeh.antennapod.adapter; - -import android.content.Context; -import android.view.ViewGroup; -import androidx.annotation.NonNull; -import androidx.recyclerview.widget.RecyclerView; -import com.google.android.material.chip.Chip; -import de.danoeh.antennapod.R; - -import java.util.List; - -public abstract class SimpleChipAdapter extends RecyclerView.Adapter { - private final Context context; - - public SimpleChipAdapter(Context context) { - this.context = context; - setHasStableIds(true); - } - - protected abstract List getChips(); - - protected abstract void onRemoveClicked(int position); - - @Override - @NonNull - public SimpleChipAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - Chip chip = new Chip(context); - chip.setCloseIconVisible(true); - chip.setCloseIconResource(R.drawable.ic_delete); - return new SimpleChipAdapter.ViewHolder(chip); - } - - @Override - public void onBindViewHolder(@NonNull SimpleChipAdapter.ViewHolder holder, int position) { - holder.chip.setText(getChips().get(position)); - holder.chip.setOnCloseIconClickListener(v -> onRemoveClicked(position)); - } - - @Override - public int getItemCount() { - return getChips().size(); - } - - @Override - public long getItemId(int position) { - return getChips().get(position).hashCode(); - } - - public static class ViewHolder extends RecyclerView.ViewHolder { - Chip chip; - - ViewHolder(Chip itemView) { - super(itemView); - chip = itemView; - } - } -} \ No newline at end of file diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/SubscriptionsRecyclerAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/SubscriptionsRecyclerAdapter.java deleted file mode 100644 index 262c1f906..000000000 --- a/app/src/main/java/de/danoeh/antennapod/adapter/SubscriptionsRecyclerAdapter.java +++ /dev/null @@ -1,308 +0,0 @@ -package de.danoeh.antennapod.adapter; - -import android.content.Context; -import android.graphics.Canvas; -import android.graphics.Rect; -import android.graphics.drawable.Drawable; -import android.os.Build; -import android.view.ContextMenu; -import android.view.InputDevice; -import android.view.LayoutInflater; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.MotionEvent; -import android.view.View; -import android.view.ViewGroup; -import android.widget.CheckBox; -import android.widget.FrameLayout; -import android.widget.ImageView; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.appcompat.content.res.AppCompatResources; -import androidx.cardview.widget.CardView; -import androidx.fragment.app.Fragment; -import androidx.recyclerview.widget.RecyclerView; - -import com.google.android.material.elevation.SurfaceColors; -import java.lang.ref.WeakReference; -import java.text.NumberFormat; -import java.util.ArrayList; -import java.util.List; - -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.activity.MainActivity; -import de.danoeh.antennapod.storage.preferences.UserPreferences; -import de.danoeh.antennapod.storage.database.NavDrawerData; -import de.danoeh.antennapod.fragment.FeedItemlistFragment; -import de.danoeh.antennapod.fragment.SubscriptionFragment; -import de.danoeh.antennapod.model.feed.Feed; - -/** - * Adapter for subscriptions - */ -public class SubscriptionsRecyclerAdapter extends SelectableAdapter - implements View.OnCreateContextMenuListener { - private static final int COVER_WITH_TITLE = 1; - - private final WeakReference mainActivityRef; - private List listItems; - private NavDrawerData.DrawerItem selectedItem = null; - int longPressedPosition = 0; // used to init actionMode - private int columnCount = 3; - - public SubscriptionsRecyclerAdapter(MainActivity mainActivity) { - super(mainActivity); - this.mainActivityRef = new WeakReference<>(mainActivity); - this.listItems = new ArrayList<>(); - setHasStableIds(true); - } - - public void setColumnCount(int columnCount) { - this.columnCount = columnCount; - } - - public Object getItem(int position) { - return listItems.get(position); - } - - public NavDrawerData.DrawerItem getSelectedItem() { - return selectedItem; - } - - @NonNull - @Override - public SubscriptionViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - View itemView = LayoutInflater.from(mainActivityRef.get()).inflate(R.layout.subscription_item, parent, false); - itemView.findViewById(R.id.titleLabel).setVisibility(viewType == COVER_WITH_TITLE ? View.VISIBLE : View.GONE); - return new SubscriptionViewHolder(itemView); - } - - @Override - public void onBindViewHolder(@NonNull SubscriptionViewHolder holder, int position) { - NavDrawerData.DrawerItem drawerItem = listItems.get(position); - boolean isFeed = drawerItem.type == NavDrawerData.DrawerItem.Type.FEED; - holder.bind(drawerItem); - holder.itemView.setOnCreateContextMenuListener(this); - if (inActionMode()) { - if (isFeed) { - holder.selectCheckbox.setVisibility(View.VISIBLE); - holder.selectView.setVisibility(View.VISIBLE); - } - holder.selectCheckbox.setChecked((isSelected(position))); - holder.selectCheckbox.setOnCheckedChangeListener((buttonView, isChecked) - -> setSelected(holder.getBindingAdapterPosition(), isChecked)); - holder.coverImage.setAlpha(0.6f); - holder.count.setVisibility(View.GONE); - } else { - holder.selectView.setVisibility(View.GONE); - holder.coverImage.setAlpha(1.0f); - } - - holder.itemView.setOnLongClickListener(v -> { - if (!inActionMode()) { - if (isFeed) { - longPressedPosition = holder.getBindingAdapterPosition(); - } - selectedItem = drawerItem; - } - return false; - }); - - holder.itemView.setOnTouchListener((v, e) -> { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - if (e.isFromSource(InputDevice.SOURCE_MOUSE) - && e.getButtonState() == MotionEvent.BUTTON_SECONDARY) { - if (!inActionMode()) { - if (isFeed) { - longPressedPosition = holder.getBindingAdapterPosition(); - } - selectedItem = drawerItem; - } - } - } - return false; - }); - holder.itemView.setOnClickListener(v -> { - if (isFeed) { - if (inActionMode()) { - holder.selectCheckbox.setChecked(!isSelected(holder.getBindingAdapterPosition())); - } else { - Fragment fragment = FeedItemlistFragment - .newInstance(((NavDrawerData.FeedDrawerItem) drawerItem).feed.getId()); - mainActivityRef.get().loadChildFragment(fragment); - } - } else if (!inActionMode()) { - Fragment fragment = SubscriptionFragment.newInstance(drawerItem.getTitle()); - mainActivityRef.get().loadChildFragment(fragment); - } - }); - - } - - @Override - public int getItemCount() { - return listItems.size(); - } - - @Override - public long getItemId(int position) { - if (position >= listItems.size()) { - return RecyclerView.NO_ID; // Dummy views - } - return listItems.get(position).id; - } - - @Override - public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { - if (inActionMode() || selectedItem == null) { - return; - } - MenuInflater inflater = mainActivityRef.get().getMenuInflater(); - if (selectedItem.type == NavDrawerData.DrawerItem.Type.FEED) { - inflater.inflate(R.menu.nav_feed_context, menu); - menu.findItem(R.id.multi_select).setVisible(true); - } else { - inflater.inflate(R.menu.nav_folder_context, menu); - } - menu.setHeaderTitle(selectedItem.getTitle()); - } - - public boolean onContextItemSelected(MenuItem item) { - if (item.getItemId() == R.id.multi_select) { - startSelectMode(longPressedPosition); - return true; - } - return false; - } - - public List getSelectedItems() { - List items = new ArrayList<>(); - for (int i = 0; i < getItemCount(); i++) { - if (isSelected(i)) { - NavDrawerData.DrawerItem drawerItem = listItems.get(i); - if (drawerItem.type == NavDrawerData.DrawerItem.Type.FEED) { - Feed feed = ((NavDrawerData.FeedDrawerItem) drawerItem).feed; - items.add(feed); - } - } - } - return items; - } - - public void setItems(List listItems) { - this.listItems = listItems; - notifyDataSetChanged(); - } - - @Override - public void setSelected(int pos, boolean selected) { - NavDrawerData.DrawerItem drawerItem = listItems.get(pos); - if (drawerItem.type == NavDrawerData.DrawerItem.Type.FEED) { - super.setSelected(pos, selected); - } - } - - @Override - public int getItemViewType(int position) { - return UserPreferences.shouldShowSubscriptionTitle() ? COVER_WITH_TITLE : 0; - } - - public class SubscriptionViewHolder extends RecyclerView.ViewHolder { - private final TextView title; - private final ImageView coverImage; - private final TextView count; - private final TextView fallbackTitle; - private final FrameLayout selectView; - private final CheckBox selectCheckbox; - private final CardView card; - private final View errorIcon; - - public SubscriptionViewHolder(@NonNull View itemView) { - super(itemView); - title = itemView.findViewById(R.id.titleLabel); - coverImage = itemView.findViewById(R.id.coverImage); - count = itemView.findViewById(R.id.countViewPill); - fallbackTitle = itemView.findViewById(R.id.fallbackTitleLabel); - selectView = itemView.findViewById(R.id.selectContainer); - selectCheckbox = itemView.findViewById(R.id.selectCheckBox); - card = itemView.findViewById(R.id.outerContainer); - errorIcon = itemView.findViewById(R.id.errorIcon); - } - - public void bind(NavDrawerData.DrawerItem drawerItem) { - Drawable drawable = AppCompatResources.getDrawable(selectView.getContext(), - R.drawable.ic_checkbox_background); - selectView.setBackground(drawable); // Setting this in XML crashes API <= 21 - title.setText(drawerItem.getTitle()); - fallbackTitle.setText(drawerItem.getTitle()); - coverImage.setContentDescription(drawerItem.getTitle()); - if (drawerItem.getCounter() > 0) { - count.setText(NumberFormat.getInstance().format(drawerItem.getCounter())); - count.setVisibility(View.VISIBLE); - } else { - count.setVisibility(View.GONE); - } - - CoverLoader coverLoader = new CoverLoader(); - boolean textAndImageCombined; - if (drawerItem.type == NavDrawerData.DrawerItem.Type.FEED) { - Feed feed = ((NavDrawerData.FeedDrawerItem) drawerItem).feed; - textAndImageCombined = feed.isLocalFeed() && feed.getImageUrl() != null - && feed.getImageUrl().startsWith(Feed.PREFIX_GENERATIVE_COVER); - coverLoader.withUri(feed.getImageUrl()); - errorIcon.setVisibility(feed.hasLastUpdateFailed() ? View.VISIBLE : View.GONE); - } else { - textAndImageCombined = true; - coverLoader.withResource(R.drawable.ic_tag); - errorIcon.setVisibility(View.GONE); - } - if (UserPreferences.shouldShowSubscriptionTitle()) { - // No need for fallback title when already showing title - fallbackTitle.setVisibility(View.GONE); - } else { - coverLoader.withPlaceholderView(fallbackTitle, textAndImageCombined); - } - coverLoader.withCoverView(coverImage); - coverLoader.load(); - - float density = mainActivityRef.get().getResources().getDisplayMetrics().density; - card.setCardBackgroundColor(SurfaceColors.getColorForElevation(mainActivityRef.get(), 1 * density)); - - int textPadding = columnCount <= 3 ? 16 : 8; - title.setPadding(textPadding, textPadding, textPadding, textPadding); - fallbackTitle.setPadding(textPadding, textPadding, textPadding, textPadding); - - int textSize = 14; - if (columnCount == 3) { - textSize = 15; - } else if (columnCount == 2) { - textSize = 16; - } - title.setTextSize(textSize); - fallbackTitle.setTextSize(textSize); - } - } - - public static float convertDpToPixel(Context context, float dp) { - return dp * context.getResources().getDisplayMetrics().density; - } - - public static class GridDividerItemDecorator extends RecyclerView.ItemDecoration { - @Override - public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { - super.onDraw(c, parent, state); - } - - @Override - public void getItemOffsets(@NonNull Rect outRect, - @NonNull View view, - @NonNull RecyclerView parent, - @NonNull RecyclerView.State state) { - super.getItemOffsets(outRect, view, parent, state); - Context context = parent.getContext(); - int insetOffset = (int) convertDpToPixel(context, 1f); - outRect.set(insetOffset, insetOffset, insetOffset, insetOffset); - } - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/CancelDownloadActionButton.java b/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/CancelDownloadActionButton.java deleted file mode 100644 index fa4b19b4d..000000000 --- a/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/CancelDownloadActionButton.java +++ /dev/null @@ -1,41 +0,0 @@ -package de.danoeh.antennapod.adapter.actionbutton; - -import android.content.Context; -import androidx.annotation.DrawableRes; -import androidx.annotation.StringRes; - -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterface; -import de.danoeh.antennapod.model.feed.FeedItem; -import de.danoeh.antennapod.model.feed.FeedMedia; -import de.danoeh.antennapod.storage.preferences.UserPreferences; -import de.danoeh.antennapod.storage.database.DBWriter; - -public class CancelDownloadActionButton extends ItemActionButton { - - public CancelDownloadActionButton(FeedItem item) { - super(item); - } - - @Override - @StringRes - public int getLabel() { - return R.string.cancel_download_label; - } - - @Override - @DrawableRes - public int getDrawable() { - return R.drawable.ic_cancel; - } - - @Override - public void onClick(Context context) { - FeedMedia media = item.getMedia(); - DownloadServiceInterface.get().cancel(context, media); - if (UserPreferences.isEnableAutodownload()) { - item.disableAutoDownload(); - DBWriter.setFeedItem(item); - } - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/DeleteActionButton.java b/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/DeleteActionButton.java deleted file mode 100644 index 6b9114e81..000000000 --- a/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/DeleteActionButton.java +++ /dev/null @@ -1,53 +0,0 @@ -package de.danoeh.antennapod.adapter.actionbutton; - -import android.content.Context; -import android.view.View; -import androidx.annotation.DrawableRes; -import androidx.annotation.StringRes; - -import java.util.Collections; - -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.model.feed.FeedItem; -import de.danoeh.antennapod.model.feed.FeedMedia; -import de.danoeh.antennapod.storage.database.DBWriter; -import de.danoeh.antennapod.view.LocalDeleteModal; - -public class DeleteActionButton extends ItemActionButton { - - public DeleteActionButton(FeedItem item) { - super(item); - } - - @Override - @StringRes - public int getLabel() { - return R.string.delete_label; - } - - @Override - @DrawableRes - public int getDrawable() { - return R.drawable.ic_delete; - } - - @Override - public void onClick(Context context) { - final FeedMedia media = item.getMedia(); - if (media == null) { - return; - } - - LocalDeleteModal.showLocalFeedDeleteWarningIfNecessary(context, Collections.singletonList(item), - () -> DBWriter.deleteFeedMediaOfItem(context, media)); - } - - @Override - public int getVisibility() { - if (item.getMedia() != null && (item.getMedia().isDownloaded() || item.getFeed().isLocalFeed())) { - return View.VISIBLE; - } - - return View.INVISIBLE; - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/DownloadActionButton.java b/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/DownloadActionButton.java deleted file mode 100644 index af573218c..000000000 --- a/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/DownloadActionButton.java +++ /dev/null @@ -1,74 +0,0 @@ -package de.danoeh.antennapod.adapter.actionbutton; - -import android.content.Context; -import android.view.View; - -import androidx.annotation.DrawableRes; -import androidx.annotation.NonNull; -import androidx.annotation.StringRes; - -import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterface; -import de.danoeh.antennapod.model.feed.FeedItem; -import de.danoeh.antennapod.model.feed.FeedMedia; -import de.danoeh.antennapod.storage.preferences.UsageStatistics; -import de.danoeh.antennapod.net.common.NetworkUtils; - -public class DownloadActionButton extends ItemActionButton { - - public DownloadActionButton(FeedItem item) { - super(item); - } - - @Override - @StringRes - public int getLabel() { - return R.string.download_label; - } - - @Override - @DrawableRes - public int getDrawable() { - return R.drawable.ic_download; - } - - @Override - public int getVisibility() { - return item.getFeed().isLocalFeed() ? View.INVISIBLE : View.VISIBLE; - } - - @Override - public void onClick(Context context) { - final FeedMedia media = item.getMedia(); - if (media == null || shouldNotDownload(media)) { - return; - } - - UsageStatistics.logAction(UsageStatistics.ACTION_DOWNLOAD); - - if (NetworkUtils.isEpisodeDownloadAllowed()) { - DownloadServiceInterface.get().downloadNow(context, item, false); - } else { - MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(context) - .setTitle(R.string.confirm_mobile_download_dialog_title) - .setPositiveButton(R.string.confirm_mobile_download_dialog_download_later, - (d, w) -> DownloadServiceInterface.get().downloadNow(context, item, false)) - .setNeutralButton(R.string.confirm_mobile_download_dialog_allow_this_time, - (d, w) -> DownloadServiceInterface.get().downloadNow(context, item, true)) - .setNegativeButton(R.string.cancel_label, null); - if (NetworkUtils.isNetworkRestricted() && NetworkUtils.isVpnOverWifi()) { - builder.setMessage(R.string.confirm_mobile_download_dialog_message_vpn); - } else { - builder.setMessage(R.string.confirm_mobile_download_dialog_message); - } - - builder.show(); - } - } - - private boolean shouldNotDownload(@NonNull FeedMedia media) { - boolean isDownloading = DownloadServiceInterface.get().isDownloadingEpisode(media.getDownloadUrl()); - return isDownloading || media.isDownloaded(); - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/ItemActionButton.java b/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/ItemActionButton.java deleted file mode 100644 index 07ed5d443..000000000 --- a/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/ItemActionButton.java +++ /dev/null @@ -1,64 +0,0 @@ -package de.danoeh.antennapod.adapter.actionbutton; - -import android.content.Context; -import android.widget.ImageView; -import androidx.annotation.DrawableRes; -import androidx.annotation.NonNull; -import androidx.annotation.StringRes; -import android.view.View; - -import de.danoeh.antennapod.playback.service.PlaybackStatus; -import de.danoeh.antennapod.model.feed.FeedItem; -import de.danoeh.antennapod.model.feed.FeedMedia; -import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterface; -import de.danoeh.antennapod.storage.preferences.UserPreferences; - -public abstract class ItemActionButton { - FeedItem item; - - ItemActionButton(FeedItem item) { - this.item = item; - } - - @StringRes - public abstract int getLabel(); - - @DrawableRes - public abstract int getDrawable(); - - public abstract void onClick(Context context); - - public int getVisibility() { - return View.VISIBLE; - } - - @NonNull - public static ItemActionButton forItem(@NonNull FeedItem item) { - final FeedMedia media = item.getMedia(); - if (media == null) { - return new MarkAsPlayedActionButton(item); - } - - final boolean isDownloadingMedia = DownloadServiceInterface.get().isDownloadingEpisode(media.getDownloadUrl()); - if (PlaybackStatus.isCurrentlyPlaying(media)) { - return new PauseActionButton(item); - } else if (item.getFeed().isLocalFeed()) { - return new PlayLocalActionButton(item); - } else if (media.isDownloaded()) { - return new PlayActionButton(item); - } else if (isDownloadingMedia) { - return new CancelDownloadActionButton(item); - } else if (UserPreferences.isStreamOverDownload()) { - return new StreamActionButton(item); - } else { - return new DownloadActionButton(item); - } - } - - public void configure(@NonNull View button, @NonNull ImageView icon, Context context) { - button.setVisibility(getVisibility()); - button.setContentDescription(context.getString(getLabel())); - button.setOnClickListener((view) -> onClick(context)); - icon.setImageResource(getDrawable()); - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/MarkAsPlayedActionButton.java b/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/MarkAsPlayedActionButton.java deleted file mode 100644 index 34fef11dc..000000000 --- a/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/MarkAsPlayedActionButton.java +++ /dev/null @@ -1,41 +0,0 @@ -package de.danoeh.antennapod.adapter.actionbutton; - -import android.content.Context; -import androidx.annotation.DrawableRes; -import androidx.annotation.StringRes; -import android.view.View; - -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.model.feed.FeedItem; -import de.danoeh.antennapod.storage.database.DBWriter; - -public class MarkAsPlayedActionButton extends ItemActionButton { - - public MarkAsPlayedActionButton(FeedItem item) { - super(item); - } - - @Override - @StringRes - public int getLabel() { - return (item.hasMedia() ? R.string.mark_read_label : R.string.mark_read_no_media_label); - } - - @Override - @DrawableRes - public int getDrawable() { - return R.drawable.ic_check; - } - - @Override - public void onClick(Context context) { - if (!item.isPlayed()) { - DBWriter.markItemPlayed(item, FeedItem.PLAYED, true); - } - } - - @Override - public int getVisibility() { - return (item.isPlayed()) ? View.INVISIBLE : View.VISIBLE; - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/PauseActionButton.java b/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/PauseActionButton.java deleted file mode 100644 index 44b488df9..000000000 --- a/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/PauseActionButton.java +++ /dev/null @@ -1,42 +0,0 @@ -package de.danoeh.antennapod.adapter.actionbutton; - -import android.content.Context; -import android.view.KeyEvent; -import androidx.annotation.DrawableRes; -import androidx.annotation.StringRes; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.playback.service.PlaybackStatus; -import de.danoeh.antennapod.model.feed.FeedItem; -import de.danoeh.antennapod.model.feed.FeedMedia; -import de.danoeh.antennapod.ui.appstartintent.MediaButtonStarter; - -public class PauseActionButton extends ItemActionButton { - - public PauseActionButton(FeedItem item) { - super(item); - } - - @Override - @StringRes - public int getLabel() { - return R.string.pause_label; - } - - @Override - @DrawableRes - public int getDrawable() { - return R.drawable.ic_pause; - } - - @Override - public void onClick(Context context) { - FeedMedia media = item.getMedia(); - if (media == null) { - return; - } - - if (PlaybackStatus.isCurrentlyPlaying(media)) { - context.sendBroadcast(MediaButtonStarter.createIntent(context, KeyEvent.KEYCODE_MEDIA_PAUSE)); - } - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/PlayActionButton.java b/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/PlayActionButton.java deleted file mode 100644 index 45a8e0cdd..000000000 --- a/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/PlayActionButton.java +++ /dev/null @@ -1,60 +0,0 @@ -package de.danoeh.antennapod.adapter.actionbutton; - -import android.content.Context; -import android.util.Log; -import androidx.annotation.DrawableRes; -import androidx.annotation.StringRes; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.playback.service.PlaybackService; -import de.danoeh.antennapod.playback.service.PlaybackServiceStarter; -import de.danoeh.antennapod.storage.database.DBWriter; -import de.danoeh.antennapod.event.FeedItemEvent; -import de.danoeh.antennapod.event.MessageEvent; -import de.danoeh.antennapod.model.feed.FeedItem; -import de.danoeh.antennapod.model.feed.FeedMedia; -import de.danoeh.antennapod.model.playback.MediaType; -import org.greenrobot.eventbus.EventBus; - -public class PlayActionButton extends ItemActionButton { - private static final String TAG = "PlayActionButton"; - - public PlayActionButton(FeedItem item) { - super(item); - } - - @Override - @StringRes - public int getLabel() { - return R.string.play_label; - } - - @Override - @DrawableRes - public int getDrawable() { - return R.drawable.ic_play_24dp; - } - - @Override - public void onClick(Context context) { - FeedMedia media = item.getMedia(); - if (media == null) { - return; - } - if (!media.fileExists()) { - Log.i(TAG, "Missing episode. Will update the database now."); - media.setDownloaded(false); - media.setLocalFileUrl(null); - DBWriter.setFeedMedia(media); - EventBus.getDefault().post(FeedItemEvent.updated(media.getItem())); - EventBus.getDefault().post(new MessageEvent(context.getString(R.string.error_file_not_found))); - return; - } - new PlaybackServiceStarter(context, media) - .callEvenIfRunning(true) - .start(); - - if (media.getMediaType() == MediaType.VIDEO) { - context.startActivity(PlaybackService.getPlayerActivityIntent(context, media)); - } - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/PlayLocalActionButton.java b/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/PlayLocalActionButton.java deleted file mode 100644 index 6acb62434..000000000 --- a/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/PlayLocalActionButton.java +++ /dev/null @@ -1,46 +0,0 @@ -package de.danoeh.antennapod.adapter.actionbutton; - -import android.content.Context; -import androidx.annotation.DrawableRes; -import androidx.annotation.StringRes; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.model.feed.FeedItem; -import de.danoeh.antennapod.model.feed.FeedMedia; -import de.danoeh.antennapod.model.playback.MediaType; -import de.danoeh.antennapod.playback.service.PlaybackService; -import de.danoeh.antennapod.playback.service.PlaybackServiceStarter; - -public class PlayLocalActionButton extends ItemActionButton { - - public PlayLocalActionButton(FeedItem item) { - super(item); - } - - @Override - @StringRes - public int getLabel() { - return R.string.play_label; - } - - @Override - @DrawableRes - public int getDrawable() { - return R.drawable.ic_play_24dp; - } - - @Override - public void onClick(Context context) { - final FeedMedia media = item.getMedia(); - if (media == null) { - return; - } - - new PlaybackServiceStarter(context, media) - .callEvenIfRunning(true) - .start(); - - if (media.getMediaType() == MediaType.VIDEO) { - context.startActivity(PlaybackService.getPlayerActivityIntent(context, media)); - } - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/StreamActionButton.java b/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/StreamActionButton.java deleted file mode 100644 index 372a7984e..000000000 --- a/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/StreamActionButton.java +++ /dev/null @@ -1,56 +0,0 @@ -package de.danoeh.antennapod.adapter.actionbutton; - -import android.content.Context; - -import androidx.annotation.DrawableRes; -import androidx.annotation.StringRes; - -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.model.feed.FeedItem; -import de.danoeh.antennapod.model.feed.FeedMedia; -import de.danoeh.antennapod.model.playback.MediaType; -import de.danoeh.antennapod.playback.service.PlaybackService; -import de.danoeh.antennapod.playback.service.PlaybackServiceStarter; -import de.danoeh.antennapod.storage.preferences.UsageStatistics; -import de.danoeh.antennapod.net.common.NetworkUtils; -import de.danoeh.antennapod.dialog.StreamingConfirmationDialog; - -public class StreamActionButton extends ItemActionButton { - - public StreamActionButton(FeedItem item) { - super(item); - } - - @Override - @StringRes - public int getLabel() { - return R.string.stream_label; - } - - @Override - @DrawableRes - public int getDrawable() { - return R.drawable.ic_stream; - } - - @Override - public void onClick(Context context) { - final FeedMedia media = item.getMedia(); - if (media == null) { - return; - } - UsageStatistics.logAction(UsageStatistics.ACTION_STREAM); - - if (!NetworkUtils.isStreamingAllowed()) { - new StreamingConfirmationDialog(context, media).show(); - return; - } - new PlaybackServiceStarter(context, media) - .callEvenIfRunning(true) - .start(); - - if (media.getMediaType() == MediaType.VIDEO) { - context.startActivity(PlaybackService.getPlayerActivityIntent(context, media)); - } - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/VisitWebsiteActionButton.java b/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/VisitWebsiteActionButton.java deleted file mode 100644 index 03ccce2fe..000000000 --- a/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/VisitWebsiteActionButton.java +++ /dev/null @@ -1,38 +0,0 @@ -package de.danoeh.antennapod.adapter.actionbutton; - -import android.content.Context; -import android.view.View; -import androidx.annotation.DrawableRes; -import androidx.annotation.StringRes; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.model.feed.FeedItem; -import de.danoeh.antennapod.core.util.IntentUtils; - -public class VisitWebsiteActionButton extends ItemActionButton { - - public VisitWebsiteActionButton(FeedItem item) { - super(item); - } - - @Override - @StringRes - public int getLabel() { - return R.string.visit_website_label; - } - - @Override - @DrawableRes - public int getDrawable() { - return R.drawable.ic_web; - } - - @Override - public void onClick(Context context) { - IntentUtils.openInBrowser(context, item.getLink()); - } - - @Override - public int getVisibility() { - return (item.getLink() == null) ? View.INVISIBLE : View.VISIBLE; - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/AllEpisodesFilterDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/AllEpisodesFilterDialog.java deleted file mode 100644 index f47a8f8eb..000000000 --- a/app/src/main/java/de/danoeh/antennapod/dialog/AllEpisodesFilterDialog.java +++ /dev/null @@ -1,31 +0,0 @@ -package de.danoeh.antennapod.dialog; - -import android.os.Bundle; -import de.danoeh.antennapod.model.feed.FeedItemFilter; -import org.greenrobot.eventbus.EventBus; - -import java.util.Set; - -public class AllEpisodesFilterDialog extends ItemFilterDialog { - - public static AllEpisodesFilterDialog newInstance(FeedItemFilter filter) { - AllEpisodesFilterDialog dialog = new AllEpisodesFilterDialog(); - Bundle arguments = new Bundle(); - arguments.putSerializable(ARGUMENT_FILTER, filter); - dialog.setArguments(arguments); - return dialog; - } - - @Override - void onFilterChanged(Set newFilterValues) { - EventBus.getDefault().post(new AllEpisodesFilterChangedEvent(newFilterValues)); - } - - public static class AllEpisodesFilterChangedEvent { - public final Set filterValues; - - public AllEpisodesFilterChangedEvent(Set filterValues) { - this.filterValues = filterValues; - } - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/DownloadLogDetailsDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/DownloadLogDetailsDialog.java deleted file mode 100644 index 82e32aed3..000000000 --- a/app/src/main/java/de/danoeh/antennapod/dialog/DownloadLogDetailsDialog.java +++ /dev/null @@ -1,65 +0,0 @@ -package de.danoeh.antennapod.dialog; - -import android.content.ClipData; -import android.content.ClipboardManager; -import android.content.Context; -import android.os.Build; -import android.widget.TextView; -import androidx.annotation.NonNull; -import androidx.appcompat.app.AlertDialog; -import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.core.util.DownloadErrorLabel; -import de.danoeh.antennapod.model.download.DownloadResult; -import de.danoeh.antennapod.storage.database.DBReader; -import de.danoeh.antennapod.event.MessageEvent; -import de.danoeh.antennapod.model.feed.Feed; -import de.danoeh.antennapod.model.feed.FeedMedia; -import org.greenrobot.eventbus.EventBus; - -public class DownloadLogDetailsDialog extends MaterialAlertDialogBuilder { - - public DownloadLogDetailsDialog(@NonNull Context context, DownloadResult status) { - super(context); - - String url = "unknown"; - if (status.getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA) { - FeedMedia media = DBReader.getFeedMedia(status.getFeedfileId()); - if (media != null) { - url = media.getDownloadUrl(); - } - } else if (status.getFeedfileType() == Feed.FEEDFILETYPE_FEED) { - Feed feed = DBReader.getFeed(status.getFeedfileId()); - if (feed != null) { - url = feed.getDownloadUrl(); - } - } - - String message = context.getString(R.string.download_successful); - if (!status.isSuccessful()) { - message = status.getReasonDetailed(); - } - - String messageFull = context.getString(R.string.download_log_details_message, - context.getString(DownloadErrorLabel.from(status.getReason())), message, url); - setTitle(R.string.download_error_details); - setMessage(messageFull); - setPositiveButton(android.R.string.ok, null); - setNeutralButton(R.string.copy_to_clipboard, (dialog, which) -> { - ClipboardManager clipboard = (ClipboardManager) getContext() - .getSystemService(Context.CLIPBOARD_SERVICE); - ClipData clip = ClipData.newPlainText(context.getString(R.string.download_error_details), messageFull); - clipboard.setPrimaryClip(clip); - if (Build.VERSION.SDK_INT < 32) { - EventBus.getDefault().post(new MessageEvent(context.getString(R.string.copied_to_clipboard))); - } - }); - } - - @Override - public AlertDialog show() { - AlertDialog dialog = super.show(); - ((TextView) dialog.findViewById(android.R.id.message)).setTextIsSelectable(true); - return dialog; - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/DrawerPreferencesDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/DrawerPreferencesDialog.java deleted file mode 100644 index 8f174f207..000000000 --- a/app/src/main/java/de/danoeh/antennapod/dialog/DrawerPreferencesDialog.java +++ /dev/null @@ -1,50 +0,0 @@ -package de.danoeh.antennapod.dialog; - -import android.content.Context; -import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.storage.preferences.UserPreferences; -import de.danoeh.antennapod.fragment.NavDrawerFragment; - -import java.util.List; - -public class DrawerPreferencesDialog { - public static void show(Context context, Runnable callback) { - final List hiddenDrawerItems = UserPreferences.getHiddenDrawerItems(); - final String[] navTitles = context.getResources().getStringArray(R.array.nav_drawer_titles); - boolean[] checked = new boolean[NavDrawerFragment.NAV_DRAWER_TAGS.length]; - for (int i = 0; i < NavDrawerFragment.NAV_DRAWER_TAGS.length; i++) { - String tag = NavDrawerFragment.NAV_DRAWER_TAGS[i]; - if (!hiddenDrawerItems.contains(tag)) { - checked[i] = true; - } - } - MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(context); - builder.setTitle(R.string.drawer_preferences); - builder.setMultiChoiceItems(navTitles, checked, (dialog, which, isChecked) -> { - if (isChecked) { - hiddenDrawerItems.remove(NavDrawerFragment.NAV_DRAWER_TAGS[which]); - } else { - hiddenDrawerItems.add(NavDrawerFragment.NAV_DRAWER_TAGS[which]); - } - }); - builder.setPositiveButton(R.string.confirm_label, (dialog, which) -> { - UserPreferences.setHiddenDrawerItems(hiddenDrawerItems); - - if (hiddenDrawerItems.contains(UserPreferences.getDefaultPage())) { - for (String tag : NavDrawerFragment.NAV_DRAWER_TAGS) { - if (!hiddenDrawerItems.contains(tag)) { - UserPreferences.setDefaultPage(tag); - break; - } - } - } - - if (callback != null) { - callback.run(); - } - }); - builder.setNegativeButton(R.string.cancel_label, null); - builder.create().show(); - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/EditUrlSettingsDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/EditUrlSettingsDialog.java deleted file mode 100644 index 67c5d85cf..000000000 --- a/app/src/main/java/de/danoeh/antennapod/dialog/EditUrlSettingsDialog.java +++ /dev/null @@ -1,88 +0,0 @@ -package de.danoeh.antennapod.dialog; - -import android.app.Activity; -import android.os.CountDownTimer; -import android.view.LayoutInflater; -import androidx.appcompat.app.AlertDialog; -import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.net.download.serviceinterface.FeedUpdateManager; -import de.danoeh.antennapod.storage.database.DBWriter; -import de.danoeh.antennapod.databinding.EditTextDialogBinding; -import de.danoeh.antennapod.model.feed.Feed; - -import java.lang.ref.WeakReference; -import java.util.Locale; -import java.util.concurrent.ExecutionException; - -public abstract class EditUrlSettingsDialog { - public static final String TAG = "EditUrlSettingsDialog"; - private final WeakReference activityRef; - private final Feed feed; - - public EditUrlSettingsDialog(Activity activity, Feed feed) { - this.activityRef = new WeakReference<>(activity); - this.feed = feed; - } - - public void show() { - Activity activity = activityRef.get(); - if (activity == null) { - return; - } - - final EditTextDialogBinding binding = EditTextDialogBinding.inflate(LayoutInflater.from(activity)); - - binding.urlEditText.setText(feed.getDownloadUrl()); - - new MaterialAlertDialogBuilder(activity) - .setView(binding.getRoot()) - .setTitle(R.string.edit_url_menu) - .setPositiveButton(android.R.string.ok, (d, input) -> - showConfirmAlertDialog(String.valueOf(binding.urlEditText.getText()))) - .setNegativeButton(R.string.cancel_label, null) - .show(); - } - - private void onConfirmed(String original, String updated) { - try { - DBWriter.updateFeedDownloadURL(original, updated).get(); - feed.setDownloadUrl(updated); - FeedUpdateManager.getInstance().runOnce(activityRef.get(), feed); - } catch (ExecutionException | InterruptedException e) { - throw new RuntimeException(e); - } - } - - private void showConfirmAlertDialog(String url) { - Activity activity = activityRef.get(); - - AlertDialog alertDialog = new MaterialAlertDialogBuilder(activity) - .setTitle(R.string.edit_url_menu) - .setMessage(R.string.edit_url_confirmation_msg) - .setPositiveButton(android.R.string.ok, (d, input) -> { - onConfirmed(feed.getDownloadUrl(), url); - setUrl(url); - }) - .setNegativeButton(R.string.cancel_label, null) - .show(); - alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false); - - new CountDownTimer(15000, 1000) { - @Override - public void onTick(long millisUntilFinished) { - alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setText( - String.format(Locale.getDefault(), "%s (%d)", - activity.getString(android.R.string.ok), millisUntilFinished / 1000 + 1)); - } - - @Override - public void onFinish() { - alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(true); - alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setText(android.R.string.ok); - } - }.start(); - } - - protected abstract void setUrl(String url); -} diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/EpisodeFilterDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/EpisodeFilterDialog.java deleted file mode 100644 index 220650f0f..000000000 --- a/app/src/main/java/de/danoeh/antennapod/dialog/EpisodeFilterDialog.java +++ /dev/null @@ -1,112 +0,0 @@ -package de.danoeh.antennapod.dialog; - -import android.content.Context; -import android.content.DialogInterface; -import android.text.TextUtils; -import android.view.LayoutInflater; -import androidx.recyclerview.widget.GridLayoutManager; -import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.adapter.SimpleChipAdapter; -import de.danoeh.antennapod.databinding.EpisodeFilterDialogBinding; -import de.danoeh.antennapod.model.feed.FeedFilter; -import de.danoeh.antennapod.view.ItemOffsetDecoration; - -import java.util.List; - -/** - * Displays a dialog with a text box for filtering episodes and two radio buttons for exclusion/inclusion - */ -public abstract class EpisodeFilterDialog extends MaterialAlertDialogBuilder { - private final EpisodeFilterDialogBinding viewBinding; - private final List termList; - - public EpisodeFilterDialog(Context context, FeedFilter filter) { - super(context); - viewBinding = EpisodeFilterDialogBinding.inflate(LayoutInflater.from(context)); - - setTitle(R.string.episode_filters_label); - setView(viewBinding.getRoot()); - - viewBinding.durationCheckBox.setOnCheckedChangeListener( - (buttonView, isChecked) -> viewBinding.episodeFilterDurationText.setEnabled(isChecked)); - if (filter.hasMinimalDurationFilter()) { - viewBinding.durationCheckBox.setChecked(true); - // Store minimal duration in seconds, show in minutes - viewBinding.episodeFilterDurationText - .setText(String.valueOf(filter.getMinimalDurationFilter() / 60)); - } else { - viewBinding.episodeFilterDurationText.setEnabled(false); - } - - if (filter.excludeOnly()) { - termList = filter.getExcludeFilter(); - viewBinding.excludeRadio.setChecked(true); - } else { - termList = filter.getIncludeFilter(); - viewBinding.includeRadio.setChecked(true); - } - setupWordsList(); - - setNegativeButton(R.string.cancel_label, null); - setPositiveButton(R.string.confirm_label, this::onConfirmClick); - } - - private void setupWordsList() { - viewBinding.termsRecycler.setLayoutManager(new GridLayoutManager(getContext(), 2)); - viewBinding.termsRecycler.addItemDecoration(new ItemOffsetDecoration(getContext(), 4)); - SimpleChipAdapter adapter = new SimpleChipAdapter(getContext()) { - @Override - protected List getChips() { - return termList; - } - - @Override - protected void onRemoveClicked(int position) { - termList.remove(position); - notifyDataSetChanged(); - } - }; - viewBinding.termsRecycler.setAdapter(adapter); - viewBinding.termsTextInput.setEndIconOnClickListener(v -> { - String newWord = viewBinding.termsTextInput.getEditText().getText().toString().replace("\"", "").trim(); - if (TextUtils.isEmpty(newWord) || termList.contains(newWord)) { - return; - } - termList.add(newWord); - viewBinding.termsTextInput.getEditText().setText(""); - adapter.notifyDataSetChanged(); - }); - } - - protected abstract void onConfirmed(FeedFilter filter); - - private void onConfirmClick(DialogInterface dialog, int which) { - int minimalDuration = -1; - if (viewBinding.durationCheckBox.isChecked()) { - try { - // Store minimal duration in seconds - minimalDuration = Integer.parseInt( - viewBinding.episodeFilterDurationText.getText().toString()) * 60; - } catch (NumberFormatException e) { - // Do not change anything on error - } - } - String excludeFilter = ""; - String includeFilter = ""; - if (viewBinding.includeRadio.isChecked()) { - includeFilter = toFilterString(termList); - } else { - excludeFilter = toFilterString(termList); - } - onConfirmed(new FeedFilter(includeFilter, excludeFilter, minimalDuration)); - } - - private String toFilterString(List words) { - StringBuilder result = new StringBuilder(); - for (String word : words) { - result.append("\"").append(word).append("\" "); - } - return result.toString(); - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/FeedItemFilterDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/FeedItemFilterDialog.java deleted file mode 100644 index a88a6600a..000000000 --- a/app/src/main/java/de/danoeh/antennapod/dialog/FeedItemFilterDialog.java +++ /dev/null @@ -1,26 +0,0 @@ -package de.danoeh.antennapod.dialog; - -import android.os.Bundle; -import de.danoeh.antennapod.storage.database.DBWriter; -import de.danoeh.antennapod.model.feed.Feed; - -import java.util.Set; - -public class FeedItemFilterDialog extends ItemFilterDialog { - private static final String ARGUMENT_FEED_ID = "feedId"; - - public static FeedItemFilterDialog newInstance(Feed feed) { - FeedItemFilterDialog dialog = new FeedItemFilterDialog(); - Bundle arguments = new Bundle(); - arguments.putSerializable(ARGUMENT_FILTER, feed.getItemFilter()); - arguments.putLong(ARGUMENT_FEED_ID, feed.getId()); - dialog.setArguments(arguments); - return dialog; - } - - @Override - void onFilterChanged(Set newFilterValues) { - long feedId = getArguments().getLong(ARGUMENT_FEED_ID); - DBWriter.setFeedItemsFilter(feedId, newFilterValues); - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/FeedPreferenceSkipDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/FeedPreferenceSkipDialog.java deleted file mode 100644 index 77c9ff67e..000000000 --- a/app/src/main/java/de/danoeh/antennapod/dialog/FeedPreferenceSkipDialog.java +++ /dev/null @@ -1,48 +0,0 @@ -package de.danoeh.antennapod.dialog; - -import android.content.Context; -import android.view.View; -import android.widget.EditText; -import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import de.danoeh.antennapod.R; - -/** - * Displays a dialog with a username and password text field and an optional checkbox to save username and preferences. - */ -public abstract class FeedPreferenceSkipDialog extends MaterialAlertDialogBuilder { - - public FeedPreferenceSkipDialog(Context context, int skipIntroInitialValue, - int skipEndInitialValue) { - super(context); - setTitle(R.string.pref_feed_skip); - View rootView = View.inflate(context, R.layout.feed_pref_skip_dialog, null); - setView(rootView); - - final EditText etxtSkipIntro = rootView.findViewById(R.id.etxtSkipIntro); - final EditText etxtSkipEnd = rootView.findViewById(R.id.etxtSkipEnd); - - etxtSkipIntro.setText(String.valueOf(skipIntroInitialValue)); - etxtSkipEnd.setText(String.valueOf(skipEndInitialValue)); - - setNegativeButton(R.string.cancel_label, null); - setPositiveButton(R.string.confirm_label, (dialog, which) - -> { - int skipIntro; - int skipEnding; - try { - skipIntro = Integer.parseInt(etxtSkipIntro.getText().toString()); - } catch (NumberFormatException e) { - skipIntro = 0; - } - - try { - skipEnding = Integer.parseInt(etxtSkipEnd.getText().toString()); - } catch (NumberFormatException e) { - skipEnding = 0; - } - onConfirmed(skipIntro, skipEnding); - }); - } - - protected abstract void onConfirmed(int skipIntro, int skipEndig); -} diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/FeedSortDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/FeedSortDialog.java deleted file mode 100644 index daa00b8a3..000000000 --- a/app/src/main/java/de/danoeh/antennapod/dialog/FeedSortDialog.java +++ /dev/null @@ -1,39 +0,0 @@ -package de.danoeh.antennapod.dialog; - -import android.content.Context; - -import com.google.android.material.dialog.MaterialAlertDialogBuilder; - -import de.danoeh.antennapod.model.feed.FeedOrder; -import org.greenrobot.eventbus.EventBus; - -import java.util.Arrays; -import java.util.List; - -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.event.UnreadItemsUpdateEvent; -import de.danoeh.antennapod.storage.preferences.UserPreferences; - -public class FeedSortDialog { - public static void showDialog(Context context) { - MaterialAlertDialogBuilder dialog = new MaterialAlertDialogBuilder(context); - dialog.setTitle(context.getString(R.string.pref_nav_drawer_feed_order_title)); - dialog.setNegativeButton(android.R.string.cancel, (d, listener) -> d.dismiss()); - - int selected = UserPreferences.getFeedOrder().id; - List entryValues = - Arrays.asList(context.getResources().getStringArray(R.array.nav_drawer_feed_order_values)); - final int selectedIndex = entryValues.indexOf("" + selected); - - String[] items = context.getResources().getStringArray(R.array.nav_drawer_feed_order_options); - dialog.setSingleChoiceItems(items, selectedIndex, (d, which) -> { - if (selectedIndex != which) { - UserPreferences.setFeedOrder(FeedOrder.fromOrdinal(Integer.parseInt(entryValues.get(which)))); - //Update subscriptions - EventBus.getDefault().post(new UnreadItemsUpdateEvent()); - } - d.dismiss(); - }); - dialog.show(); - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/ItemFilterDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/ItemFilterDialog.java deleted file mode 100644 index 359c513af..000000000 --- a/app/src/main/java/de/danoeh/antennapod/dialog/ItemFilterDialog.java +++ /dev/null @@ -1,123 +0,0 @@ -package de.danoeh.antennapod.dialog; - -import android.app.Dialog; -import android.os.Bundle; -import android.text.TextUtils; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.Button; -import android.widget.FrameLayout; -import android.widget.LinearLayout; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.google.android.material.bottomsheet.BottomSheetBehavior; -import com.google.android.material.bottomsheet.BottomSheetDialog; -import com.google.android.material.bottomsheet.BottomSheetDialogFragment; -import com.google.android.material.button.MaterialButtonToggleGroup; - -import java.util.Collections; -import java.util.HashSet; -import java.util.Set; - -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.core.feed.FeedItemFilterGroup; -import de.danoeh.antennapod.databinding.FilterDialogBinding; -import de.danoeh.antennapod.databinding.FilterDialogRowBinding; -import de.danoeh.antennapod.model.feed.FeedItemFilter; - -public abstract class ItemFilterDialog extends BottomSheetDialogFragment { - protected static final String ARGUMENT_FILTER = "filter"; - - private LinearLayout rows; - - @Nullable - @Override - public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, - @Nullable Bundle savedInstanceState) { - View layout = inflater.inflate(R.layout.filter_dialog, null, false); - FilterDialogBinding binding = FilterDialogBinding.bind(layout); - rows = binding.filterRows; - FeedItemFilter filter = (FeedItemFilter) getArguments().getSerializable(ARGUMENT_FILTER); - - //add filter rows - for (FeedItemFilterGroup item : FeedItemFilterGroup.values()) { - FilterDialogRowBinding rowBinding = FilterDialogRowBinding.inflate(inflater); - rowBinding.getRoot().addOnButtonCheckedListener( - (group, checkedId, isChecked) -> onFilterChanged(getNewFilterValues())); - rowBinding.filterButton1.setText(item.values[0].displayName); - rowBinding.filterButton1.setTag(item.values[0].filterId); - rowBinding.filterButton2.setText(item.values[1].displayName); - rowBinding.filterButton2.setTag(item.values[1].filterId); - rowBinding.filterButton1.setMaxLines(3); - rowBinding.filterButton1.setSingleLine(false); - rowBinding.filterButton2.setMaxLines(3); - rowBinding.filterButton2.setSingleLine(false); - rows.addView(rowBinding.getRoot(), rows.getChildCount() - 1); - } - - binding.confirmFiltermenu.setOnClickListener(view1 -> dismiss()); - binding.resetFiltermenu.setOnClickListener(view1 -> { - onFilterChanged(Collections.emptySet()); - for (int i = 0; i < rows.getChildCount(); i++) { - if (rows.getChildAt(i) instanceof MaterialButtonToggleGroup) { - ((MaterialButtonToggleGroup) rows.getChildAt(i)).clearChecked(); - } - } - }); - - for (String filterId : filter.getValues()) { - if (!TextUtils.isEmpty(filterId)) { - Button button = layout.findViewWithTag(filterId); - if (button != null) { - ((MaterialButtonToggleGroup) button.getParent()).check(button.getId()); - } - } - } - return layout; - } - - @NonNull - @Override - public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { - Dialog dialog = super.onCreateDialog(savedInstanceState); - dialog.setOnShowListener(dialogInterface -> { - BottomSheetDialog bottomSheetDialog = (BottomSheetDialog) dialogInterface; - setupFullHeight(bottomSheetDialog); - }); - return dialog; - } - - private void setupFullHeight(BottomSheetDialog bottomSheetDialog) { - FrameLayout bottomSheet = (FrameLayout) bottomSheetDialog.findViewById(R.id.design_bottom_sheet); - if (bottomSheet != null) { - BottomSheetBehavior behavior = BottomSheetBehavior.from(bottomSheet); - ViewGroup.LayoutParams layoutParams = bottomSheet.getLayoutParams(); - bottomSheet.setLayoutParams(layoutParams); - behavior.setState(BottomSheetBehavior.STATE_EXPANDED); - } - } - - protected Set getNewFilterValues() { - final Set newFilterValues = new HashSet<>(); - for (int i = 0; i < rows.getChildCount(); i++) { - if (!(rows.getChildAt(i) instanceof MaterialButtonToggleGroup)) { - continue; - } - MaterialButtonToggleGroup group = (MaterialButtonToggleGroup) rows.getChildAt(i); - if (group.getCheckedButtonId() == View.NO_ID) { - continue; - } - String tag = (String) group.findViewById(group.getCheckedButtonId()).getTag(); - if (tag == null) { // Clear buttons use no tag - continue; - } - newFilterValues.add(tag); - } - return newFilterValues; - } - - abstract void onFilterChanged(Set newFilterValues); -} diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/ItemSortDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/ItemSortDialog.java deleted file mode 100644 index cd6cc4b0a..000000000 --- a/app/src/main/java/de/danoeh/antennapod/dialog/ItemSortDialog.java +++ /dev/null @@ -1,104 +0,0 @@ -package de.danoeh.antennapod.dialog; - -import android.app.Dialog; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.FrameLayout; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import com.google.android.material.bottomsheet.BottomSheetBehavior; -import com.google.android.material.bottomsheet.BottomSheetDialog; -import com.google.android.material.bottomsheet.BottomSheetDialogFragment; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.databinding.SortDialogBinding; -import de.danoeh.antennapod.databinding.SortDialogItemActiveBinding; -import de.danoeh.antennapod.databinding.SortDialogItemBinding; -import de.danoeh.antennapod.model.feed.SortOrder; - -public class ItemSortDialog extends BottomSheetDialogFragment { - protected SortOrder sortOrder; - protected SortDialogBinding viewBinding; - - @Nullable - @Override - public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, - @Nullable Bundle savedInstanceState) { - viewBinding = SortDialogBinding.inflate(inflater); - populateList(); - viewBinding.keepSortedCheckbox.setOnCheckedChangeListener( - (buttonView, isChecked) -> ItemSortDialog.this.onSelectionChanged()); - return viewBinding.getRoot(); - } - - private void populateList() { - viewBinding.gridLayout.removeAllViews(); - onAddItem(R.string.episode_title, SortOrder.EPISODE_TITLE_A_Z, SortOrder.EPISODE_TITLE_Z_A, true); - onAddItem(R.string.feed_title, SortOrder.FEED_TITLE_A_Z, SortOrder.FEED_TITLE_Z_A, true); - onAddItem(R.string.duration, SortOrder.DURATION_SHORT_LONG, SortOrder.DURATION_LONG_SHORT, true); - onAddItem(R.string.date, SortOrder.DATE_OLD_NEW, SortOrder.DATE_NEW_OLD, false); - onAddItem(R.string.size, SortOrder.SIZE_SMALL_LARGE, SortOrder.SIZE_LARGE_SMALL, false); - onAddItem(R.string.filename, SortOrder.EPISODE_FILENAME_A_Z, SortOrder.EPISODE_FILENAME_Z_A, true); - onAddItem(R.string.random, SortOrder.RANDOM, SortOrder.RANDOM, true); - onAddItem(R.string.smart_shuffle, SortOrder.SMART_SHUFFLE_OLD_NEW, SortOrder.SMART_SHUFFLE_NEW_OLD, false); - } - - protected void onAddItem(int title, SortOrder ascending, SortOrder descending, boolean ascendingIsDefault) { - if (sortOrder == ascending || sortOrder == descending) { - SortDialogItemActiveBinding item = SortDialogItemActiveBinding.inflate( - getLayoutInflater(), viewBinding.gridLayout, false); - SortOrder other; - if (ascending == descending) { - item.button.setText(title); - other = ascending; - } else if (sortOrder == ascending) { - item.button.setText(getString(title) + "\u00A0▲"); - other = descending; - } else { - item.button.setText(getString(title) + "\u00A0▼"); - other = ascending; - } - item.button.setOnClickListener(v -> { - sortOrder = other; - populateList(); - onSelectionChanged(); - }); - viewBinding.gridLayout.addView(item.getRoot()); - } else { - SortDialogItemBinding item = SortDialogItemBinding.inflate( - getLayoutInflater(), viewBinding.gridLayout, false); - item.button.setText(title); - item.button.setOnClickListener(v -> { - sortOrder = ascendingIsDefault ? ascending : descending; - populateList(); - onSelectionChanged(); - }); - viewBinding.gridLayout.addView(item.getRoot()); - } - } - - protected void onSelectionChanged() { - } - - @NonNull - @Override - public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { - Dialog dialog = super.onCreateDialog(savedInstanceState); - dialog.setOnShowListener(dialogInterface -> { - BottomSheetDialog bottomSheetDialog = (BottomSheetDialog) dialogInterface; - setupFullHeight(bottomSheetDialog); - }); - return dialog; - } - - private void setupFullHeight(BottomSheetDialog bottomSheetDialog) { - FrameLayout bottomSheet = bottomSheetDialog.findViewById(R.id.design_bottom_sheet); - if (bottomSheet != null) { - BottomSheetBehavior behavior = BottomSheetBehavior.from(bottomSheet); - ViewGroup.LayoutParams layoutParams = bottomSheet.getLayoutParams(); - bottomSheet.setLayoutParams(layoutParams); - behavior.setState(BottomSheetBehavior.STATE_EXPANDED); - } - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/MediaPlayerErrorDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/MediaPlayerErrorDialog.java deleted file mode 100644 index 8425e0bfa..000000000 --- a/app/src/main/java/de/danoeh/antennapod/dialog/MediaPlayerErrorDialog.java +++ /dev/null @@ -1,31 +0,0 @@ -package de.danoeh.antennapod.dialog; - -import android.app.Activity; -import android.text.Spannable; -import android.text.SpannableString; -import android.text.style.ForegroundColorSpan; -import com.google.android.material.bottomsheet.BottomSheetBehavior; -import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.activity.MainActivity; -import de.danoeh.antennapod.event.PlayerErrorEvent; - -public class MediaPlayerErrorDialog { - public static void show(Activity activity, PlayerErrorEvent event) { - final MaterialAlertDialogBuilder errorDialog = new MaterialAlertDialogBuilder(activity); - errorDialog.setTitle(R.string.error_label); - - String genericMessage = activity.getString(R.string.playback_error_generic); - SpannableString errorMessage = new SpannableString(genericMessage + "\n\n" + event.getMessage()); - errorMessage.setSpan(new ForegroundColorSpan(0x88888888), - genericMessage.length(), errorMessage.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - - errorDialog.setMessage(errorMessage); - errorDialog.setPositiveButton(android.R.string.ok, (dialog, which) -> { - if (activity instanceof MainActivity) { - ((MainActivity) activity).getBottomSheet().setState(BottomSheetBehavior.STATE_COLLAPSED); - } - }); - errorDialog.create().show(); - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/PlaybackControlsDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/PlaybackControlsDialog.java deleted file mode 100644 index 97b161955..000000000 --- a/app/src/main/java/de/danoeh/antennapod/dialog/PlaybackControlsDialog.java +++ /dev/null @@ -1,78 +0,0 @@ -package de.danoeh.antennapod.dialog; - -import android.app.Dialog; -import android.os.Bundle; -import android.os.Handler; -import android.os.Looper; -import android.view.View; -import androidx.annotation.NonNull; -import androidx.appcompat.app.AlertDialog; -import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import androidx.fragment.app.DialogFragment; -import android.widget.Button; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.playback.service.PlaybackController; - -import java.util.List; - -public class PlaybackControlsDialog extends DialogFragment { - private PlaybackController controller; - private AlertDialog dialog; - - public static PlaybackControlsDialog newInstance() { - Bundle arguments = new Bundle(); - PlaybackControlsDialog dialog = new PlaybackControlsDialog(); - dialog.setArguments(arguments); - return dialog; - } - - public PlaybackControlsDialog() { - // Empty constructor required for DialogFragment - } - - @Override - public void onStart() { - super.onStart(); - controller = new PlaybackController(getActivity()) { - @Override - public void loadMediaInfo() { - setupAudioTracks(); - } - }; - controller.init(); - } - - @Override - public void onStop() { - super.onStop(); - controller.release(); - controller = null; - } - - @NonNull - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - dialog = new MaterialAlertDialogBuilder(getContext()) - .setTitle(R.string.audio_controls) - .setView(R.layout.audio_controls) - .setPositiveButton(R.string.close_label, null).create(); - return dialog; - } - - private void setupAudioTracks() { - List audioTracks = controller.getAudioTracks(); - int selectedAudioTrack = controller.getSelectedAudioTrack(); - final Button butAudioTracks = dialog.findViewById(R.id.audio_tracks); - if (audioTracks.size() < 2 || selectedAudioTrack < 0) { - butAudioTracks.setVisibility(View.GONE); - return; - } - - butAudioTracks.setVisibility(View.VISIBLE); - butAudioTracks.setText(audioTracks.get(selectedAudioTrack)); - butAudioTracks.setOnClickListener(v -> { - controller.setAudioTrack((selectedAudioTrack + 1) % audioTracks.size()); - new Handler(Looper.getMainLooper()).postDelayed(this::setupAudioTracks, 500); - }); - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/ProxyDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/ProxyDialog.java deleted file mode 100644 index 8c7a30bff..000000000 --- a/app/src/main/java/de/danoeh/antennapod/dialog/ProxyDialog.java +++ /dev/null @@ -1,316 +0,0 @@ -package de.danoeh.antennapod.dialog; - -import android.app.Dialog; -import android.content.Context; -import android.content.res.TypedArray; -import android.os.Build; -import androidx.appcompat.app.AlertDialog; -import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import android.text.Editable; -import android.text.TextUtils; -import android.text.TextWatcher; -import android.util.Patterns; -import android.view.View; -import android.widget.AdapterView; -import android.widget.ArrayAdapter; -import android.widget.EditText; -import android.widget.Spinner; -import android.widget.TextView; - -import java.io.IOException; -import java.net.InetSocketAddress; -import java.net.Proxy; -import java.net.SocketAddress; -import java.util.ArrayList; -import java.util.List; -import java.util.Locale; -import java.util.concurrent.TimeUnit; - -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.storage.preferences.UserPreferences; -import de.danoeh.antennapod.net.common.AntennapodHttpClient; -import de.danoeh.antennapod.model.download.ProxyConfig; -import de.danoeh.antennapod.ui.common.ThemeUtils; -import io.reactivex.Completable; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.Disposable; -import io.reactivex.schedulers.Schedulers; -import okhttp3.Credentials; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.Response; - -public class ProxyDialog { - private final Context context; - - private AlertDialog dialog; - - private Spinner spType; - private EditText etHost; - private EditText etPort; - private EditText etUsername; - private EditText etPassword; - - private boolean testSuccessful = false; - private TextView txtvMessage; - private Disposable disposable; - - public ProxyDialog(Context context) { - this.context = context; - } - - public Dialog show() { - View content = View.inflate(context, R.layout.proxy_settings, null); - spType = content.findViewById(R.id.spType); - - dialog = new MaterialAlertDialogBuilder(context) - .setTitle(R.string.pref_proxy_title) - .setView(content) - .setNegativeButton(R.string.cancel_label, null) - .setPositiveButton(R.string.proxy_test_label, null) - .setNeutralButton(R.string.reset, null) - .show(); - // To prevent cancelling the dialog on button click - dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener((view) -> { - if (!testSuccessful) { - dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false); - test(); - return; - } - setProxyConfig(); - AntennapodHttpClient.reinit(); - dialog.dismiss(); - }); - - dialog.getButton(AlertDialog.BUTTON_NEUTRAL).setOnClickListener((view) -> { - etHost.getText().clear(); - etPort.getText().clear(); - etUsername.getText().clear(); - etPassword.getText().clear(); - setProxyConfig(); - }); - - List types = new ArrayList<>(); - types.add(Proxy.Type.DIRECT.name()); - types.add(Proxy.Type.HTTP.name()); - if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - types.add(Proxy.Type.SOCKS.name()); - } - ArrayAdapter adapter = new ArrayAdapter<>(context, - android.R.layout.simple_spinner_item, types); - adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); - spType.setAdapter(adapter); - ProxyConfig proxyConfig = UserPreferences.getProxyConfig(); - spType.setSelection(adapter.getPosition(proxyConfig.type.name())); - etHost = content.findViewById(R.id.etHost); - if (!TextUtils.isEmpty(proxyConfig.host)) { - etHost.setText(proxyConfig.host); - } - etHost.addTextChangedListener(requireTestOnChange); - etPort = content.findViewById(R.id.etPort); - if (proxyConfig.port > 0) { - etPort.setText(String.valueOf(proxyConfig.port)); - } - etPort.addTextChangedListener(requireTestOnChange); - etUsername = content.findViewById(R.id.etUsername); - if (!TextUtils.isEmpty(proxyConfig.username)) { - etUsername.setText(proxyConfig.username); - } - etUsername.addTextChangedListener(requireTestOnChange); - etPassword = content.findViewById(R.id.etPassword); - if (!TextUtils.isEmpty(proxyConfig.password)) { - etPassword.setText(proxyConfig.password); - } - etPassword.addTextChangedListener(requireTestOnChange); - if (proxyConfig.type == Proxy.Type.DIRECT) { - enableSettings(false); - setTestRequired(false); - } - spType.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { - @Override - public void onItemSelected(AdapterView parent, View view, int position, long id) { - if (position == 0) { - dialog.getButton(AlertDialog.BUTTON_NEUTRAL).setVisibility(View.GONE); - } else { - dialog.getButton(AlertDialog.BUTTON_NEUTRAL).setVisibility(View.VISIBLE); - } - enableSettings(position > 0); - setTestRequired(position > 0); - } - - @Override - public void onNothingSelected(AdapterView parent) { - enableSettings(false); - } - }); - txtvMessage = content.findViewById(R.id.txtvMessage); - checkValidity(); - return dialog; - } - - private void setProxyConfig() { - final String type = (String) spType.getSelectedItem(); - final Proxy.Type typeEnum = Proxy.Type.valueOf(type); - final String host = etHost.getText().toString(); - final String port = etPort.getText().toString(); - - String username = etUsername.getText().toString(); - if (TextUtils.isEmpty(username)) { - username = null; - } - String password = etPassword.getText().toString(); - if (TextUtils.isEmpty(password)) { - password = null; - } - int portValue = 0; - if (!TextUtils.isEmpty(port)) { - portValue = Integer.parseInt(port); - } - ProxyConfig config = new ProxyConfig(typeEnum, host, portValue, username, password); - UserPreferences.setProxyConfig(config); - AntennapodHttpClient.setProxyConfig(config); - } - - private final TextWatcher requireTestOnChange = new TextWatcher() { - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) {} - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) {} - - @Override - public void afterTextChanged(Editable s) { - setTestRequired(true); - } - }; - - private void enableSettings(boolean enable) { - etHost.setEnabled(enable); - etPort.setEnabled(enable); - etUsername.setEnabled(enable); - etPassword.setEnabled(enable); - } - - private boolean checkValidity() { - boolean valid = true; - if (spType.getSelectedItemPosition() > 0) { - valid = checkHost(); - } - valid &= checkPort(); - return valid; - } - - private boolean checkHost() { - String host = etHost.getText().toString(); - if (host.length() == 0) { - etHost.setError(context.getString(R.string.proxy_host_empty_error)); - return false; - } - if (!"localhost".equals(host) && !Patterns.DOMAIN_NAME.matcher(host).matches()) { - etHost.setError(context.getString(R.string.proxy_host_invalid_error)); - return false; - } - return true; - } - - private boolean checkPort() { - int port = getPort(); - if (port < 0 || port > 65535) { - etPort.setError(context.getString(R.string.proxy_port_invalid_error)); - return false; - } - return true; - } - - private int getPort() { - String port = etPort.getText().toString(); - if (port.length() > 0) { - try { - return Integer.parseInt(port); - } catch (NumberFormatException e) { - // ignore - } - } - return 0; - } - - private void setTestRequired(boolean required) { - if (required) { - testSuccessful = false; - dialog.getButton(AlertDialog.BUTTON_POSITIVE).setText(R.string.proxy_test_label); - } else { - testSuccessful = true; - dialog.getButton(AlertDialog.BUTTON_POSITIVE).setText(android.R.string.ok); - } - dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(true); - } - - private void test() { - if (disposable != null) { - disposable.dispose(); - } - if (!checkValidity()) { - setTestRequired(true); - return; - } - TypedArray res = context.getTheme().obtainStyledAttributes(new int[] { android.R.attr.textColorPrimary }); - int textColorPrimary = res.getColor(0, 0); - res.recycle(); - txtvMessage.setTextColor(textColorPrimary); - txtvMessage.setText(R.string.proxy_checking); - txtvMessage.setVisibility(View.VISIBLE); - disposable = Completable.create(emitter -> { - String type = (String) spType.getSelectedItem(); - String host = etHost.getText().toString(); - String port = etPort.getText().toString(); - String username = etUsername.getText().toString(); - String password = etPassword.getText().toString(); - int portValue = 8080; - if (!TextUtils.isEmpty(port)) { - portValue = Integer.parseInt(port); - } - SocketAddress address = InetSocketAddress.createUnresolved(host, portValue); - Proxy.Type proxyType = Proxy.Type.valueOf(type.toUpperCase(Locale.US)); - OkHttpClient.Builder builder = AntennapodHttpClient.newBuilder() - .connectTimeout(10, TimeUnit.SECONDS) - .proxy(new Proxy(proxyType, address)); - if (!TextUtils.isEmpty(username)) { - builder.proxyAuthenticator((route, response) -> { - String credentials = Credentials.basic(username, password); - return response.request().newBuilder() - .header("Proxy-Authorization", credentials) - .build(); - }); - } - OkHttpClient client = builder.build(); - Request request = new Request.Builder().url("https://www.example.com").head().build(); - try (Response response = client.newCall(request).execute()) { - if (response.isSuccessful()) { - emitter.onComplete(); - } else { - emitter.onError(new IOException(response.message())); - } - } catch (IOException e) { - emitter.onError(e); - } - }) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - () -> { - txtvMessage.setTextColor(ThemeUtils.getColorFromAttr(context, R.attr.icon_green)); - txtvMessage.setText(R.string.proxy_test_successful); - setTestRequired(false); - }, - error -> { - error.printStackTrace(); - txtvMessage.setTextColor(ThemeUtils.getColorFromAttr(context, R.attr.icon_red)); - String message = String.format("%s: %s", - context.getString(R.string.proxy_test_failed), error.getMessage()); - txtvMessage.setText(message); - setTestRequired(true); - } - ); - } - -} diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/RemoveFeedDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/RemoveFeedDialog.java deleted file mode 100644 index ffa374b6f..000000000 --- a/app/src/main/java/de/danoeh/antennapod/dialog/RemoveFeedDialog.java +++ /dev/null @@ -1,84 +0,0 @@ -package de.danoeh.antennapod.dialog; - -import android.app.ProgressDialog; -import android.content.Context; -import android.content.DialogInterface; -import android.util.Log; - -import androidx.annotation.Nullable; - -import java.util.Collections; -import java.util.List; - -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.core.dialog.ConfirmationDialog; -import de.danoeh.antennapod.model.feed.Feed; -import de.danoeh.antennapod.storage.database.DBWriter; -import io.reactivex.Completable; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.schedulers.Schedulers; - -public class RemoveFeedDialog { - private static final String TAG = "RemoveFeedDialog"; - - public static void show(Context context, Feed feed, @Nullable Runnable callback) { - List feeds = Collections.singletonList(feed); - String message = getMessageId(context, feeds); - showDialog(context, feeds, message, callback); - } - - public static void show(Context context, List feeds) { - String message = getMessageId(context, feeds); - showDialog(context, feeds, message, null); - } - - private static void showDialog(Context context, List feeds, String message, @Nullable Runnable callback) { - ConfirmationDialog dialog = new ConfirmationDialog(context, R.string.remove_feed_label, message) { - @Override - public void onConfirmButtonPressed(DialogInterface clickedDialog) { - - if (callback != null) { - callback.run(); - } - - clickedDialog.dismiss(); - - ProgressDialog progressDialog = new ProgressDialog(context); - progressDialog.setMessage(context.getString(R.string.feed_remover_msg)); - progressDialog.setIndeterminate(true); - progressDialog.setCancelable(false); - progressDialog.show(); - - Completable.fromAction(() -> { - for (Feed feed : feeds) { - DBWriter.deleteFeed(context, feed.getId()).get(); - } - }) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - () -> { - Log.d(TAG, "Feed(s) deleted"); - progressDialog.dismiss(); - }, error -> { - Log.e(TAG, Log.getStackTraceString(error)); - progressDialog.dismiss(); - }); - } - }; - dialog.createNewDialog().show(); - } - - private static String getMessageId(Context context, List feeds) { - if (feeds.size() == 1) { - if (feeds.get(0).isLocalFeed()) { - return context.getString(R.string.feed_delete_confirmation_local_msg, feeds.get(0).getTitle()); - } else { - return context.getString(R.string.feed_delete_confirmation_msg, feeds.get(0).getTitle()); - } - } else { - return context.getString(R.string.feed_delete_confirmation_msg_batch); - } - - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/RenameItemDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/RenameItemDialog.java deleted file mode 100644 index bbe6fd16c..000000000 --- a/app/src/main/java/de/danoeh/antennapod/dialog/RenameItemDialog.java +++ /dev/null @@ -1,81 +0,0 @@ -package de.danoeh.antennapod.dialog; - -import android.app.Activity; - -import java.lang.ref.WeakReference; -import java.util.ArrayList; -import java.util.List; - -import android.view.LayoutInflater; -import androidx.appcompat.app.AlertDialog; -import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.storage.database.NavDrawerData; -import de.danoeh.antennapod.model.feed.Feed; -import de.danoeh.antennapod.storage.database.DBWriter; -import de.danoeh.antennapod.databinding.EditTextDialogBinding; -import de.danoeh.antennapod.model.feed.FeedPreferences; - -public class RenameItemDialog { - - private final WeakReference activityRef; - private Feed feed = null; - private NavDrawerData.DrawerItem drawerItem = null; - - public RenameItemDialog(Activity activity, Feed feed) { - this.activityRef = new WeakReference<>(activity); - this.feed = feed; - } - - public RenameItemDialog(Activity activity, NavDrawerData.DrawerItem drawerItem) { - this.activityRef = new WeakReference<>(activity); - this.drawerItem = drawerItem; - } - - public void show() { - Activity activity = activityRef.get(); - if (activity == null) { - return; - } - - final EditTextDialogBinding binding = EditTextDialogBinding.inflate(LayoutInflater.from(activity)); - String title = feed != null ? feed.getTitle() : drawerItem.getTitle(); - - binding.urlEditText.setText(title); - AlertDialog dialog = new MaterialAlertDialogBuilder(activity) - .setView(binding.getRoot()) - .setTitle(feed != null ? R.string.rename_feed_label : R.string.rename_tag_label) - .setPositiveButton(android.R.string.ok, (d, input) -> { - String newTitle = binding.urlEditText.getText().toString(); - if (feed != null) { - feed.setCustomTitle(newTitle); - DBWriter.setFeedCustomTitle(feed); - } else { - renameTag(newTitle); - } - }) - .setNeutralButton(de.danoeh.antennapod.core.R.string.reset, null) - .setNegativeButton(de.danoeh.antennapod.core.R.string.cancel_label, null) - .show(); - - // To prevent cancelling the dialog on button click - dialog.getButton(AlertDialog.BUTTON_NEUTRAL).setOnClickListener( - (view) -> binding.urlEditText.setText(title)); - } - - private void renameTag(String title) { - if (NavDrawerData.DrawerItem.Type.TAG == drawerItem.type) { - List feedPreferences = new ArrayList<>(); - for (NavDrawerData.DrawerItem item : ((NavDrawerData.TagDrawerItem) drawerItem).children) { - feedPreferences.add(((NavDrawerData.FeedDrawerItem) item).feed.getPreferences()); - } - - for (FeedPreferences preferences : feedPreferences) { - preferences.getTags().remove(drawerItem.getTitle()); - preferences.getTags().add(title); - DBWriter.setFeedPreferences(preferences); - } - } - } - -} diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/ShareDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/ShareDialog.java deleted file mode 100644 index 5fb8a352f..000000000 --- a/app/src/main/java/de/danoeh/antennapod/dialog/ShareDialog.java +++ /dev/null @@ -1,100 +0,0 @@ -package de.danoeh.antennapod.dialog; - -import android.content.Context; -import android.content.SharedPreferences; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import com.google.android.material.bottomsheet.BottomSheetDialogFragment; -import de.danoeh.antennapod.core.util.ShareUtils; -import de.danoeh.antennapod.databinding.ShareEpisodeDialogBinding; -import de.danoeh.antennapod.model.feed.FeedItem; - -public class ShareDialog extends BottomSheetDialogFragment { - private static final String ARGUMENT_FEED_ITEM = "feedItem"; - private static final String PREF_NAME = "ShareDialog"; - private static final String PREF_SHARE_EPISODE_START_AT = "prefShareEpisodeStartAt"; - private static final String PREF_SHARE_EPISODE_TYPE = "prefShareEpisodeType"; - - private Context ctx; - private FeedItem item; - private SharedPreferences prefs; - - private ShareEpisodeDialogBinding viewBinding; - - public ShareDialog() { - // Empty constructor required for DialogFragment - } - - public static ShareDialog newInstance(FeedItem item) { - Bundle arguments = new Bundle(); - arguments.putSerializable(ARGUMENT_FEED_ITEM, item); - ShareDialog dialog = new ShareDialog(); - dialog.setArguments(arguments); - return dialog; - } - - @Nullable - @Override - public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, - @Nullable Bundle savedInstanceState) { - if (getArguments() != null) { - ctx = getActivity(); - item = (FeedItem) getArguments().getSerializable(ARGUMENT_FEED_ITEM); - prefs = getActivity().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); - } - - viewBinding = ShareEpisodeDialogBinding.inflate(inflater); - viewBinding.shareDialogRadioGroup.setOnCheckedChangeListener((group, checkedId) -> - viewBinding.sharePositionCheckbox.setEnabled(checkedId == viewBinding.shareSocialRadio.getId())); - - setupOptions(); - - viewBinding.shareButton.setOnClickListener((v) -> { - boolean includePlaybackPosition = viewBinding.sharePositionCheckbox.isChecked(); - int position; - if (viewBinding.shareSocialRadio.isChecked()) { - ShareUtils.shareFeedItemLinkWithDownloadLink(ctx, item, includePlaybackPosition); - position = 1; - } else if (viewBinding.shareMediaReceiverRadio.isChecked()) { - ShareUtils.shareMediaDownloadLink(ctx, item.getMedia()); - position = 2; - } else if (viewBinding.shareMediaFileRadio.isChecked()) { - ShareUtils.shareFeedItemFile(ctx, item.getMedia()); - position = 3; - } else { - throw new IllegalStateException("Unknown share method"); - } - prefs.edit() - .putBoolean(PREF_SHARE_EPISODE_START_AT, includePlaybackPosition) - .putInt(PREF_SHARE_EPISODE_TYPE, position) - .apply(); - dismiss(); - }); - return viewBinding.getRoot(); - } - - private void setupOptions() { - final boolean hasMedia = item.getMedia() != null; - boolean downloaded = hasMedia && item.getMedia().isDownloaded(); - viewBinding.shareMediaFileRadio.setVisibility(downloaded ? View.VISIBLE : View.GONE); - - boolean hasDownloadUrl = hasMedia && item.getMedia().getDownloadUrl() != null; - if (!hasDownloadUrl) { - viewBinding.shareMediaReceiverRadio.setVisibility(View.GONE); - } - int type = prefs.getInt(PREF_SHARE_EPISODE_TYPE, 1); - if ((type == 2 && !hasDownloadUrl) || (type == 3 && !downloaded)) { - type = 1; - } - viewBinding.shareSocialRadio.setChecked(type == 1); - viewBinding.shareMediaReceiverRadio.setChecked(type == 2); - viewBinding.shareMediaFileRadio.setChecked(type == 3); - - boolean switchIsChecked = prefs.getBoolean(PREF_SHARE_EPISODE_START_AT, false); - viewBinding.sharePositionCheckbox.setChecked(switchIsChecked); - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/SkipPreferenceDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/SkipPreferenceDialog.java deleted file mode 100644 index a5eca4bc2..000000000 --- a/app/src/main/java/de/danoeh/antennapod/dialog/SkipPreferenceDialog.java +++ /dev/null @@ -1,64 +0,0 @@ -package de.danoeh.antennapod.dialog; - -import android.content.Context; -import android.widget.TextView; -import androidx.appcompat.app.AlertDialog; -import com.google.android.material.dialog.MaterialAlertDialogBuilder; - -import java.text.NumberFormat; -import java.util.Locale; - -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.storage.preferences.UserPreferences; - -/** - * Shows the dialog that allows setting the skip time. - */ -public class SkipPreferenceDialog { - public static void showSkipPreference(Context context, SkipDirection direction, TextView textView) { - int checked = 0; - - int skipSecs; - if (direction == SkipDirection.SKIP_FORWARD) { - skipSecs = UserPreferences.getFastForwardSecs(); - } else { - skipSecs = UserPreferences.getRewindSecs(); - } - - final int[] values = context.getResources().getIntArray(R.array.seek_delta_values); - final String[] choices = new String[values.length]; - for (int i = 0; i < values.length; i++) { - if (skipSecs == values[i]) { - checked = i; - } - choices[i] = String.format(Locale.getDefault(), - "%d %s", values[i], context.getString(R.string.time_seconds)); - } - - MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(context); - builder.setTitle(direction == SkipDirection.SKIP_FORWARD ? R.string.pref_fast_forward : R.string.pref_rewind); - builder.setSingleChoiceItems(choices, checked, (dialog, which) -> { - int choice = ((AlertDialog) dialog).getListView().getCheckedItemPosition(); - if (choice < 0 || choice >= values.length) { - System.err.printf("Choice in showSkipPreference is out of bounds %d", choice); - } else { - int seconds = values[choice]; - if (direction == SkipDirection.SKIP_FORWARD) { - UserPreferences.setFastForwardSecs(seconds); - } else { - UserPreferences.setRewindSecs(seconds); - } - if (textView != null) { - textView.setText(NumberFormat.getInstance().format(seconds)); - } - dialog.dismiss(); - } - }); - builder.setNegativeButton(R.string.cancel_label, null); - builder.show(); - } - - public enum SkipDirection { - SKIP_FORWARD, SKIP_REWIND - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/SleepTimerDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/SleepTimerDialog.java deleted file mode 100644 index 5525ee8c2..000000000 --- a/app/src/main/java/de/danoeh/antennapod/dialog/SleepTimerDialog.java +++ /dev/null @@ -1,214 +0,0 @@ -package de.danoeh.antennapod.dialog; - -import android.app.Activity; -import android.app.Dialog; -import android.content.Context; -import android.os.Bundle; -import android.text.format.DateFormat; -import android.view.View; -import android.view.inputmethod.InputMethodManager; -import android.widget.Button; -import android.widget.CheckBox; -import android.widget.EditText; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.fragment.app.DialogFragment; - -import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import com.google.android.material.snackbar.Snackbar; - -import de.danoeh.antennapod.playback.service.PlaybackController; -import de.danoeh.antennapod.playback.service.PlaybackService; -import org.greenrobot.eventbus.EventBus; -import org.greenrobot.eventbus.Subscribe; -import org.greenrobot.eventbus.ThreadMode; - -import java.util.Locale; - -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.storage.preferences.SleepTimerPreferences; -import de.danoeh.antennapod.ui.common.Converter; -import de.danoeh.antennapod.event.playback.SleepTimerUpdatedEvent; - -public class SleepTimerDialog extends DialogFragment { - private PlaybackController controller; - private EditText etxtTime; - private LinearLayout timeSetup; - private LinearLayout timeDisplay; - private TextView time; - private CheckBox chAutoEnable; - - public SleepTimerDialog() { - - } - - @Override - public void onStart() { - super.onStart(); - controller = new PlaybackController(getActivity()) { - @Override - public void loadMediaInfo() { - } - }; - controller.init(); - EventBus.getDefault().register(this); - } - - @Override - public void onStop() { - super.onStop(); - if (controller != null) { - controller.release(); - } - EventBus.getDefault().unregister(this); - } - - @NonNull - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - View content = View.inflate(getContext(), R.layout.time_dialog, null); - MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(getContext()); - builder.setTitle(R.string.sleep_timer_label); - builder.setView(content); - builder.setPositiveButton(R.string.close_label, null); - - etxtTime = content.findViewById(R.id.etxtTime); - timeSetup = content.findViewById(R.id.timeSetup); - timeDisplay = content.findViewById(R.id.timeDisplay); - timeDisplay.setVisibility(View.GONE); - time = content.findViewById(R.id.time); - Button extendSleepFiveMinutesButton = content.findViewById(R.id.extendSleepFiveMinutesButton); - extendSleepFiveMinutesButton.setText(getString(R.string.extend_sleep_timer_label, 5)); - Button extendSleepTenMinutesButton = content.findViewById(R.id.extendSleepTenMinutesButton); - extendSleepTenMinutesButton.setText(getString(R.string.extend_sleep_timer_label, 10)); - Button extendSleepTwentyMinutesButton = content.findViewById(R.id.extendSleepTwentyMinutesButton); - extendSleepTwentyMinutesButton.setText(getString(R.string.extend_sleep_timer_label, 20)); - extendSleepFiveMinutesButton.setOnClickListener(v -> { - if (controller != null) { - controller.extendSleepTimer(5 * 1000 * 60); - } - }); - extendSleepTenMinutesButton.setOnClickListener(v -> { - if (controller != null) { - controller.extendSleepTimer(10 * 1000 * 60); - } - }); - extendSleepTwentyMinutesButton.setOnClickListener(v -> { - if (controller != null) { - controller.extendSleepTimer(20 * 1000 * 60); - } - }); - - etxtTime.setText(SleepTimerPreferences.lastTimerValue()); - etxtTime.postDelayed(() -> { - InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); - imm.showSoftInput(etxtTime, InputMethodManager.SHOW_IMPLICIT); - }, 100); - - final CheckBox cbShakeToReset = content.findViewById(R.id.cbShakeToReset); - final CheckBox cbVibrate = content.findViewById(R.id.cbVibrate); - chAutoEnable = content.findViewById(R.id.chAutoEnable); - final ImageView changeTimesButton = content.findViewById(R.id.changeTimesButton); - - cbShakeToReset.setChecked(SleepTimerPreferences.shakeToReset()); - cbVibrate.setChecked(SleepTimerPreferences.vibrate()); - chAutoEnable.setChecked(SleepTimerPreferences.autoEnable()); - changeTimesButton.setEnabled(chAutoEnable.isChecked()); - changeTimesButton.setAlpha(chAutoEnable.isChecked() ? 1.0f : 0.5f); - - cbShakeToReset.setOnCheckedChangeListener((buttonView, isChecked) - -> SleepTimerPreferences.setShakeToReset(isChecked)); - cbVibrate.setOnCheckedChangeListener((buttonView, isChecked) - -> SleepTimerPreferences.setVibrate(isChecked)); - chAutoEnable.setOnCheckedChangeListener((compoundButton, isChecked) - -> { - SleepTimerPreferences.setAutoEnable(isChecked); - changeTimesButton.setEnabled(isChecked); - changeTimesButton.setAlpha(isChecked ? 1.0f : 0.5f); - }); - updateAutoEnableText(); - - changeTimesButton.setOnClickListener(changeTimesBtn -> { - int from = SleepTimerPreferences.autoEnableFrom(); - int to = SleepTimerPreferences.autoEnableTo(); - showTimeRangeDialog(getContext(), from, to); - }); - - Button disableButton = content.findViewById(R.id.disableSleeptimerButton); - disableButton.setOnClickListener(v -> { - if (controller != null) { - controller.disableSleepTimer(); - } - }); - Button setButton = content.findViewById(R.id.setSleeptimerButton); - setButton.setOnClickListener(v -> { - if (!PlaybackService.isRunning) { - Snackbar.make(content, R.string.no_media_playing_label, Snackbar.LENGTH_LONG).show(); - return; - } - try { - long time = Long.parseLong(etxtTime.getText().toString()); - if (time == 0) { - throw new NumberFormatException("Timer must not be zero"); - } - SleepTimerPreferences.setLastTimer(etxtTime.getText().toString()); - if (controller != null) { - controller.setSleepTimer(SleepTimerPreferences.timerMillis()); - } - closeKeyboard(content); - } catch (NumberFormatException e) { - e.printStackTrace(); - Snackbar.make(content, R.string.time_dialog_invalid_input, Snackbar.LENGTH_LONG).show(); - } - }); - return builder.create(); - } - - private void showTimeRangeDialog(Context context, int from, int to) { - TimeRangeDialog dialog = new TimeRangeDialog(context, from, to); - dialog.setOnDismissListener(v -> { - SleepTimerPreferences.setAutoEnableFrom(dialog.getFrom()); - SleepTimerPreferences.setAutoEnableTo(dialog.getTo()); - updateAutoEnableText(); - }); - dialog.show(); - } - - private void updateAutoEnableText() { - String text; - int from = SleepTimerPreferences.autoEnableFrom(); - int to = SleepTimerPreferences.autoEnableTo(); - - if (from == to) { - text = getString(R.string.auto_enable_label); - } else if (DateFormat.is24HourFormat(getContext())) { - String formattedFrom = String.format(Locale.getDefault(), "%02d:00", from); - String formattedTo = String.format(Locale.getDefault(), "%02d:00", to); - text = getString(R.string.auto_enable_label_with_times, formattedFrom, formattedTo); - } else { - String formattedFrom = String.format(Locale.getDefault(), "%02d:00 %s", - from % 12, from >= 12 ? "PM" : "AM"); - String formattedTo = String.format(Locale.getDefault(), "%02d:00 %s", - to % 12, to >= 12 ? "PM" : "AM"); - text = getString(R.string.auto_enable_label_with_times, formattedFrom, formattedTo); - - } - chAutoEnable.setText(text); - } - - @Subscribe(threadMode = ThreadMode.MAIN) - @SuppressWarnings("unused") - public void timerUpdated(SleepTimerUpdatedEvent event) { - timeDisplay.setVisibility(event.isOver() || event.isCancelled() ? View.GONE : View.VISIBLE); - timeSetup.setVisibility(event.isOver() || event.isCancelled() ? View.VISIBLE : View.GONE); - time.setText(Converter.getDurationStringLong((int) event.getTimeLeft())); - } - - private void closeKeyboard(View content) { - InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Activity.INPUT_METHOD_SERVICE); - imm.hideSoftInputFromWindow(content.getWindowToken(), 0); - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/StreamingConfirmationDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/StreamingConfirmationDialog.java deleted file mode 100644 index 5b733138f..000000000 --- a/app/src/main/java/de/danoeh/antennapod/dialog/StreamingConfirmationDialog.java +++ /dev/null @@ -1,38 +0,0 @@ -package de.danoeh.antennapod.dialog; - -import android.content.Context; -import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.playback.service.PlaybackServiceStarter; -import de.danoeh.antennapod.storage.preferences.UserPreferences; -import de.danoeh.antennapod.model.playback.Playable; - -public class StreamingConfirmationDialog { - private final Context context; - private final Playable playable; - - public StreamingConfirmationDialog(Context context, Playable playable) { - this.context = context; - this.playable = playable; - } - - public void show() { - new MaterialAlertDialogBuilder(context) - .setTitle(R.string.stream_label) - .setMessage(R.string.confirm_mobile_streaming_notification_message) - .setPositiveButton(R.string.confirm_mobile_streaming_button_once, (dialog, which) -> stream()) - .setNegativeButton(R.string.confirm_mobile_streaming_button_always, (dialog, which) -> { - UserPreferences.setAllowMobileStreaming(true); - stream(); - }) - .setNeutralButton(R.string.cancel_label, null) - .show(); - } - - private void stream() { - new PlaybackServiceStarter(context, playable) - .callEvenIfRunning(true) - .shouldStreamThisTime(true) - .start(); - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/SubscriptionsFilterDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/SubscriptionsFilterDialog.java deleted file mode 100644 index 929e6b1ad..000000000 --- a/app/src/main/java/de/danoeh/antennapod/dialog/SubscriptionsFilterDialog.java +++ /dev/null @@ -1,133 +0,0 @@ -package de.danoeh.antennapod.dialog; - -import android.app.Dialog; -import android.os.Bundle; -import android.text.TextUtils; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.Button; -import android.widget.FrameLayout; -import android.widget.LinearLayout; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import com.google.android.material.bottomsheet.BottomSheetBehavior; -import com.google.android.material.bottomsheet.BottomSheetDialog; -import com.google.android.material.bottomsheet.BottomSheetDialogFragment; -import com.google.android.material.button.MaterialButtonToggleGroup; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.core.feed.SubscriptionsFilterGroup; -import de.danoeh.antennapod.databinding.FilterDialogBinding; -import de.danoeh.antennapod.databinding.FilterDialogRowBinding; -import de.danoeh.antennapod.event.UnreadItemsUpdateEvent; -import de.danoeh.antennapod.model.feed.SubscriptionsFilter; -import de.danoeh.antennapod.storage.preferences.UserPreferences; -import org.greenrobot.eventbus.EventBus; - -import java.util.Arrays; -import java.util.Collections; -import java.util.HashSet; -import java.util.Set; - -public class SubscriptionsFilterDialog extends BottomSheetDialogFragment { - private LinearLayout rows; - - @Nullable - @Override - public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, - @Nullable Bundle savedInstanceState) { - SubscriptionsFilter subscriptionsFilter = UserPreferences.getSubscriptionsFilter(); - FilterDialogBinding dialogBinding = FilterDialogBinding.inflate(inflater); - rows = dialogBinding.filterRows; - - for (SubscriptionsFilterGroup item : SubscriptionsFilterGroup.values()) { - FilterDialogRowBinding binding = FilterDialogRowBinding.inflate(inflater); - binding.getRoot().addOnButtonCheckedListener( - (group, checkedId, isChecked) -> updateFilter(getFilterValues())); - binding.buttonGroup.setWeightSum(item.values.length); - binding.filterButton1.setText(item.values[0].displayName); - binding.filterButton1.setTag(item.values[0].filterId); - if (item.values.length == 2) { - binding.filterButton2.setText(item.values[1].displayName); - binding.filterButton2.setTag(item.values[1].filterId); - } else { - binding.filterButton2.setVisibility(View.GONE); - } - binding.filterButton1.setMaxLines(3); - binding.filterButton1.setSingleLine(false); - binding.filterButton2.setMaxLines(3); - binding.filterButton2.setSingleLine(false); - rows.addView(binding.getRoot(), rows.getChildCount() - 1); - } - - final Set filterValues = new HashSet<>(Arrays.asList(subscriptionsFilter.getValues())); - for (String filterId : filterValues) { - if (!TextUtils.isEmpty(filterId)) { - Button button = dialogBinding.getRoot().findViewWithTag(filterId); - if (button != null) { - ((MaterialButtonToggleGroup) button.getParent()).check(button.getId()); - } - } - } - - dialogBinding.confirmFiltermenu.setOnClickListener(view -> { - updateFilter(getFilterValues()); - dismiss(); - }); - dialogBinding.resetFiltermenu.setOnClickListener(view -> { - updateFilter(Collections.emptySet()); - for (int i = 0; i < rows.getChildCount(); i++) { - if (rows.getChildAt(i) instanceof MaterialButtonToggleGroup) { - ((MaterialButtonToggleGroup) rows.getChildAt(i)).clearChecked(); - } - } - }); - return dialogBinding.getRoot(); - } - - private Set getFilterValues() { - Set filterValues = new HashSet<>(); - for (int i = 0; i < rows.getChildCount(); i++) { - if (!(rows.getChildAt(i) instanceof MaterialButtonToggleGroup)) { - continue; - } - MaterialButtonToggleGroup group = (MaterialButtonToggleGroup) rows.getChildAt(i); - if (group.getCheckedButtonId() == View.NO_ID) { - continue; - } - String tag = (String) group.findViewById(group.getCheckedButtonId()).getTag(); - if (tag == null) { // Clear buttons use no tag - continue; - } - filterValues.add(tag); - } - return filterValues; - } - - @NonNull - @Override - public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { - Dialog dialog = super.onCreateDialog(savedInstanceState); - dialog.setOnShowListener(dialogInterface -> { - BottomSheetDialog bottomSheetDialog = (BottomSheetDialog) dialogInterface; - setupFullHeight(bottomSheetDialog); - }); - return dialog; - } - - private void setupFullHeight(BottomSheetDialog bottomSheetDialog) { - FrameLayout bottomSheet = bottomSheetDialog.findViewById(R.id.design_bottom_sheet); - if (bottomSheet != null) { - BottomSheetBehavior behavior = BottomSheetBehavior.from(bottomSheet); - ViewGroup.LayoutParams layoutParams = bottomSheet.getLayoutParams(); - bottomSheet.setLayoutParams(layoutParams); - behavior.setState(BottomSheetBehavior.STATE_EXPANDED); - } - } - - private static void updateFilter(Set filterValues) { - SubscriptionsFilter subscriptionsFilter = new SubscriptionsFilter(filterValues.toArray(new String[0])); - UserPreferences.setSubscriptionsFilter(subscriptionsFilter); - EventBus.getDefault().post(new UnreadItemsUpdateEvent()); - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/SwipeActionsDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/SwipeActionsDialog.java deleted file mode 100644 index 2cd03e21d..000000000 --- a/app/src/main/java/de/danoeh/antennapod/dialog/SwipeActionsDialog.java +++ /dev/null @@ -1,223 +0,0 @@ -package de.danoeh.antennapod.dialog; - -import android.content.Context; -import android.content.SharedPreferences; -import android.graphics.PorterDuff; -import android.graphics.drawable.Drawable; -import android.view.LayoutInflater; -import android.view.View; - -import androidx.appcompat.app.AlertDialog; -import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import androidx.appcompat.content.res.AppCompatResources; -import androidx.core.graphics.drawable.DrawableCompat; -import androidx.gridlayout.widget.GridLayout; - -import java.util.ArrayList; -import java.util.List; - -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.databinding.FeeditemlistItemBinding; -import de.danoeh.antennapod.databinding.SwipeactionsDialogBinding; -import de.danoeh.antennapod.databinding.SwipeactionsPickerBinding; -import de.danoeh.antennapod.databinding.SwipeactionsPickerItemBinding; -import de.danoeh.antennapod.databinding.SwipeactionsRowBinding; -import de.danoeh.antennapod.fragment.AllEpisodesFragment; -import de.danoeh.antennapod.fragment.CompletedDownloadsFragment; -import de.danoeh.antennapod.fragment.FeedItemlistFragment; -import de.danoeh.antennapod.fragment.InboxFragment; -import de.danoeh.antennapod.fragment.PlaybackHistoryFragment; -import de.danoeh.antennapod.fragment.QueueFragment; -import de.danoeh.antennapod.fragment.swipeactions.AddToQueueSwipeAction; -import de.danoeh.antennapod.fragment.swipeactions.DeleteSwipeAction; -import de.danoeh.antennapod.fragment.swipeactions.MarkFavoriteSwipeAction; -import de.danoeh.antennapod.fragment.swipeactions.RemoveFromHistorySwipeAction; -import de.danoeh.antennapod.fragment.swipeactions.RemoveFromInboxSwipeAction; -import de.danoeh.antennapod.fragment.swipeactions.RemoveFromQueueSwipeAction; -import de.danoeh.antennapod.fragment.swipeactions.StartDownloadSwipeAction; -import de.danoeh.antennapod.fragment.swipeactions.SwipeAction; -import de.danoeh.antennapod.fragment.swipeactions.SwipeActions; -import de.danoeh.antennapod.fragment.swipeactions.TogglePlaybackStateSwipeAction; -import de.danoeh.antennapod.ui.common.ThemeUtils; - -public class SwipeActionsDialog { - private static final int LEFT = 1; - private static final int RIGHT = 0; - - private final Context context; - private final String tag; - - private SwipeAction rightAction; - private SwipeAction leftAction; - private List keys; - - public SwipeActionsDialog(Context context, String tag) { - this.context = context; - this.tag = tag; - } - - public void show(Callback prefsChanged) { - SwipeActions.Actions actions = SwipeActions.getPrefsWithDefaults(context, tag); - leftAction = actions.left; - rightAction = actions.right; - - final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(context); - - keys = new ArrayList<>(); - if (tag.equals(QueueFragment.TAG)) { - keys.add(new RemoveFromQueueSwipeAction()); - } else { - keys.add(new AddToQueueSwipeAction()); - } - if (!tag.equals(CompletedDownloadsFragment.TAG)) { - keys.add(new StartDownloadSwipeAction()); - } - if (!tag.equals(CompletedDownloadsFragment.TAG) - && ! tag.equals(QueueFragment.TAG) - && !tag.equals(PlaybackHistoryFragment.TAG)) { - keys.add(new RemoveFromInboxSwipeAction()); - } - if (!tag.equals(InboxFragment.TAG)) { - keys.add(new DeleteSwipeAction()); - } - keys.add(new MarkFavoriteSwipeAction()); - if (tag.equals(PlaybackHistoryFragment.TAG)) { - keys.add(new RemoveFromHistorySwipeAction()); - } - if (!tag.equals(InboxFragment.TAG)) { - keys.add(new TogglePlaybackStateSwipeAction()); - } - - String forFragment = ""; - switch (tag) { - case InboxFragment.TAG: - forFragment = context.getString(R.string.inbox_label); - break; - case AllEpisodesFragment.TAG: - forFragment = context.getString(R.string.episodes_label); - break; - case CompletedDownloadsFragment.TAG: - forFragment = context.getString(R.string.downloads_label); - break; - case FeedItemlistFragment.TAG: - forFragment = context.getString(R.string.individual_subscription); - break; - case QueueFragment.TAG: - forFragment = context.getString(R.string.queue_label); - break; - case PlaybackHistoryFragment.TAG: - forFragment = context.getString(R.string.playback_history_label); - break; - default: break; - } - - builder.setTitle(context.getString(R.string.swipeactions_label) + " - " + forFragment); - SwipeactionsDialogBinding viewBinding = SwipeactionsDialogBinding.inflate(LayoutInflater.from(context)); - builder.setView(viewBinding.getRoot()); - - viewBinding.enableSwitch.setOnCheckedChangeListener((compoundButton, b) -> { - viewBinding.actionLeftContainer.getRoot().setAlpha(b ? 1.0f : 0.4f); - viewBinding.actionRightContainer.getRoot().setAlpha(b ? 1.0f : 0.4f); - }); - - viewBinding.enableSwitch.setChecked(SwipeActions.isSwipeActionEnabled(context, tag)); - - setupSwipeDirectionView(viewBinding.actionLeftContainer, LEFT); - setupSwipeDirectionView(viewBinding.actionRightContainer, RIGHT); - - builder.setPositiveButton(R.string.confirm_label, (dialog, which) -> { - savePrefs(tag, rightAction.getId(), leftAction.getId()); - saveActionsEnabledPrefs(viewBinding.enableSwitch.isChecked()); - prefsChanged.onCall(); - }); - - builder.setNegativeButton(R.string.cancel_label, null); - builder.create().show(); - } - - private void setupSwipeDirectionView(SwipeactionsRowBinding view, int direction) { - SwipeAction action = direction == LEFT ? leftAction : rightAction; - - view.swipeDirectionLabel.setText(direction == LEFT ? R.string.swipe_left : R.string.swipe_right); - view.swipeActionLabel.setText(action.getTitle(context)); - populateMockEpisode(view.mockEpisode); - if (direction == RIGHT && view.previewContainer.getChildAt(0) != view.swipeIcon) { - view.previewContainer.removeView(view.swipeIcon); - view.previewContainer.addView(view.swipeIcon, 0); - } - - view.swipeIcon.setImageResource(action.getActionIcon()); - view.swipeIcon.setColorFilter(ThemeUtils.getColorFromAttr(context, action.getActionColor())); - - view.changeButton.setOnClickListener(v -> showPicker(view, direction)); - view.previewContainer.setOnClickListener(v -> showPicker(view, direction)); - } - - private void showPicker(SwipeactionsRowBinding view, int direction) { - MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(context); - builder.setTitle(direction == LEFT ? R.string.swipe_left : R.string.swipe_right); - - SwipeactionsPickerBinding picker = SwipeactionsPickerBinding.inflate(LayoutInflater.from(context)); - builder.setView(picker.getRoot()); - builder.setNegativeButton(R.string.cancel_label, null); - AlertDialog dialog = builder.show(); - - for (int i = 0; i < keys.size(); i++) { - final int actionIndex = i; - SwipeAction action = keys.get(actionIndex); - SwipeactionsPickerItemBinding item = SwipeactionsPickerItemBinding.inflate(LayoutInflater.from(context)); - item.swipeActionLabel.setText(action.getTitle(context)); - - Drawable icon = DrawableCompat.wrap(AppCompatResources.getDrawable(context, action.getActionIcon())); - icon.mutate(); - icon.setTintMode(PorterDuff.Mode.SRC_ATOP); - if ((direction == LEFT && leftAction == action) || (direction == RIGHT && rightAction == action)) { - icon.setTint(ThemeUtils.getColorFromAttr(context, action.getActionColor())); - item.swipeActionLabel.setTextColor(ThemeUtils.getColorFromAttr(context, action.getActionColor())); - } else { - icon.setTint(ThemeUtils.getColorFromAttr(context, R.attr.action_icon_color)); - } - item.swipeIcon.setImageDrawable(icon); - - item.getRoot().setOnClickListener(v -> { - if (direction == LEFT) { - leftAction = keys.get(actionIndex); - } else { - rightAction = keys.get(actionIndex); - } - setupSwipeDirectionView(view, direction); - dialog.dismiss(); - }); - GridLayout.LayoutParams param = new GridLayout.LayoutParams( - GridLayout.spec(GridLayout.UNDEFINED, GridLayout.BASELINE), - GridLayout.spec(GridLayout.UNDEFINED, GridLayout.FILL, 1f)); - param.width = 0; - picker.pickerGridLayout.addView(item.getRoot(), param); - } - picker.pickerGridLayout.setColumnCount(2); - picker.pickerGridLayout.setRowCount((keys.size() + 1) / 2); - } - - private void populateMockEpisode(FeeditemlistItemBinding view) { - view.container.setAlpha(0.3f); - view.secondaryActionButton.secondaryActionButton.setVisibility(View.GONE); - view.dragHandle.setVisibility(View.GONE); - view.statusInbox.setVisibility(View.GONE); - view.txtvTitle.setText("███████"); - view.txtvPosition.setText("█████"); - } - - private void savePrefs(String tag, String right, String left) { - SharedPreferences prefs = context.getSharedPreferences(SwipeActions.PREF_NAME, Context.MODE_PRIVATE); - prefs.edit().putString(SwipeActions.KEY_PREFIX_SWIPEACTIONS + tag, right + "," + left).apply(); - } - - private void saveActionsEnabledPrefs(Boolean enabled) { - SharedPreferences prefs = context.getSharedPreferences(SwipeActions.PREF_NAME, Context.MODE_PRIVATE); - prefs.edit().putBoolean(SwipeActions.KEY_PREFIX_NO_ACTION + tag, enabled).apply(); - } - - public interface Callback { - void onCall(); - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/TagSettingsDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/TagSettingsDialog.java deleted file mode 100644 index 8112c0955..000000000 --- a/app/src/main/java/de/danoeh/antennapod/dialog/TagSettingsDialog.java +++ /dev/null @@ -1,156 +0,0 @@ -package de.danoeh.antennapod.dialog; - -import android.app.Dialog; -import android.os.Bundle; -import android.text.TextUtils; -import android.util.Log; -import android.view.MotionEvent; -import android.view.View; -import android.widget.ArrayAdapter; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.fragment.app.DialogFragment; -import androidx.recyclerview.widget.GridLayoutManager; - -import com.google.android.material.dialog.MaterialAlertDialogBuilder; - -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.adapter.SimpleChipAdapter; -import de.danoeh.antennapod.storage.database.DBReader; -import de.danoeh.antennapod.storage.database.DBWriter; -import de.danoeh.antennapod.storage.database.NavDrawerData; -import de.danoeh.antennapod.databinding.EditTagsDialogBinding; -import de.danoeh.antennapod.model.feed.FeedCounter; -import de.danoeh.antennapod.model.feed.FeedOrder; -import de.danoeh.antennapod.model.feed.FeedPreferences; -import de.danoeh.antennapod.view.ItemOffsetDecoration; -import io.reactivex.Observable; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.schedulers.Schedulers; - -public class TagSettingsDialog extends DialogFragment { - public static final String TAG = "TagSettingsDialog"; - private static final String ARG_FEED_PREFERENCES = "feed_preferences"; - private List displayedTags; - private EditTagsDialogBinding viewBinding; - private SimpleChipAdapter adapter; - - public static TagSettingsDialog newInstance(List preferencesList) { - TagSettingsDialog fragment = new TagSettingsDialog(); - Bundle args = new Bundle(); - args.putSerializable(ARG_FEED_PREFERENCES, new ArrayList<>(preferencesList)); - fragment.setArguments(args); - return fragment; - } - - @NonNull - @Override - public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { - ArrayList feedPreferencesList = - (ArrayList) getArguments().getSerializable(ARG_FEED_PREFERENCES); - Set commonTags = new HashSet<>(feedPreferencesList.get(0).getTags()); - - for (FeedPreferences preference : feedPreferencesList) { - commonTags.retainAll(preference.getTags()); - } - displayedTags = new ArrayList<>(commonTags); - displayedTags.remove(FeedPreferences.TAG_ROOT); - - viewBinding = EditTagsDialogBinding.inflate(getLayoutInflater()); - viewBinding.tagsRecycler.setLayoutManager(new GridLayoutManager(getContext(), 2)); - viewBinding.tagsRecycler.addItemDecoration(new ItemOffsetDecoration(getContext(), 4)); - adapter = new SimpleChipAdapter(getContext()) { - @Override - protected List getChips() { - return displayedTags; - } - - @Override - protected void onRemoveClicked(int position) { - displayedTags.remove(position); - notifyDataSetChanged(); - } - }; - viewBinding.tagsRecycler.setAdapter(adapter); - viewBinding.rootFolderCheckbox.setChecked(commonTags.contains(FeedPreferences.TAG_ROOT)); - - viewBinding.newTagTextInput.setEndIconOnClickListener(v -> - addTag(viewBinding.newTagEditText.getText().toString().trim())); - - loadTags(); - viewBinding.newTagEditText.setThreshold(1); - viewBinding.newTagEditText.setOnTouchListener(new View.OnTouchListener() { - @Override - public boolean onTouch(View v, MotionEvent event) { - viewBinding.newTagEditText.showDropDown(); - viewBinding.newTagEditText.requestFocus(); - return false; - } - }); - - if (feedPreferencesList.size() > 1) { - viewBinding.commonTagsInfo.setVisibility(View.VISIBLE); - } - - MaterialAlertDialogBuilder dialog = new MaterialAlertDialogBuilder(getContext()); - dialog.setView(viewBinding.getRoot()); - dialog.setTitle(R.string.feed_tags_label); - dialog.setPositiveButton(android.R.string.ok, (d, input) -> { - addTag(viewBinding.newTagEditText.getText().toString().trim()); - updatePreferencesTags(feedPreferencesList, commonTags); - }); - dialog.setNegativeButton(R.string.cancel_label, null); - return dialog.create(); - } - - private void loadTags() { - Observable.fromCallable( - () -> { - NavDrawerData data = DBReader.getNavDrawerData(null, FeedOrder.ALPHABETICAL, FeedCounter.SHOW_NONE); - List items = data.items; - List folders = new ArrayList(); - for (NavDrawerData.DrawerItem item : items) { - if (item.type == NavDrawerData.DrawerItem.Type.TAG) { - folders.add(item.getTitle()); - } - } - return folders; - }) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - result -> { - ArrayAdapter acAdapter = new ArrayAdapter(getContext(), - R.layout.single_tag_text_view, result); - viewBinding.newTagEditText.setAdapter(acAdapter); - }, error -> { - Log.e(TAG, Log.getStackTraceString(error)); - }); - } - - private void addTag(String name) { - if (TextUtils.isEmpty(name) || displayedTags.contains(name)) { - return; - } - displayedTags.add(name); - viewBinding.newTagEditText.setText(""); - adapter.notifyDataSetChanged(); - } - - private void updatePreferencesTags(List feedPreferencesList, Set commonTags) { - if (viewBinding.rootFolderCheckbox.isChecked()) { - displayedTags.add(FeedPreferences.TAG_ROOT); - } - for (FeedPreferences preferences : feedPreferencesList) { - preferences.getTags().removeAll(commonTags); - preferences.getTags().addAll(displayedTags); - DBWriter.setFeedPreferences(preferences); - } - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/TimeRangeDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/TimeRangeDialog.java deleted file mode 100644 index 1d84c7c22..000000000 --- a/app/src/main/java/de/danoeh/antennapod/dialog/TimeRangeDialog.java +++ /dev/null @@ -1,187 +0,0 @@ -package de.danoeh.antennapod.dialog; - -import android.content.Context; -import android.graphics.Canvas; -import android.graphics.Paint; -import android.graphics.Point; -import android.graphics.RectF; -import android.text.format.DateFormat; -import android.view.MotionEvent; -import android.view.View; -import androidx.annotation.NonNull; -import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.ui.common.ThemeUtils; - -import java.util.Locale; - -public class TimeRangeDialog extends MaterialAlertDialogBuilder { - private final TimeRangeView view; - - public TimeRangeDialog(@NonNull Context context, int from, int to) { - super(context); - view = new TimeRangeView(context, from, to); - setView(view); - setPositiveButton(android.R.string.ok, null); - } - - public int getFrom() { - return view.from; - } - - public int getTo() { - return view.to; - } - - static class TimeRangeView extends View { - private static final int DIAL_ALPHA = 120; - private final Paint paintDial = new Paint(); - private final Paint paintSelected = new Paint(); - private final Paint paintText = new Paint(); - private int from; - private int to; - private final RectF bounds = new RectF(); - int touching = 0; - - public TimeRangeView(Context context) { // Used by Android tools - this(context, 0, 0); - } - - public TimeRangeView(Context context, int from, int to) { - super(context); - this.from = from; - this.to = to; - setup(); - } - - private void setup() { - paintDial.setAntiAlias(true); - paintDial.setStyle(Paint.Style.STROKE); - paintDial.setStrokeCap(Paint.Cap.ROUND); - paintDial.setColor(ThemeUtils.getColorFromAttr(getContext(), android.R.attr.textColorPrimary)); - paintDial.setAlpha(DIAL_ALPHA); - - paintSelected.setAntiAlias(true); - paintSelected.setStyle(Paint.Style.STROKE); - paintSelected.setStrokeCap(Paint.Cap.ROUND); - paintSelected.setColor(ThemeUtils.getColorFromAttr(getContext(), R.attr.colorAccent)); - - paintText.setAntiAlias(true); - paintText.setStyle(Paint.Style.FILL); - paintText.setColor(ThemeUtils.getColorFromAttr(getContext(), android.R.attr.textColorPrimary)); - paintText.setTextAlign(Paint.Align.CENTER); - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - if (MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY - && MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY) { - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - } else if (MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY) { - super.onMeasure(widthMeasureSpec, widthMeasureSpec); - } else if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY) { - super.onMeasure(heightMeasureSpec, heightMeasureSpec); - } else if (MeasureSpec.getSize(widthMeasureSpec) < MeasureSpec.getSize(heightMeasureSpec)) { - super.onMeasure(widthMeasureSpec, widthMeasureSpec); - } else { - super.onMeasure(heightMeasureSpec, heightMeasureSpec); - } - } - - @Override - protected void onDraw(Canvas canvas) { - super.onDraw(canvas); - - float size = getHeight(); // square - float padding = size * 0.1f; - paintDial.setStrokeWidth(size * 0.005f); - bounds.set(padding, padding, size - padding, size - padding); - - paintText.setAlpha(DIAL_ALPHA); - canvas.drawArc(bounds, 0, 360, false, paintDial); - for (int i = 0; i < 24; i++) { - paintDial.setStrokeWidth(size * 0.005f); - if (i % 6 == 0) { - paintDial.setStrokeWidth(size * 0.01f); - Point textPos = radToPoint(i / 24.0f * 360.f, size / 2 - 2.5f * padding); - paintText.setTextSize(0.4f * padding); - canvas.drawText(String.valueOf(i), textPos.x, - textPos.y + (-paintText.descent() - paintText.ascent()) / 2, paintText); - } - Point outer = radToPoint(i / 24.0f * 360.f, size / 2 - 1.7f * padding); - Point inner = radToPoint(i / 24.0f * 360.f, size / 2 - 1.9f * padding); - canvas.drawLine(outer.x, outer.y, inner.x, inner.y, paintDial); - } - paintText.setAlpha(255); - - float angleFrom = (float) from / 24 * 360 - 90; - float angleDistance = (float) ((to - from + 24) % 24) / 24 * 360; - paintSelected.setStrokeWidth(padding / 6); - paintSelected.setStyle(Paint.Style.STROKE); - canvas.drawArc(bounds, angleFrom, angleDistance, false, paintSelected); - paintSelected.setStyle(Paint.Style.FILL); - Point p1 = radToPoint(angleFrom + 90, size / 2 - padding); - canvas.drawCircle(p1.x, p1.y, padding / 2, paintSelected); - Point p2 = radToPoint(angleFrom + angleDistance + 90, size / 2 - padding); - canvas.drawCircle(p2.x, p2.y, padding / 2, paintSelected); - - paintText.setTextSize(0.6f * padding); - String timeRange; - if (from == to) { - timeRange = getContext().getString(R.string.sleep_timer_always); - } else if (DateFormat.is24HourFormat(getContext())) { - timeRange = String.format(Locale.getDefault(), "%02d:00 - %02d:00", from, to); - } else { - timeRange = String.format(Locale.getDefault(), "%02d:00 %s - %02d:00 %s", from % 12, - from >= 12 ? "PM" : "AM", to % 12, to >= 12 ? "PM" : "AM"); - } - canvas.drawText(timeRange, size / 2, (size - paintText.descent() - paintText.ascent()) / 2, paintText); - } - - protected Point radToPoint(float angle, float radius) { - return new Point((int) (getWidth() / 2.0 + radius * Math.sin(-angle * Math.PI / 180.0 + Math.PI)), - (int) (getHeight() / 2.0 + radius * Math.cos(-angle * Math.PI / 180.0 + Math.PI))); - } - - @Override - public boolean onTouchEvent(MotionEvent event) { - getParent().requestDisallowInterceptTouchEvent(true); - Point center = new Point(getWidth() / 2, getHeight() / 2); - double angleRad = Math.atan2(center.y - event.getY(), center.x - event.getX()); - float angle = (float) (angleRad * (180 / Math.PI)); - angle += 360 + 360 - 90; - angle %= 360; - - if (event.getAction() == MotionEvent.ACTION_DOWN) { - float fromDistance = Math.abs(angle - (float) from / 24 * 360); - float toDistance = Math.abs(angle - (float) to / 24 * 360); - if (fromDistance < 15 || fromDistance > (360 - 15)) { - touching = 1; - return true; - } else if (toDistance < 15 || toDistance > (360 - 15)) { - touching = 2; - return true; - } - } else if (event.getAction() == MotionEvent.ACTION_MOVE) { - int newTime = (int) (24 * (angle / 360.0)); - if (from == to && touching != 0) { - // Switch which handle is focussed such that selection is the smaller arc - touching = (((newTime - to + 24) % 24) < 12) ? 2 : 1; - } - if (touching == 1) { - from = newTime; - invalidate(); - return true; - } else if (touching == 2) { - to = newTime; - invalidate(); - return true; - } - } else if (touching != 0) { - touching = 0; - return true; - } - return super.onTouchEvent(event); - } - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/VariableSpeedDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/VariableSpeedDialog.java deleted file mode 100644 index ee5777d35..000000000 --- a/app/src/main/java/de/danoeh/antennapod/dialog/VariableSpeedDialog.java +++ /dev/null @@ -1,181 +0,0 @@ -package de.danoeh.antennapod.dialog; - -import android.os.Bundle; -import android.os.Handler; -import android.os.Looper; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.CheckBox; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.recyclerview.widget.GridLayoutManager; -import androidx.recyclerview.widget.RecyclerView; -import com.google.android.material.bottomsheet.BottomSheetDialogFragment; -import com.google.android.material.chip.Chip; -import com.google.android.material.snackbar.Snackbar; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.event.playback.SpeedChangedEvent; -import de.danoeh.antennapod.playback.service.PlaybackController; -import de.danoeh.antennapod.storage.preferences.UserPreferences; -import de.danoeh.antennapod.view.ItemOffsetDecoration; -import de.danoeh.antennapod.view.PlaybackSpeedSeekBar; -import org.greenrobot.eventbus.EventBus; -import org.greenrobot.eventbus.Subscribe; -import org.greenrobot.eventbus.ThreadMode; - -import java.text.DecimalFormatSymbols; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Locale; - -public class VariableSpeedDialog extends BottomSheetDialogFragment { - private SpeedSelectionAdapter adapter; - private PlaybackController controller; - private final List selectedSpeeds; - private PlaybackSpeedSeekBar speedSeekBar; - private Chip addCurrentSpeedChip; - private CheckBox skipSilenceCheckbox; - - public VariableSpeedDialog() { - DecimalFormatSymbols format = new DecimalFormatSymbols(Locale.US); - format.setDecimalSeparator('.'); - selectedSpeeds = new ArrayList<>(UserPreferences.getPlaybackSpeedArray()); - } - - @Override - public void onStart() { - super.onStart(); - controller = new PlaybackController(getActivity()) { - @Override - public void loadMediaInfo() { - updateSpeed(new SpeedChangedEvent(controller.getCurrentPlaybackSpeedMultiplier())); - updateSkipSilence(controller.getCurrentPlaybackSkipSilence()); - } - }; - controller.init(); - EventBus.getDefault().register(this); - updateSpeed(new SpeedChangedEvent(controller.getCurrentPlaybackSpeedMultiplier())); - updateSkipSilence(controller.getCurrentPlaybackSkipSilence()); - } - - @Override - public void onStop() { - super.onStop(); - controller.release(); - controller = null; - EventBus.getDefault().unregister(this); - } - - @Subscribe(threadMode = ThreadMode.MAIN) - public void updateSpeed(SpeedChangedEvent event) { - speedSeekBar.updateSpeed(event.getNewSpeed()); - addCurrentSpeedChip.setText(String.format(Locale.getDefault(), "%1$.2f", event.getNewSpeed())); - } - - public void updateSkipSilence(boolean skipSilence) { - skipSilenceCheckbox.setChecked(skipSilence); - } - - @Nullable - @Override - public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, - @Nullable Bundle savedInstanceState) { - View root = View.inflate(getContext(), R.layout.speed_select_dialog, null); - speedSeekBar = root.findViewById(R.id.speed_seek_bar); - speedSeekBar.setProgressChangedListener(multiplier -> { - UserPreferences.setPlaybackSpeed(multiplier); - if (controller != null) { - controller.setPlaybackSpeed(multiplier); - } - }); - RecyclerView selectedSpeedsGrid = root.findViewById(R.id.selected_speeds_grid); - selectedSpeedsGrid.setLayoutManager(new GridLayoutManager(getContext(), 3)); - selectedSpeedsGrid.addItemDecoration(new ItemOffsetDecoration(getContext(), 4)); - adapter = new SpeedSelectionAdapter(); - adapter.setHasStableIds(true); - selectedSpeedsGrid.setAdapter(adapter); - - addCurrentSpeedChip = root.findViewById(R.id.add_current_speed_chip); - addCurrentSpeedChip.setCloseIconVisible(true); - addCurrentSpeedChip.setCloseIconResource(R.drawable.ic_add); - addCurrentSpeedChip.setOnCloseIconClickListener(v -> addCurrentSpeed()); - addCurrentSpeedChip.setCloseIconContentDescription(getString(R.string.add_preset)); - addCurrentSpeedChip.setOnClickListener(v -> addCurrentSpeed()); - - skipSilenceCheckbox = root.findViewById(R.id.skipSilence); - skipSilenceCheckbox.setChecked(UserPreferences.isSkipSilence()); - skipSilenceCheckbox.setOnCheckedChangeListener((buttonView, isChecked) -> { - UserPreferences.setSkipSilence(isChecked); - controller.setSkipSilence(isChecked); - }); - return root; - } - - private void addCurrentSpeed() { - float newSpeed = speedSeekBar.getCurrentSpeed(); - if (selectedSpeeds.contains(newSpeed)) { - Snackbar.make(addCurrentSpeedChip, - getString(R.string.preset_already_exists, newSpeed), Snackbar.LENGTH_LONG).show(); - } else { - selectedSpeeds.add(newSpeed); - Collections.sort(selectedSpeeds); - UserPreferences.setPlaybackSpeedArray(selectedSpeeds); - adapter.notifyDataSetChanged(); - } - } - - public class SpeedSelectionAdapter extends RecyclerView.Adapter { - - @Override - @NonNull - public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - Chip chip = new Chip(getContext()); - chip.setTextAlignment(View.TEXT_ALIGNMENT_CENTER); - return new ViewHolder(chip); - } - - @Override - public void onBindViewHolder(@NonNull ViewHolder holder, int position) { - float speed = selectedSpeeds.get(position); - - holder.chip.setText(String.format(Locale.getDefault(), "%1$.2f", speed)); - holder.chip.setOnLongClickListener(v -> { - selectedSpeeds.remove(speed); - UserPreferences.setPlaybackSpeedArray(selectedSpeeds); - notifyDataSetChanged(); - return true; - }); - holder.chip.setOnClickListener(v -> { - UserPreferences.setPlaybackSpeed(speed); - new Handler(Looper.getMainLooper()).postDelayed(() -> { - if (controller != null) { - controller.setPlaybackSpeed(speed); - dismiss(); - } - }, 200); - }); - } - - @Override - public int getItemCount() { - return selectedSpeeds.size(); - } - - @Override - public long getItemId(int position) { - return selectedSpeeds.get(position).hashCode(); - } - - public class ViewHolder extends RecyclerView.ViewHolder { - Chip chip; - - ViewHolder(Chip itemView) { - super(itemView); - chip = itemView; - } - } - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/rating/RatingDialogFragment.java b/app/src/main/java/de/danoeh/antennapod/dialog/rating/RatingDialogFragment.java deleted file mode 100644 index 4e9da8b9a..000000000 --- a/app/src/main/java/de/danoeh/antennapod/dialog/rating/RatingDialogFragment.java +++ /dev/null @@ -1,79 +0,0 @@ -package de.danoeh.antennapod.dialog.rating; - -import android.app.Dialog; -import android.content.DialogInterface; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.text.HtmlCompat; -import androidx.fragment.app.DialogFragment; -import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.core.util.IntentUtils; -import de.danoeh.antennapod.databinding.RatingDialogBinding; -import de.danoeh.antennapod.ui.common.DateFormatter; - -import java.util.Date; - -public class RatingDialogFragment extends DialogFragment { - private static final String EXTRA_TOTAL_TIME = "totalTime"; - private static final String EXTRA_OLDEST_DATE = "oldestDate"; - - public static RatingDialogFragment newInstance(long totalTime, long oldestDate) { - RatingDialogFragment fragment = new RatingDialogFragment(); - Bundle arguments = new Bundle(); - arguments.putLong(EXTRA_TOTAL_TIME, totalTime); - arguments.putLong(EXTRA_OLDEST_DATE, oldestDate); - fragment.setArguments(arguments); - return fragment; - } - - @NonNull - @Override - public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { - return new MaterialAlertDialogBuilder(getContext()) - .setView(onCreateView(getLayoutInflater(), null, savedInstanceState)) - .create(); - } - - @Nullable - @Override - public View onCreateView(@NonNull LayoutInflater inflater, - @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - RatingDialogBinding viewBinding = RatingDialogBinding.inflate(inflater); - long totalTime = getArguments().getLong(EXTRA_TOTAL_TIME, 0); - long oldestDate = getArguments().getLong(EXTRA_OLDEST_DATE, 0); - - viewBinding.headerLabel.setText(HtmlCompat.fromHtml(getString(R.string.rating_tagline, - DateFormatter.formatAbbrev(getContext(), new Date(oldestDate)), - "
", totalTime / 3600L, - "
"), HtmlCompat.FROM_HTML_MODE_LEGACY)); - viewBinding.neverAgainButton.setOnClickListener(v -> { - new RatingDialogManager(getActivity()).saveRated(); - dismiss(); - }); - viewBinding.showLaterButton.setOnClickListener(v -> { - new RatingDialogManager(getActivity()).resetStartDate(); - dismiss(); - }); - viewBinding.rateButton.setOnClickListener(v -> { - IntentUtils.openInBrowser(getContext(), - "https://play.google.com/store/apps/details?id=de.danoeh.antennapod"); - new RatingDialogManager(getActivity()).saveRated(); - }); - viewBinding.contibuteButton.setOnClickListener(v -> { - IntentUtils.openInBrowser(getContext(), IntentUtils.getLocalizedWebsiteLink(getContext()) + "/contribute/"); - new RatingDialogManager(getActivity()).saveRated(); - }); - return viewBinding.getRoot(); - } - - @Override - public void onCancel(@NonNull DialogInterface dialog) { - super.onCancel(dialog); - new RatingDialogManager(getActivity()).resetStartDate(); - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/rating/RatingDialogManager.java b/app/src/main/java/de/danoeh/antennapod/dialog/rating/RatingDialogManager.java deleted file mode 100644 index 53e1c8c72..000000000 --- a/app/src/main/java/de/danoeh/antennapod/dialog/rating/RatingDialogManager.java +++ /dev/null @@ -1,94 +0,0 @@ -package de.danoeh.antennapod.dialog.rating; - -import android.content.Context; -import android.content.SharedPreferences; - -import java.util.concurrent.TimeUnit; - -import android.util.Log; -import androidx.fragment.app.FragmentActivity; -import de.danoeh.antennapod.core.BuildConfig; -import de.danoeh.antennapod.storage.database.DBReader; -import de.danoeh.antennapod.storage.database.StatisticsItem; -import io.reactivex.Observable; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.Disposable; -import io.reactivex.schedulers.Schedulers; -import kotlin.Pair; - -public class RatingDialogManager { - private static final int AFTER_DAYS = 20; - private static final String TAG = "RatingDialog"; - private static final String PREFS_NAME = "RatingPrefs"; - private static final String KEY_RATED = "KEY_WAS_RATED"; - private static final String KEY_FIRST_START_DATE = "KEY_FIRST_HIT_DATE"; - - private final SharedPreferences preferences; - private final FragmentActivity fragmentActivity; - private Disposable disposable; - - public RatingDialogManager(FragmentActivity activity) { - this.fragmentActivity = activity; - preferences = activity.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); - } - - public void showIfNeeded() { - //noinspection ConstantConditions - if (isRated() || BuildConfig.DEBUG || "free".equals(BuildConfig.FLAVOR)) { - return; - } else if (!enoughTimeSinceInstall()) { - return; - } - - if (disposable != null) { - disposable.dispose(); - } - disposable = Observable.fromCallable( - () -> { - DBReader.StatisticsResult statisticsData = DBReader.getStatistics(false, 0, Long.MAX_VALUE); - long totalTime = 0; - for (StatisticsItem item : statisticsData.feedTime) { - totalTime += item.timePlayed; - } - return new Pair<>(totalTime, statisticsData.oldestDate); - }) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(result -> { - long totalTime = result.getFirst(); - long oldestDate = result.getSecond(); - if (totalTime < TimeUnit.SECONDS.convert(15, TimeUnit.HOURS)) { - return; - } else if (oldestDate > System.currentTimeMillis() - - TimeUnit.MILLISECONDS.convert(AFTER_DAYS, TimeUnit.DAYS)) { - return; // In case the app was opened but nothing was played - } - RatingDialogFragment.newInstance(result.getFirst(), result.getSecond()) - .show(fragmentActivity.getSupportFragmentManager(), TAG); - }, error -> Log.e(TAG, Log.getStackTraceString(error))); - } - - private boolean isRated() { - return preferences.getBoolean(KEY_RATED, false); - } - - public void saveRated() { - preferences.edit().putBoolean(KEY_RATED, true).apply(); - } - - public void resetStartDate() { - preferences.edit().putLong(KEY_FIRST_START_DATE, System.currentTimeMillis()).apply(); - } - - private boolean enoughTimeSinceInstall() { - if (preferences.getLong(KEY_FIRST_START_DATE, 0) == 0) { - resetStartDate(); - return false; - } - long now = System.currentTimeMillis(); - long firstDate = preferences.getLong(KEY_FIRST_START_DATE, now); - long diff = now - firstDate; - long diffDays = TimeUnit.DAYS.convert(diff, TimeUnit.MILLISECONDS); - return diffDays >= AFTER_DAYS; - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/error/CrashReportWriter.java b/app/src/main/java/de/danoeh/antennapod/error/CrashReportWriter.java deleted file mode 100644 index 356fd34ee..000000000 --- a/app/src/main/java/de/danoeh/antennapod/error/CrashReportWriter.java +++ /dev/null @@ -1,67 +0,0 @@ -package de.danoeh.antennapod.error; - -import android.os.Build; -import android.util.Log; - -import de.danoeh.antennapod.BuildConfig; -import org.apache.commons.io.IOUtils; - -import java.io.File; -import java.io.IOException; -import java.io.PrintWriter; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.Locale; - -import de.danoeh.antennapod.storage.preferences.UserPreferences; - -public class CrashReportWriter implements Thread.UncaughtExceptionHandler { - - private static final String TAG = "CrashReportWriter"; - - private final Thread.UncaughtExceptionHandler defaultHandler; - - public CrashReportWriter() { - defaultHandler = Thread.getDefaultUncaughtExceptionHandler(); - } - - public static File getFile() { - return new File(UserPreferences.getDataFolder(null), "crash-report.log"); - } - - @Override - public void uncaughtException(Thread thread, Throwable ex) { - write(ex); - defaultHandler.uncaughtException(thread, ex); - } - - public static void write(Throwable exception) { - File path = getFile(); - PrintWriter out = null; - try { - out = new PrintWriter(path, "UTF-8"); - out.println("## Crash info"); - out.println("Time: " + new SimpleDateFormat("dd-MM-yyyy HH:mm:ss", Locale.getDefault()).format(new Date())); - out.println("AntennaPod version: " + BuildConfig.VERSION_NAME); - out.println(); - out.println("## StackTrace"); - out.println("```"); - exception.printStackTrace(out); - out.println("```"); - } catch (IOException e) { - Log.e(TAG, Log.getStackTraceString(e)); - } finally { - IOUtils.closeQuietly(out); - } - } - - public static String getSystemInfo() { - return "## Environment" - + "\nAndroid version: " + Build.VERSION.RELEASE - + "\nOS version: " + System.getProperty("os.version") - + "\nAntennaPod version: " + BuildConfig.VERSION_NAME - + "\nModel: " + Build.MODEL - + "\nDevice: " + Build.DEVICE - + "\nProduct: " + Build.PRODUCT; - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/error/RxJavaErrorHandlerSetup.java b/app/src/main/java/de/danoeh/antennapod/error/RxJavaErrorHandlerSetup.java deleted file mode 100644 index 1c7f5f0b4..000000000 --- a/app/src/main/java/de/danoeh/antennapod/error/RxJavaErrorHandlerSetup.java +++ /dev/null @@ -1,36 +0,0 @@ -package de.danoeh.antennapod.error; - -import android.util.Log; -import de.danoeh.antennapod.BuildConfig; -import io.reactivex.exceptions.UndeliverableException; -import io.reactivex.plugins.RxJavaPlugins; - -public class RxJavaErrorHandlerSetup { - private static final String TAG = "RxJavaErrorHandler"; - - private RxJavaErrorHandlerSetup() { - - } - - public static void setupRxJavaErrorHandler() { - RxJavaPlugins.setErrorHandler(exception -> { - if (exception instanceof UndeliverableException) { - // Probably just disposed because the fragment was left - Log.d(TAG, "Ignored exception: " + Log.getStackTraceString(exception)); - return; - } - - // Usually, undeliverable exceptions are wrapped in an UndeliverableException. - // If an undeliverable exception is a NPE (or some others), wrapping does not happen. - // AntennaPod threads might throw NPEs after disposing because we set controllers to null. - // Just swallow all exceptions here. - Log.e(TAG, Log.getStackTraceString(exception)); - CrashReportWriter.write(exception); - - if (BuildConfig.DEBUG) { - Thread.currentThread().getUncaughtExceptionHandler() - .uncaughtException(Thread.currentThread(), exception); - } - }); - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/AddFeedFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/AddFeedFragment.java deleted file mode 100644 index 8fe5e1c18..000000000 --- a/app/src/main/java/de/danoeh/antennapod/fragment/AddFeedFragment.java +++ /dev/null @@ -1,216 +0,0 @@ -package de.danoeh.antennapod.fragment; - -import android.content.ActivityNotFoundException; -import android.content.ClipData; -import android.content.ClipboardManager; -import android.content.Context; -import android.content.Intent; -import android.net.Uri; -import android.os.Bundle; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.view.inputmethod.InputMethodManager; - -import androidx.activity.result.ActivityResultLauncher; -import androidx.activity.result.contract.ActivityResultContracts; -import androidx.activity.result.contract.ActivityResultContracts.GetContent; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import androidx.documentfile.provider.DocumentFile; -import androidx.fragment.app.Fragment; -import com.google.android.material.snackbar.Snackbar; - -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.activity.MainActivity; -import de.danoeh.antennapod.activity.OpmlImportActivity; -import de.danoeh.antennapod.model.feed.Feed; -import de.danoeh.antennapod.net.download.serviceinterface.FeedUpdateManager; -import de.danoeh.antennapod.storage.database.FeedDatabaseWriter; -import de.danoeh.antennapod.model.feed.SortOrder; -import de.danoeh.antennapod.databinding.AddfeedBinding; -import de.danoeh.antennapod.databinding.EditTextDialogBinding; -import de.danoeh.antennapod.net.discovery.CombinedSearcher; -import de.danoeh.antennapod.net.discovery.FyydPodcastSearcher; -import de.danoeh.antennapod.net.discovery.ItunesPodcastSearcher; -import de.danoeh.antennapod.net.discovery.PodcastIndexPodcastSearcher; -import de.danoeh.antennapod.ui.appstartintent.OnlineFeedviewActivityStarter; -import de.danoeh.antennapod.ui.discovery.OnlineSearchFragment; -import io.reactivex.Observable; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.schedulers.Schedulers; - -import java.util.Collections; - -/** - * Provides actions for adding new podcast subscriptions. - */ -public class AddFeedFragment extends Fragment { - - public static final String TAG = "AddFeedFragment"; - private static final String KEY_UP_ARROW = "up_arrow"; - - private AddfeedBinding viewBinding; - private MainActivity activity; - private boolean displayUpArrow; - - private final ActivityResultLauncher chooseOpmlImportPathLauncher = - registerForActivityResult(new GetContent(), this::chooseOpmlImportPathResult); - private final ActivityResultLauncher addLocalFolderLauncher = - registerForActivityResult(new AddLocalFolder(), this::addLocalFolderResult); - - @Override - @Nullable - public View onCreateView(@NonNull LayoutInflater inflater, - @Nullable ViewGroup container, - @Nullable Bundle savedInstanceState) { - super.onCreateView(inflater, container, savedInstanceState); - viewBinding = AddfeedBinding.inflate(inflater); - activity = (MainActivity) getActivity(); - - displayUpArrow = getParentFragmentManager().getBackStackEntryCount() != 0; - if (savedInstanceState != null) { - displayUpArrow = savedInstanceState.getBoolean(KEY_UP_ARROW); - } - ((MainActivity) getActivity()).setupToolbarToggle(viewBinding.toolbar, displayUpArrow); - - viewBinding.searchItunesButton.setOnClickListener(v - -> activity.loadChildFragment(OnlineSearchFragment.newInstance(ItunesPodcastSearcher.class))); - viewBinding.searchFyydButton.setOnClickListener(v - -> activity.loadChildFragment(OnlineSearchFragment.newInstance(FyydPodcastSearcher.class))); - viewBinding.searchPodcastIndexButton.setOnClickListener(v - -> activity.loadChildFragment(OnlineSearchFragment.newInstance(PodcastIndexPodcastSearcher.class))); - - viewBinding.combinedFeedSearchEditText.setOnEditorActionListener((v, actionId, event) -> { - performSearch(); - return true; - }); - - viewBinding.addViaUrlButton.setOnClickListener(v - -> showAddViaUrlDialog()); - - viewBinding.opmlImportButton.setOnClickListener(v -> { - try { - chooseOpmlImportPathLauncher.launch("*/*"); - } catch (ActivityNotFoundException e) { - e.printStackTrace(); - ((MainActivity) getActivity()) - .showSnackbarAbovePlayer(R.string.unable_to_start_system_file_manager, Snackbar.LENGTH_LONG); - } - }); - - viewBinding.addLocalFolderButton.setOnClickListener(v -> { - try { - addLocalFolderLauncher.launch(null); - } catch (ActivityNotFoundException e) { - e.printStackTrace(); - ((MainActivity) getActivity()) - .showSnackbarAbovePlayer(R.string.unable_to_start_system_file_manager, Snackbar.LENGTH_LONG); - } - }); - viewBinding.searchButton.setOnClickListener(view -> performSearch()); - - return viewBinding.getRoot(); - } - - @Override - public void onSaveInstanceState(@NonNull Bundle outState) { - outState.putBoolean(KEY_UP_ARROW, displayUpArrow); - super.onSaveInstanceState(outState); - } - - private void showAddViaUrlDialog() { - MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(getContext()); - builder.setTitle(R.string.add_podcast_by_url); - final EditTextDialogBinding dialogBinding = EditTextDialogBinding.inflate(getLayoutInflater()); - dialogBinding.urlEditText.setHint(R.string.add_podcast_by_url_hint); - - ClipboardManager clipboard = (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE); - final ClipData clipData = clipboard.getPrimaryClip(); - if (clipData != null && clipData.getItemCount() > 0 && clipData.getItemAt(0).getText() != null) { - final String clipboardContent = clipData.getItemAt(0).getText().toString(); - if (clipboardContent.trim().startsWith("http")) { - dialogBinding.urlEditText.setText(clipboardContent.trim()); - } - } - builder.setView(dialogBinding.getRoot()); - builder.setPositiveButton(R.string.confirm_label, - (dialog, which) -> addUrl(dialogBinding.urlEditText.getText().toString())); - builder.setNegativeButton(R.string.cancel_label, null); - builder.show(); - } - - private void addUrl(String url) { - startActivity(new OnlineFeedviewActivityStarter(getContext(), url).withManualUrl().getIntent()); - } - - private void performSearch() { - viewBinding.combinedFeedSearchEditText.clearFocus(); - InputMethodManager in = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE); - in.hideSoftInputFromWindow(viewBinding.combinedFeedSearchEditText.getWindowToken(), 0); - String query = viewBinding.combinedFeedSearchEditText.getText().toString(); - if (query.matches("http[s]?://.*")) { - addUrl(query); - return; - } - activity.loadChildFragment(OnlineSearchFragment.newInstance(CombinedSearcher.class, query)); - viewBinding.combinedFeedSearchEditText.post(() -> viewBinding.combinedFeedSearchEditText.setText("")); - } - - private void chooseOpmlImportPathResult(final Uri uri) { - if (uri == null) { - return; - } - final Intent intent = new Intent(getContext(), OpmlImportActivity.class); - intent.setData(uri); - startActivity(intent); - } - - private void addLocalFolderResult(final Uri uri) { - if (uri == null) { - return; - } - Observable.fromCallable(() -> addLocalFolder(uri)) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - feed -> { - Fragment fragment = FeedItemlistFragment.newInstance(feed.getId()); - ((MainActivity) getActivity()).loadChildFragment(fragment); - }, error -> { - Log.e(TAG, Log.getStackTraceString(error)); - ((MainActivity) getActivity()) - .showSnackbarAbovePlayer(error.getLocalizedMessage(), Snackbar.LENGTH_LONG); - }); - } - - private Feed addLocalFolder(Uri uri) { - getActivity().getContentResolver().takePersistableUriPermission(uri, - Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); - DocumentFile documentFile = DocumentFile.fromTreeUri(getContext(), uri); - if (documentFile == null) { - throw new IllegalArgumentException("Unable to retrieve document tree"); - } - String title = documentFile.getName(); - if (title == null) { - title = getString(R.string.local_folder); - } - Feed dirFeed = new Feed(Feed.PREFIX_LOCAL_FOLDER + uri.toString(), null, title); - dirFeed.setItems(Collections.emptyList()); - dirFeed.setSortOrder(SortOrder.EPISODE_TITLE_A_Z); - Feed fromDatabase = FeedDatabaseWriter.updateFeed(getContext(), dirFeed, false); - FeedUpdateManager.getInstance().runOnce(requireContext(), fromDatabase); - return fromDatabase; - } - - private static class AddLocalFolder extends ActivityResultContracts.OpenDocumentTree { - @NonNull - @Override - public Intent createIntent(@NonNull final Context context, @Nullable final Uri input) { - return super.createIntent(context, input) - .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - } - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/AllEpisodesFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/AllEpisodesFragment.java deleted file mode 100644 index 5ef188534..000000000 --- a/app/src/main/java/de/danoeh/antennapod/fragment/AllEpisodesFragment.java +++ /dev/null @@ -1,147 +0,0 @@ -package de.danoeh.antennapod.fragment; - -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.storage.database.DBReader; -import de.danoeh.antennapod.dialog.AllEpisodesFilterDialog; -import de.danoeh.antennapod.dialog.ItemSortDialog; -import de.danoeh.antennapod.event.FeedListUpdateEvent; -import de.danoeh.antennapod.model.feed.FeedItem; -import de.danoeh.antennapod.model.feed.FeedItemFilter; -import de.danoeh.antennapod.model.feed.SortOrder; -import de.danoeh.antennapod.storage.preferences.UserPreferences; -import org.apache.commons.lang3.StringUtils; -import org.greenrobot.eventbus.EventBus; -import org.greenrobot.eventbus.Subscribe; - -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; - -/** - * Shows all episodes (possibly filtered by user). - */ -public class AllEpisodesFragment extends EpisodesListFragment { - public static final String TAG = "EpisodesFragment"; - public static final String PREF_NAME = "PrefAllEpisodesFragment"; - - @NonNull - @Override - public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - final View root = super.onCreateView(inflater, container, savedInstanceState); - toolbar.inflateMenu(R.menu.episodes); - toolbar.setTitle(R.string.episodes_label); - updateToolbar(); - updateFilterUi(); - txtvInformation.setOnClickListener( - v -> AllEpisodesFilterDialog.newInstance(getFilter()).show(getChildFragmentManager(), null)); - return root; - } - - @NonNull - @Override - protected List loadData() { - return DBReader.getEpisodes(0, page * EPISODES_PER_PAGE, getFilter(), - UserPreferences.getAllEpisodesSortOrder()); - } - - @NonNull - @Override - protected List loadMoreData(int page) { - return DBReader.getEpisodes((page - 1) * EPISODES_PER_PAGE, EPISODES_PER_PAGE, getFilter(), - UserPreferences.getAllEpisodesSortOrder()); - } - - @Override - protected int loadTotalItemCount() { - return DBReader.getTotalEpisodeCount(getFilter()); - } - - @Override - protected FeedItemFilter getFilter() { - return new FeedItemFilter(UserPreferences.getPrefFilterAllEpisodes()); - } - - @Override - protected String getFragmentTag() { - return TAG; - } - - @Override - protected String getPrefName() { - return PREF_NAME; - } - - @Override - public boolean onMenuItemClick(MenuItem item) { - if (super.onMenuItemClick(item)) { - return true; - } - if (item.getItemId() == R.id.filter_items) { - AllEpisodesFilterDialog.newInstance(getFilter()).show(getChildFragmentManager(), null); - return true; - } else if (item.getItemId() == R.id.action_favorites) { - ArrayList filter = new ArrayList<>(getFilter().getValuesList()); - if (filter.contains(FeedItemFilter.IS_FAVORITE)) { - filter.remove(FeedItemFilter.IS_FAVORITE); - } else { - filter.add(FeedItemFilter.IS_FAVORITE); - } - onFilterChanged(new AllEpisodesFilterDialog.AllEpisodesFilterChangedEvent(new HashSet<>(filter))); - return true; - } else if (item.getItemId() == R.id.episodes_sort) { - new AllEpisodesSortDialog().show(getChildFragmentManager().beginTransaction(), "SortDialog"); - return true; - } - return false; - } - - @Subscribe - public void onFilterChanged(AllEpisodesFilterDialog.AllEpisodesFilterChangedEvent event) { - UserPreferences.setPrefFilterAllEpisodes(StringUtils.join(event.filterValues, ",")); - updateFilterUi(); - page = 1; - loadItems(); - } - - private void updateFilterUi() { - swipeActions.setFilter(getFilter()); - if (getFilter().getValues().length > 0) { - txtvInformation.setVisibility(View.VISIBLE); - emptyView.setMessage(R.string.no_all_episodes_filtered_label); - } else { - txtvInformation.setVisibility(View.GONE); - emptyView.setMessage(R.string.no_all_episodes_label); - } - toolbar.getMenu().findItem(R.id.action_favorites).setIcon( - getFilter().showIsFavorite ? R.drawable.ic_star : R.drawable.ic_star_border); - } - - public static class AllEpisodesSortDialog extends ItemSortDialog { - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - sortOrder = UserPreferences.getAllEpisodesSortOrder(); - } - - @Override - protected void onAddItem(int title, SortOrder ascending, SortOrder descending, boolean ascendingIsDefault) { - if (ascending == SortOrder.DATE_OLD_NEW || ascending == SortOrder.DURATION_SHORT_LONG) { - super.onAddItem(title, ascending, descending, ascendingIsDefault); - } - } - - @Override - protected void onSelectionChanged() { - super.onSelectionChanged(); - UserPreferences.setAllEpisodesSortOrder(sortOrder); - EventBus.getDefault().post(new FeedListUpdateEvent(0)); - } - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/AudioPlayerFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/AudioPlayerFragment.java deleted file mode 100644 index 3695e7426..000000000 --- a/app/src/main/java/de/danoeh/antennapod/fragment/AudioPlayerFragment.java +++ /dev/null @@ -1,562 +0,0 @@ -package de.danoeh.antennapod.fragment; - -import android.content.Intent; -import android.os.Bundle; -import android.util.Log; -import android.view.KeyEvent; -import android.view.LayoutInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageButton; -import android.widget.ProgressBar; -import android.widget.SeekBar; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.cardview.widget.CardView; -import androidx.fragment.app.Fragment; -import androidx.interpolator.view.animation.FastOutSlowInInterpolator; -import androidx.viewpager2.adapter.FragmentStateAdapter; -import androidx.viewpager2.widget.ViewPager2; - -import com.google.android.material.appbar.MaterialToolbar; -import com.google.android.material.bottomsheet.BottomSheetBehavior; -import com.google.android.material.elevation.SurfaceColors; - -import de.danoeh.antennapod.playback.service.PlaybackController; -import de.danoeh.antennapod.ui.appstartintent.MediaButtonStarter; -import de.danoeh.antennapod.ui.episodes.PlaybackSpeedUtils; -import de.danoeh.antennapod.ui.episodes.TimeSpeedConverter; -import org.greenrobot.eventbus.EventBus; -import org.greenrobot.eventbus.Subscribe; -import org.greenrobot.eventbus.ThreadMode; - -import java.text.DecimalFormat; -import java.text.NumberFormat; -import java.util.List; - -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.activity.MainActivity; -import de.danoeh.antennapod.core.util.ChapterUtils; -import de.danoeh.antennapod.ui.common.Converter; -import de.danoeh.antennapod.dialog.MediaPlayerErrorDialog; -import de.danoeh.antennapod.dialog.SkipPreferenceDialog; -import de.danoeh.antennapod.dialog.SleepTimerDialog; -import de.danoeh.antennapod.dialog.VariableSpeedDialog; -import de.danoeh.antennapod.event.FavoritesEvent; -import de.danoeh.antennapod.event.PlayerErrorEvent; -import de.danoeh.antennapod.event.UnreadItemsUpdateEvent; -import de.danoeh.antennapod.event.playback.BufferUpdateEvent; -import de.danoeh.antennapod.event.playback.PlaybackPositionEvent; -import de.danoeh.antennapod.event.playback.PlaybackServiceEvent; -import de.danoeh.antennapod.event.playback.SleepTimerUpdatedEvent; -import de.danoeh.antennapod.event.playback.SpeedChangedEvent; -import de.danoeh.antennapod.menuhandler.FeedItemMenuHandler; -import de.danoeh.antennapod.model.feed.Chapter; -import de.danoeh.antennapod.model.feed.FeedItem; -import de.danoeh.antennapod.model.feed.FeedMedia; -import de.danoeh.antennapod.model.playback.Playable; -import de.danoeh.antennapod.playback.cast.CastEnabledActivity; -import de.danoeh.antennapod.storage.preferences.UserPreferences; -import de.danoeh.antennapod.ui.common.PlaybackSpeedIndicatorView; -import de.danoeh.antennapod.view.ChapterSeekBar; -import de.danoeh.antennapod.view.PlayButton; -import io.reactivex.Maybe; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.Disposable; -import io.reactivex.schedulers.Schedulers; - -/** - * Shows the audio player. - */ -public class AudioPlayerFragment extends Fragment implements - ChapterSeekBar.OnSeekBarChangeListener, MaterialToolbar.OnMenuItemClickListener { - public static final String TAG = "AudioPlayerFragment"; - public static final int POS_COVER = 0; - public static final int POS_DESCRIPTION = 1; - private static final int NUM_CONTENT_FRAGMENTS = 2; - - PlaybackSpeedIndicatorView butPlaybackSpeed; - TextView txtvPlaybackSpeed; - private ViewPager2 pager; - private TextView txtvPosition; - private TextView txtvLength; - private ChapterSeekBar sbPosition; - private ImageButton butRev; - private TextView txtvRev; - private PlayButton butPlay; - private ImageButton butFF; - private TextView txtvFF; - private ImageButton butSkip; - private MaterialToolbar toolbar; - private ProgressBar progressIndicator; - private CardView cardViewSeek; - private TextView txtvSeek; - - private PlaybackController controller; - private Disposable disposable; - private boolean showTimeLeft; - private boolean seekedToChapterStart = false; - private int currentChapterIndex = -1; - private int duration; - - @Override - public View onCreateView(@NonNull LayoutInflater inflater, - @Nullable ViewGroup container, - @Nullable Bundle savedInstanceState) { - super.onCreateView(inflater, container, savedInstanceState); - View root = inflater.inflate(R.layout.audioplayer_fragment, container, false); - root.setOnTouchListener((v, event) -> true); // Avoid clicks going through player to fragments below - toolbar = root.findViewById(R.id.toolbar); - toolbar.setTitle(""); - toolbar.setNavigationOnClickListener(v -> - ((MainActivity) getActivity()).getBottomSheet().setState(BottomSheetBehavior.STATE_COLLAPSED)); - toolbar.setOnMenuItemClickListener(this); - - ExternalPlayerFragment externalPlayerFragment = new ExternalPlayerFragment(); - getChildFragmentManager().beginTransaction() - .replace(R.id.playerFragment, externalPlayerFragment, ExternalPlayerFragment.TAG) - .commit(); - root.findViewById(R.id.playerFragment).setBackgroundColor( - SurfaceColors.getColorForElevation(getContext(), 8 * getResources().getDisplayMetrics().density)); - - butPlaybackSpeed = root.findViewById(R.id.butPlaybackSpeed); - txtvPlaybackSpeed = root.findViewById(R.id.txtvPlaybackSpeed); - sbPosition = root.findViewById(R.id.sbPosition); - txtvPosition = root.findViewById(R.id.txtvPosition); - txtvLength = root.findViewById(R.id.txtvLength); - butRev = root.findViewById(R.id.butRev); - txtvRev = root.findViewById(R.id.txtvRev); - butPlay = root.findViewById(R.id.butPlay); - butFF = root.findViewById(R.id.butFF); - txtvFF = root.findViewById(R.id.txtvFF); - butSkip = root.findViewById(R.id.butSkip); - progressIndicator = root.findViewById(R.id.progLoading); - cardViewSeek = root.findViewById(R.id.cardViewSeek); - txtvSeek = root.findViewById(R.id.txtvSeek); - - setupLengthTextView(); - setupControlButtons(); - butPlaybackSpeed.setOnClickListener(v -> new VariableSpeedDialog().show(getChildFragmentManager(), null)); - sbPosition.setOnSeekBarChangeListener(this); - - pager = root.findViewById(R.id.pager); - pager.setAdapter(new AudioPlayerPagerAdapter(this)); - // Required for getChildAt(int) in ViewPagerBottomSheetBehavior to return the correct page - pager.setOffscreenPageLimit((int) NUM_CONTENT_FRAGMENTS); - pager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() { - @Override - public void onPageSelected(int position) { - pager.post(() -> { - if (getActivity() != null) { - // By the time this is posted, the activity might be closed again. - ((MainActivity) getActivity()).getBottomSheet().updateScrollingChild(); - } - }); - } - }); - - return root; - } - - private void setChapterDividers(Playable media) { - - if (media == null) { - return; - } - - float[] dividerPos = null; - - if (media.getChapters() != null && !media.getChapters().isEmpty()) { - List chapters = media.getChapters(); - dividerPos = new float[chapters.size()]; - - for (int i = 0; i < chapters.size(); i++) { - dividerPos[i] = chapters.get(i).getStart() / (float) duration; - } - } - - sbPosition.setDividerPos(dividerPos); - } - - private void setupControlButtons() { - butRev.setOnClickListener(v -> { - if (controller != null) { - int curr = controller.getPosition(); - controller.seekTo(curr - UserPreferences.getRewindSecs() * 1000); - } - }); - butRev.setOnLongClickListener(v -> { - SkipPreferenceDialog.showSkipPreference(getContext(), - SkipPreferenceDialog.SkipDirection.SKIP_REWIND, txtvRev); - return true; - }); - butPlay.setOnClickListener(v -> { - if (controller != null) { - controller.init(); - controller.playPause(); - } - }); - butFF.setOnClickListener(v -> { - if (controller != null) { - int curr = controller.getPosition(); - controller.seekTo(curr + UserPreferences.getFastForwardSecs() * 1000); - } - }); - butFF.setOnLongClickListener(v -> { - SkipPreferenceDialog.showSkipPreference(getContext(), - SkipPreferenceDialog.SkipDirection.SKIP_FORWARD, txtvFF); - return false; - }); - butSkip.setOnClickListener(v -> getActivity().sendBroadcast( - MediaButtonStarter.createIntent(getContext(), KeyEvent.KEYCODE_MEDIA_NEXT))); - } - - @Subscribe(threadMode = ThreadMode.MAIN) - public void onUnreadItemsUpdate(UnreadItemsUpdateEvent event) { - if (controller == null) { - return; - } - updatePosition(new PlaybackPositionEvent(controller.getPosition(), - controller.getDuration())); - } - - @Subscribe(threadMode = ThreadMode.MAIN) - public void onPlaybackServiceChanged(PlaybackServiceEvent event) { - if (event.action == PlaybackServiceEvent.Action.SERVICE_SHUT_DOWN) { - ((MainActivity) getActivity()).getBottomSheet().setState(BottomSheetBehavior.STATE_COLLAPSED); - } - } - - private void setupLengthTextView() { - showTimeLeft = UserPreferences.shouldShowRemainingTime(); - txtvLength.setOnClickListener(v -> { - if (controller == null) { - return; - } - showTimeLeft = !showTimeLeft; - UserPreferences.setShowRemainTimeSetting(showTimeLeft); - updatePosition(new PlaybackPositionEvent(controller.getPosition(), - controller.getDuration())); - }); - } - - @Subscribe(threadMode = ThreadMode.MAIN) - public void updatePlaybackSpeedButton(SpeedChangedEvent event) { - String speedStr = new DecimalFormat("0.00").format(event.getNewSpeed()); - txtvPlaybackSpeed.setText(speedStr); - butPlaybackSpeed.setSpeed(event.getNewSpeed()); - } - - private void loadMediaInfo(boolean includingChapters) { - if (disposable != null) { - disposable.dispose(); - } - disposable = Maybe.create(emitter -> { - Playable media = controller.getMedia(); - if (media != null) { - if (includingChapters) { - ChapterUtils.loadChapters(media, getContext(), false); - } - emitter.onSuccess(media); - } else { - emitter.onComplete(); - } - }) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(media -> { - updateUi(media); - if (media.getChapters() == null && !includingChapters) { - loadMediaInfo(true); - } - }, error -> Log.e(TAG, Log.getStackTraceString(error)), - () -> updateUi(null)); - } - - private PlaybackController newPlaybackController() { - return new PlaybackController(getActivity()) { - @Override - protected void updatePlayButtonShowsPlay(boolean showPlay) { - butPlay.setIsShowPlay(showPlay); - } - - @Override - public void loadMediaInfo() { - AudioPlayerFragment.this.loadMediaInfo(false); - } - - @Override - public void onPlaybackEnd() { - ((MainActivity) getActivity()).getBottomSheet().setState(BottomSheetBehavior.STATE_COLLAPSED); - } - }; - } - - private void updateUi(Playable media) { - if (controller == null || media == null) { - return; - } - duration = controller.getDuration(); - updatePosition(new PlaybackPositionEvent(media.getPosition(), media.getDuration())); - updatePlaybackSpeedButton(new SpeedChangedEvent(PlaybackSpeedUtils.getCurrentPlaybackSpeed(media))); - setChapterDividers(media); - setupOptionsMenu(media); - } - - @Subscribe(threadMode = ThreadMode.MAIN) - @SuppressWarnings("unused") - public void sleepTimerUpdate(SleepTimerUpdatedEvent event) { - if (event.isCancelled() || event.wasJustEnabled()) { - AudioPlayerFragment.this.loadMediaInfo(false); - } - } - - @Override - public void onStart() { - super.onStart(); - controller = newPlaybackController(); - controller.init(); - loadMediaInfo(false); - EventBus.getDefault().register(this); - txtvRev.setText(NumberFormat.getInstance().format(UserPreferences.getRewindSecs())); - txtvFF.setText(NumberFormat.getInstance().format(UserPreferences.getFastForwardSecs())); - } - - @Override - public void onStop() { - super.onStop(); - controller.release(); - controller = null; - progressIndicator.setVisibility(View.GONE); // Controller released; we will not receive buffering updates - EventBus.getDefault().unregister(this); - if (disposable != null) { - disposable.dispose(); - } - } - - @Subscribe(threadMode = ThreadMode.MAIN) - @SuppressWarnings("unused") - public void bufferUpdate(BufferUpdateEvent event) { - if (event.hasStarted()) { - progressIndicator.setVisibility(View.VISIBLE); - } else if (event.hasEnded()) { - progressIndicator.setVisibility(View.GONE); - } else if (controller != null && controller.isStreaming()) { - sbPosition.setSecondaryProgress((int) (event.getProgress() * sbPosition.getMax())); - } else { - sbPosition.setSecondaryProgress(0); - } - } - - @Subscribe(threadMode = ThreadMode.MAIN) - public void updatePosition(PlaybackPositionEvent event) { - if (controller == null || txtvPosition == null || txtvLength == null || sbPosition == null) { - return; - } - - TimeSpeedConverter converter = new TimeSpeedConverter(controller.getCurrentPlaybackSpeedMultiplier()); - int currentPosition = converter.convert(event.getPosition()); - int duration = converter.convert(event.getDuration()); - int remainingTime = converter.convert(Math.max(event.getDuration() - event.getPosition(), 0)); - currentChapterIndex = ChapterUtils.getCurrentChapterIndex(controller.getMedia(), currentPosition); - Log.d(TAG, "currentPosition " + Converter.getDurationStringLong(currentPosition)); - if (currentPosition == Playable.INVALID_TIME || duration == Playable.INVALID_TIME) { - Log.w(TAG, "Could not react to position observer update because of invalid time"); - return; - } - txtvPosition.setText(Converter.getDurationStringLong(currentPosition)); - txtvPosition.setContentDescription(getString(R.string.position, - Converter.getDurationStringLocalized(getContext(), currentPosition))); - showTimeLeft = UserPreferences.shouldShowRemainingTime(); - if (showTimeLeft) { - txtvLength.setContentDescription(getString(R.string.remaining_time, - Converter.getDurationStringLocalized(getContext(), remainingTime))); - txtvLength.setText(((remainingTime > 0) ? "-" : "") + Converter.getDurationStringLong(remainingTime)); - } else { - txtvLength.setContentDescription(getString(R.string.chapter_duration, - Converter.getDurationStringLocalized(getContext(), duration))); - txtvLength.setText(Converter.getDurationStringLong(duration)); - } - - if (!sbPosition.isPressed()) { - float progress = ((float) event.getPosition()) / event.getDuration(); - sbPosition.setProgress((int) (progress * sbPosition.getMax())); - } - } - - @Subscribe(threadMode = ThreadMode.MAIN) - public void favoritesChanged(FavoritesEvent event) { - AudioPlayerFragment.this.loadMediaInfo(false); - } - - @Subscribe(threadMode = ThreadMode.MAIN) - public void mediaPlayerError(PlayerErrorEvent event) { - MediaPlayerErrorDialog.show(getActivity(), event); - } - - @Override - public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { - if (controller == null || txtvLength == null) { - return; - } - - if (fromUser) { - float prog = progress / ((float) seekBar.getMax()); - TimeSpeedConverter converter = new TimeSpeedConverter(controller.getCurrentPlaybackSpeedMultiplier()); - int position = converter.convert((int) (prog * controller.getDuration())); - int newChapterIndex = ChapterUtils.getCurrentChapterIndex(controller.getMedia(), position); - if (newChapterIndex > -1) { - if (!sbPosition.isPressed() && currentChapterIndex != newChapterIndex) { - currentChapterIndex = newChapterIndex; - position = (int) controller.getMedia().getChapters().get(currentChapterIndex).getStart(); - seekedToChapterStart = true; - controller.seekTo(position); - updateUi(controller.getMedia()); - sbPosition.highlightCurrentChapter(); - } - txtvSeek.setText(controller.getMedia().getChapters().get(newChapterIndex).getTitle() - + "\n" + Converter.getDurationStringLong(position)); - } else { - txtvSeek.setText(Converter.getDurationStringLong(position)); - } - } else if (duration != controller.getDuration()) { - updateUi(controller.getMedia()); - } - } - - @Override - public void onStartTrackingTouch(SeekBar seekBar) { - // interrupt position Observer, restart later - cardViewSeek.setScaleX(.8f); - cardViewSeek.setScaleY(.8f); - cardViewSeek.animate() - .setInterpolator(new FastOutSlowInInterpolator()) - .alpha(1f).scaleX(1f).scaleY(1f) - .setDuration(200) - .start(); - } - - @Override - public void onStopTrackingTouch(SeekBar seekBar) { - if (controller != null) { - if (seekedToChapterStart) { - seekedToChapterStart = false; - } else { - float prog = seekBar.getProgress() / ((float) seekBar.getMax()); - controller.seekTo((int) (prog * controller.getDuration())); - } - } - cardViewSeek.setScaleX(1f); - cardViewSeek.setScaleY(1f); - cardViewSeek.animate() - .setInterpolator(new FastOutSlowInInterpolator()) - .alpha(0f).scaleX(.8f).scaleY(.8f) - .setDuration(200) - .start(); - } - - public void setupOptionsMenu(Playable media) { - if (toolbar.getMenu().size() == 0) { - toolbar.inflateMenu(R.menu.mediaplayer); - } - if (controller == null) { - return; - } - boolean isFeedMedia = media instanceof FeedMedia; - toolbar.getMenu().findItem(R.id.open_feed_item).setVisible(isFeedMedia); - if (isFeedMedia) { - FeedItemMenuHandler.onPrepareMenu(toolbar.getMenu(), ((FeedMedia) media).getItem()); - } - - toolbar.getMenu().findItem(R.id.set_sleeptimer_item).setVisible(!controller.sleepTimerActive()); - toolbar.getMenu().findItem(R.id.disable_sleeptimer_item).setVisible(controller.sleepTimerActive()); - - ((CastEnabledActivity) getActivity()).requestCastButton(toolbar.getMenu()); - } - - @Override - public boolean onMenuItemClick(MenuItem item) { - if (controller == null) { - return false; - } - Playable media = controller.getMedia(); - if (media == null) { - return false; - } - - final @Nullable FeedItem feedItem = (media instanceof FeedMedia) ? ((FeedMedia) media).getItem() : null; - if (feedItem != null && FeedItemMenuHandler.onMenuItemClicked(this, item.getItemId(), feedItem)) { - return true; - } - - final int itemId = item.getItemId(); - if (itemId == R.id.disable_sleeptimer_item || itemId == R.id.set_sleeptimer_item) { - new SleepTimerDialog().show(getChildFragmentManager(), "SleepTimerDialog"); - return true; - } else if (itemId == R.id.open_feed_item) { - if (feedItem != null) { - Intent intent = MainActivity.getIntentToOpenFeed(getContext(), feedItem.getFeedId()); - startActivity(intent); - } - return true; - } - return false; - } - - public void fadePlayerToToolbar(float slideOffset) { - float playerFadeProgress = Math.max(0.0f, Math.min(0.2f, slideOffset - 0.2f)) / 0.2f; - View player = getView().findViewById(R.id.playerFragment); - player.setAlpha(1 - playerFadeProgress); - player.setVisibility(playerFadeProgress > 0.99f ? View.INVISIBLE : View.VISIBLE); - float toolbarFadeProgress = Math.max(0.0f, Math.min(0.2f, slideOffset - 0.6f)) / 0.2f; - toolbar.setAlpha(toolbarFadeProgress); - toolbar.setVisibility(toolbarFadeProgress < 0.01f ? View.INVISIBLE : View.VISIBLE); - } - - private static class AudioPlayerPagerAdapter extends FragmentStateAdapter { - private static final String TAG = "AudioPlayerPagerAdapter"; - - public AudioPlayerPagerAdapter(@NonNull Fragment fragment) { - super(fragment); - } - - @NonNull - @Override - public Fragment createFragment(int position) { - Log.d(TAG, "getItem(" + position + ")"); - - switch (position) { - case POS_COVER: - return new CoverFragment(); - default: - case POS_DESCRIPTION: - return new ItemDescriptionFragment(); - } - } - - @Override - public int getItemCount() { - return NUM_CONTENT_FRAGMENTS; - } - } - - public void scrollToPage(int page, boolean smoothScroll) { - if (pager == null) { - return; - } - - pager.setCurrentItem(page, smoothScroll); - - Fragment visibleChild = getChildFragmentManager().findFragmentByTag("f" + POS_DESCRIPTION); - if (visibleChild instanceof ItemDescriptionFragment) { - ((ItemDescriptionFragment) visibleChild).scrollToTop(); - } - } - - public void scrollToPage(int page) { - scrollToPage(page, false); - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/ChaptersFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/ChaptersFragment.java deleted file mode 100644 index 14a8e68e6..000000000 --- a/app/src/main/java/de/danoeh/antennapod/fragment/ChaptersFragment.java +++ /dev/null @@ -1,192 +0,0 @@ -package de.danoeh.antennapod.fragment; - -import android.app.Dialog; -import android.content.DialogInterface; -import android.os.Bundle; -import android.text.TextUtils; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.ProgressBar; -import android.widget.Toast; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.app.AppCompatDialogFragment; -import androidx.coordinatorlayout.widget.CoordinatorLayout; -import androidx.recyclerview.widget.DividerItemDecoration; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; -import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.adapter.ChaptersListAdapter; -import de.danoeh.antennapod.core.util.ChapterUtils; -import de.danoeh.antennapod.event.playback.PlaybackPositionEvent; -import de.danoeh.antennapod.model.feed.Chapter; -import de.danoeh.antennapod.model.feed.FeedMedia; -import de.danoeh.antennapod.model.playback.Playable; -import de.danoeh.antennapod.playback.base.PlayerStatus; -import de.danoeh.antennapod.playback.service.PlaybackController; -import io.reactivex.Maybe; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.Disposable; -import io.reactivex.schedulers.Schedulers; -import org.greenrobot.eventbus.EventBus; -import org.greenrobot.eventbus.Subscribe; -import org.greenrobot.eventbus.ThreadMode; - -public class ChaptersFragment extends AppCompatDialogFragment { - public static final String TAG = "ChaptersFragment"; - private ChaptersListAdapter adapter; - private PlaybackController controller; - private Disposable disposable; - private int focusedChapter = -1; - private Playable media; - private LinearLayoutManager layoutManager; - private ProgressBar progressBar; - - @NonNull - @Override - public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { - - AlertDialog dialog = new MaterialAlertDialogBuilder(requireContext()) - .setTitle(getString(R.string.chapters_label)) - .setView(onCreateView(getLayoutInflater())) - .setPositiveButton(getString(R.string.close_label), null) //dismisses - .setNeutralButton(getString(R.string.refresh_label), null) - .create(); - dialog.show(); - dialog.getButton(DialogInterface.BUTTON_NEUTRAL).setVisibility(View.INVISIBLE); - dialog.getButton(DialogInterface.BUTTON_NEUTRAL).setOnClickListener(v -> { - progressBar.setVisibility(View.VISIBLE); - loadMediaInfo(true); - }); - - return dialog; - } - - - public View onCreateView(@NonNull LayoutInflater inflater) { - View root = inflater.inflate(R.layout.simple_list_fragment, null, false); - root.findViewById(R.id.toolbar).setVisibility(View.GONE); - RecyclerView recyclerView = root.findViewById(R.id.recyclerView); - progressBar = root.findViewById(R.id.progLoading); - layoutManager = new LinearLayoutManager(getActivity()); - recyclerView.setLayoutManager(layoutManager); - recyclerView.addItemDecoration(new DividerItemDecoration(recyclerView.getContext(), - layoutManager.getOrientation())); - - adapter = new ChaptersListAdapter(getActivity(), pos -> { - if (controller.getStatus() != PlayerStatus.PLAYING) { - controller.playPause(); - } - Chapter chapter = adapter.getItem(pos); - controller.seekTo((int) chapter.getStart()); - updateChapterSelection(pos, true); - }); - recyclerView.setAdapter(adapter); - - progressBar.setVisibility(View.VISIBLE); - - CoordinatorLayout.LayoutParams wrapHeight = new CoordinatorLayout.LayoutParams( - CoordinatorLayout.LayoutParams.MATCH_PARENT, CoordinatorLayout.LayoutParams.WRAP_CONTENT); - recyclerView.setLayoutParams(wrapHeight); - - return root; - } - - @Override - public void onStart() { - super.onStart(); - controller = new PlaybackController(getActivity()) { - @Override - public void loadMediaInfo() { - ChaptersFragment.this.loadMediaInfo(false); - } - }; - controller.init(); - EventBus.getDefault().register(this); - loadMediaInfo(false); - } - - @Override - public void onStop() { - super.onStop(); - - if (disposable != null) { - disposable.dispose(); - } - controller.release(); - controller = null; - EventBus.getDefault().unregister(this); - } - - @Subscribe(threadMode = ThreadMode.MAIN) - public void onEventMainThread(PlaybackPositionEvent event) { - updateChapterSelection(getCurrentChapter(media), false); - adapter.notifyTimeChanged(event.getPosition()); - } - - private int getCurrentChapter(Playable media) { - if (controller == null) { - return -1; - } - return ChapterUtils.getCurrentChapterIndex(media, controller.getPosition()); - } - - private void loadMediaInfo(boolean forceRefresh) { - if (disposable != null) { - disposable.dispose(); - } - disposable = Maybe.create(emitter -> { - Playable media = controller.getMedia(); - if (media != null) { - ChapterUtils.loadChapters(media, getContext(), forceRefresh); - emitter.onSuccess(media); - } else { - emitter.onComplete(); - } - }) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(media -> onMediaChanged((Playable) media), - error -> Log.e(TAG, Log.getStackTraceString(error))); - } - - private void onMediaChanged(Playable media) { - this.media = media; - focusedChapter = -1; - if (adapter == null) { - return; - } - if (media.getChapters() != null && media.getChapters().size() == 0) { - dismiss(); - Toast.makeText(getContext(), R.string.no_chapters_label, Toast.LENGTH_LONG).show(); - } else { - progressBar.setVisibility(View.GONE); - } - adapter.setMedia(media); - ((AlertDialog) getDialog()).getButton(DialogInterface.BUTTON_NEUTRAL).setVisibility(View.INVISIBLE); - if (media instanceof FeedMedia && ((FeedMedia) media).getItem() != null - && !TextUtils.isEmpty(((FeedMedia) media).getItem().getPodcastIndexChapterUrl())) { - ((AlertDialog) getDialog()).getButton(DialogInterface.BUTTON_NEUTRAL).setVisibility(View.VISIBLE); - } - int positionOfCurrentChapter = getCurrentChapter(media); - updateChapterSelection(positionOfCurrentChapter, true); - } - - private void updateChapterSelection(int position, boolean scrollTo) { - if (adapter == null) { - return; - } - - if (position != -1 && focusedChapter != position) { - focusedChapter = position; - adapter.notifyChapterChanged(focusedChapter); - if (scrollTo && (layoutManager.findFirstCompletelyVisibleItemPosition() >= position - || layoutManager.findLastCompletelyVisibleItemPosition() <= position)) { - layoutManager.scrollToPositionWithOffset(position, 100); - } - } - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/CompletedDownloadsFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/CompletedDownloadsFragment.java deleted file mode 100644 index 9db4f585a..000000000 --- a/app/src/main/java/de/danoeh/antennapod/fragment/CompletedDownloadsFragment.java +++ /dev/null @@ -1,392 +0,0 @@ -package de.danoeh.antennapod.fragment; - -import android.os.Bundle; -import android.util.Log; -import android.view.ContextMenu; -import android.view.LayoutInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ProgressBar; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.fragment.app.Fragment; -import com.google.android.material.appbar.MaterialToolbar; -import com.google.android.material.snackbar.Snackbar; -import com.leinardi.android.speeddial.SpeedDialView; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.activity.MainActivity; -import de.danoeh.antennapod.adapter.EpisodeItemListAdapter; -import de.danoeh.antennapod.adapter.actionbutton.DeleteActionButton; -import de.danoeh.antennapod.event.DownloadLogEvent; -import de.danoeh.antennapod.core.menuhandler.MenuItemUtils; -import de.danoeh.antennapod.net.download.serviceinterface.FeedUpdateManager; -import de.danoeh.antennapod.storage.database.DBReader; -import de.danoeh.antennapod.core.util.FeedItemUtil; -import de.danoeh.antennapod.dialog.ItemSortDialog; -import de.danoeh.antennapod.event.EpisodeDownloadEvent; -import de.danoeh.antennapod.event.FeedItemEvent; -import de.danoeh.antennapod.event.PlayerStatusEvent; -import de.danoeh.antennapod.event.UnreadItemsUpdateEvent; -import de.danoeh.antennapod.event.playback.PlaybackPositionEvent; -import de.danoeh.antennapod.fragment.actions.EpisodeMultiSelectActionHandler; -import de.danoeh.antennapod.fragment.swipeactions.SwipeActions; -import de.danoeh.antennapod.menuhandler.FeedItemMenuHandler; -import de.danoeh.antennapod.model.feed.FeedItem; -import de.danoeh.antennapod.model.feed.FeedItemFilter; -import de.danoeh.antennapod.model.feed.SortOrder; -import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterface; -import de.danoeh.antennapod.storage.preferences.UserPreferences; -import de.danoeh.antennapod.view.EmptyViewHandler; -import de.danoeh.antennapod.view.EpisodeItemListRecyclerView; -import de.danoeh.antennapod.view.LiftOnScrollListener; -import de.danoeh.antennapod.view.viewholder.EpisodeItemViewHolder; -import io.reactivex.Observable; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.Disposable; -import io.reactivex.schedulers.Schedulers; -import org.greenrobot.eventbus.EventBus; -import org.greenrobot.eventbus.Subscribe; -import org.greenrobot.eventbus.ThreadMode; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -/** - * Displays all completed downloads and provides a button to delete them. - */ -public class CompletedDownloadsFragment extends Fragment - implements EpisodeItemListAdapter.OnSelectModeListener, MaterialToolbar.OnMenuItemClickListener { - public static final String TAG = "DownloadsFragment"; - public static final String ARG_SHOW_LOGS = "show_logs"; - private static final String KEY_UP_ARROW = "up_arrow"; - - private Set runningDownloads = new HashSet<>(); - private List items = new ArrayList<>(); - private CompletedDownloadsListAdapter adapter; - private EpisodeItemListRecyclerView recyclerView; - private Disposable disposable; - private EmptyViewHandler emptyView; - private boolean displayUpArrow; - private SpeedDialView speedDialView; - private SwipeActions swipeActions; - private ProgressBar progressBar; - private MaterialToolbar toolbar; - - @Override - public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, - @Nullable Bundle savedInstanceState) { - View root = inflater.inflate(R.layout.simple_list_fragment, container, false); - toolbar = root.findViewById(R.id.toolbar); - toolbar.setTitle(R.string.downloads_label); - toolbar.inflateMenu(R.menu.downloads_completed); - toolbar.setOnMenuItemClickListener(this); - toolbar.setOnLongClickListener(v -> { - recyclerView.scrollToPosition(5); - recyclerView.post(() -> recyclerView.smoothScrollToPosition(0)); - return false; - }); - displayUpArrow = getParentFragmentManager().getBackStackEntryCount() != 0; - if (savedInstanceState != null) { - displayUpArrow = savedInstanceState.getBoolean(KEY_UP_ARROW); - } - ((MainActivity) getActivity()).setupToolbarToggle(toolbar, displayUpArrow); - - recyclerView = root.findViewById(R.id.recyclerView); - recyclerView.setRecycledViewPool(((MainActivity) getActivity()).getRecycledViewPool()); - adapter = new CompletedDownloadsListAdapter((MainActivity) getActivity()); - adapter.setOnSelectModeListener(this); - recyclerView.setAdapter(adapter); - recyclerView.addOnScrollListener(new LiftOnScrollListener(root.findViewById(R.id.appbar))); - swipeActions = new SwipeActions(this, TAG).attachTo(recyclerView); - swipeActions.setFilter(new FeedItemFilter(FeedItemFilter.DOWNLOADED)); - - progressBar = root.findViewById(R.id.progLoading); - progressBar.setVisibility(View.VISIBLE); - - speedDialView = root.findViewById(R.id.fabSD); - speedDialView.setOverlayLayout(root.findViewById(R.id.fabSDOverlay)); - speedDialView.inflate(R.menu.episodes_apply_action_speeddial); - speedDialView.removeActionItemById(R.id.download_batch); - speedDialView.removeActionItemById(R.id.mark_read_batch); - speedDialView.removeActionItemById(R.id.mark_unread_batch); - speedDialView.removeActionItemById(R.id.remove_from_queue_batch); - speedDialView.removeActionItemById(R.id.remove_all_inbox_item); - speedDialView.setOnChangeListener(new SpeedDialView.OnChangeListener() { - @Override - public boolean onMainActionSelected() { - return false; - } - - @Override - public void onToggleChanged(boolean open) { - if (open && adapter.getSelectedCount() == 0) { - ((MainActivity) getActivity()).showSnackbarAbovePlayer(R.string.no_items_selected, - Snackbar.LENGTH_SHORT); - speedDialView.close(); - } - } - }); - speedDialView.setOnActionSelectedListener(actionItem -> { - new EpisodeMultiSelectActionHandler(((MainActivity) getActivity()), actionItem.getId()) - .handleAction(adapter.getSelectedItems()); - adapter.endSelectMode(); - return true; - }); - if (getArguments() != null && getArguments().getBoolean(ARG_SHOW_LOGS, false)) { - new DownloadLogFragment().show(getChildFragmentManager(), null); - } - - addEmptyView(); - EventBus.getDefault().register(this); - return root; - } - - @Override - public void onSaveInstanceState(@NonNull Bundle outState) { - outState.putBoolean(KEY_UP_ARROW, displayUpArrow); - super.onSaveInstanceState(outState); - } - - @Override - public void onDestroyView() { - EventBus.getDefault().unregister(this); - adapter.endSelectMode(); - if (toolbar != null) { - toolbar.setOnMenuItemClickListener(null); - toolbar.setOnLongClickListener(null); - } - super.onDestroyView(); - } - - @Override - public void onStart() { - super.onStart(); - loadItems(); - } - - @Override - public void onStop() { - super.onStop(); - if (disposable != null) { - disposable.dispose(); - } - } - - @Override - public boolean onMenuItemClick(MenuItem item) { - if (item.getItemId() == R.id.refresh_item) { - FeedUpdateManager.getInstance().runOnceOrAsk(requireContext()); - return true; - } else if (item.getItemId() == R.id.action_download_logs) { - new DownloadLogFragment().show(getChildFragmentManager(), null); - return true; - } else if (item.getItemId() == R.id.action_search) { - ((MainActivity) getActivity()).loadChildFragment(SearchFragment.newInstance()); - return true; - } else if (item.getItemId() == R.id.downloads_sort) { - new DownloadsSortDialog().show(getChildFragmentManager(), "SortDialog"); - return true; - } - return false; - } - - @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) - public void onEventMainThread(EpisodeDownloadEvent event) { - Set newRunningDownloads = new HashSet<>(); - for (String url : event.getUrls()) { - if (DownloadServiceInterface.get().isDownloadingEpisode(url)) { - newRunningDownloads.add(url); - } - } - if (!newRunningDownloads.equals(runningDownloads)) { - runningDownloads = newRunningDownloads; - loadItems(); - return; // Refreshed anyway - } - for (String downloadUrl : event.getUrls()) { - int pos = FeedItemUtil.indexOfItemWithDownloadUrl(items, downloadUrl); - if (pos >= 0) { - adapter.notifyItemChangedCompat(pos); - } - } - } - - @Override - public boolean onContextItemSelected(@NonNull MenuItem item) { - FeedItem selectedItem = adapter.getLongPressedItem(); - if (selectedItem == null) { - Log.i(TAG, "Selected item at current position was null, ignoring selection"); - return super.onContextItemSelected(item); - } - if (adapter.onContextItemSelected(item)) { - return true; - } - - return FeedItemMenuHandler.onMenuItemClicked(this, item.getItemId(), selectedItem); - } - - private void addEmptyView() { - emptyView = new EmptyViewHandler(getActivity()); - emptyView.setIcon(R.drawable.ic_download); - emptyView.setTitle(R.string.no_comp_downloads_head_label); - emptyView.setMessage(R.string.no_comp_downloads_label); - emptyView.attachToRecyclerView(recyclerView); - } - - @Subscribe(threadMode = ThreadMode.MAIN) - public void onEventMainThread(FeedItemEvent event) { - Log.d(TAG, "onEventMainThread() called with: " + "event = [" + event + "]"); - if (items == null) { - return; - } else if (adapter == null) { - loadItems(); - return; - } - for (int i = 0, size = event.items.size(); i < size; i++) { - FeedItem item = event.items.get(i); - int pos = FeedItemUtil.indexOfItemWithId(items, item.getId()); - if (pos >= 0) { - items.remove(pos); - if (item.getMedia().isDownloaded()) { - items.add(pos, item); - adapter.notifyItemChangedCompat(pos); - } else { - adapter.notifyItemRemoved(pos); - } - } - } - } - - @Subscribe(threadMode = ThreadMode.MAIN) - public void onEventMainThread(PlaybackPositionEvent event) { - if (adapter != null) { - for (int i = 0; i < adapter.getItemCount(); i++) { - EpisodeItemViewHolder holder = (EpisodeItemViewHolder) recyclerView.findViewHolderForAdapterPosition(i); - if (holder != null && holder.isCurrentlyPlayingItem()) { - holder.notifyPlaybackPositionUpdated(event); - break; - } - } - } - } - - @Subscribe(threadMode = ThreadMode.MAIN) - public void onPlayerStatusChanged(PlayerStatusEvent event) { - loadItems(); - } - - @Subscribe(threadMode = ThreadMode.MAIN) - public void onDownloadLogChanged(DownloadLogEvent event) { - loadItems(); - } - - @Subscribe(threadMode = ThreadMode.MAIN) - public void onUnreadItemsChanged(UnreadItemsUpdateEvent event) { - loadItems(); - } - - private void loadItems() { - if (disposable != null) { - disposable.dispose(); - } - emptyView.hide(); - disposable = Observable.fromCallable(() -> { - SortOrder sortOrder = UserPreferences.getDownloadsSortedOrder(); - List downloadedItems = DBReader.getEpisodes(0, Integer.MAX_VALUE, - new FeedItemFilter(FeedItemFilter.DOWNLOADED), sortOrder); - - List mediaUrls = new ArrayList<>(); - if (runningDownloads == null) { - return downloadedItems; - } - for (String url : runningDownloads) { - if (FeedItemUtil.indexOfItemWithDownloadUrl(downloadedItems, url) != -1) { - continue; // Already in list - } - mediaUrls.add(url); - } - List currentDownloads = DBReader.getFeedItemsWithUrl(mediaUrls); - currentDownloads.addAll(downloadedItems); - return currentDownloads; - }) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - result -> { - items = result; - adapter.setDummyViews(0); - progressBar.setVisibility(View.GONE); - adapter.updateItems(result); - }, error -> { - adapter.setDummyViews(0); - adapter.updateItems(Collections.emptyList()); - Log.e(TAG, Log.getStackTraceString(error)); - }); - } - - @Override - public void onStartSelectMode() { - swipeActions.detach(); - speedDialView.setVisibility(View.VISIBLE); - } - - @Override - public void onEndSelectMode() { - speedDialView.close(); - speedDialView.setVisibility(View.GONE); - swipeActions.attachTo(recyclerView); - } - - private class CompletedDownloadsListAdapter extends EpisodeItemListAdapter { - - public CompletedDownloadsListAdapter(MainActivity mainActivity) { - super(mainActivity); - } - - @Override - public void afterBindViewHolder(EpisodeItemViewHolder holder, int pos) { - if (!inActionMode()) { - if (holder.getFeedItem().isDownloaded()) { - DeleteActionButton actionButton = new DeleteActionButton(getItem(pos)); - actionButton.configure(holder.secondaryActionButton, holder.secondaryActionIcon, getActivity()); - } - } - } - - @Override - public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { - super.onCreateContextMenu(menu, v, menuInfo); - if (!inActionMode()) { - menu.findItem(R.id.multi_select).setVisible(true); - } - MenuItemUtils.setOnClickListeners(menu, CompletedDownloadsFragment.this::onContextItemSelected); - } - } - - public static class DownloadsSortDialog extends ItemSortDialog { - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - sortOrder = UserPreferences.getDownloadsSortedOrder(); - } - - @Override - protected void onAddItem(int title, SortOrder ascending, SortOrder descending, boolean ascendingIsDefault) { - if (ascending == SortOrder.DATE_OLD_NEW || ascending == SortOrder.DURATION_SHORT_LONG - || ascending == SortOrder.EPISODE_TITLE_A_Z || ascending == SortOrder.SIZE_SMALL_LARGE) { - super.onAddItem(title, ascending, descending, ascendingIsDefault); - } - } - - @Override - protected void onSelectionChanged() { - super.onSelectionChanged(); - UserPreferences.setDownloadsSortedOrder(sortOrder); - EventBus.getDefault().post(DownloadLogEvent.listUpdated()); - } - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/CoverFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/CoverFragment.java deleted file mode 100644 index 3076b6e63..000000000 --- a/app/src/main/java/de/danoeh/antennapod/fragment/CoverFragment.java +++ /dev/null @@ -1,341 +0,0 @@ -package de.danoeh.antennapod.fragment; - -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.AnimatorSet; -import android.animation.ObjectAnimator; -import android.content.ClipData; -import android.content.ClipboardManager; -import android.content.Intent; -import android.content.res.Configuration; -import android.graphics.ColorFilter; -import android.graphics.drawable.Drawable; -import android.os.Build; -import android.os.Bundle; -import android.text.TextUtils; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.LinearLayout; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.content.ContextCompat; -import androidx.core.graphics.BlendModeColorFilterCompat; -import androidx.core.graphics.BlendModeCompat; -import androidx.fragment.app.Fragment; -import com.bumptech.glide.Glide; -import com.bumptech.glide.RequestBuilder; -import com.bumptech.glide.load.resource.bitmap.FitCenter; -import com.bumptech.glide.load.resource.bitmap.RoundedCorners; -import com.bumptech.glide.request.RequestOptions; -import com.google.android.material.snackbar.Snackbar; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.activity.MainActivity; -import de.danoeh.antennapod.core.util.ChapterUtils; -import de.danoeh.antennapod.playback.service.PlaybackController; -import de.danoeh.antennapod.ui.common.DateFormatter; -import de.danoeh.antennapod.databinding.CoverFragmentBinding; -import de.danoeh.antennapod.event.playback.PlaybackPositionEvent; -import de.danoeh.antennapod.model.feed.Chapter; -import de.danoeh.antennapod.model.feed.EmbeddedChapterImage; -import de.danoeh.antennapod.model.feed.FeedMedia; -import de.danoeh.antennapod.model.playback.Playable; -import de.danoeh.antennapod.ui.episodes.ImageResourceUtils; -import io.reactivex.Maybe; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.Disposable; -import io.reactivex.schedulers.Schedulers; -import org.apache.commons.lang3.StringUtils; -import org.greenrobot.eventbus.EventBus; -import org.greenrobot.eventbus.Subscribe; -import org.greenrobot.eventbus.ThreadMode; - -import static android.widget.LinearLayout.LayoutParams.MATCH_PARENT; -import static android.widget.LinearLayout.LayoutParams.WRAP_CONTENT; - -/** - * Displays the cover and the title of a FeedItem. - */ -public class CoverFragment extends Fragment { - private static final String TAG = "CoverFragment"; - private CoverFragmentBinding viewBinding; - private PlaybackController controller; - private Disposable disposable; - private int displayedChapterIndex = -1; - private Playable media; - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - viewBinding = CoverFragmentBinding.inflate(inflater); - viewBinding.imgvCover.setOnClickListener(v -> onPlayPause()); - viewBinding.openDescription.setOnClickListener(view -> ((AudioPlayerFragment) requireParentFragment()) - .scrollToPage(AudioPlayerFragment.POS_DESCRIPTION, true)); - ColorFilter colorFilter = BlendModeColorFilterCompat.createBlendModeColorFilterCompat( - viewBinding.txtvPodcastTitle.getCurrentTextColor(), BlendModeCompat.SRC_IN); - viewBinding.butNextChapter.setColorFilter(colorFilter); - viewBinding.butPrevChapter.setColorFilter(colorFilter); - viewBinding.descriptionIcon.setColorFilter(colorFilter); - viewBinding.chapterButton.setOnClickListener(v -> - new ChaptersFragment().show(getChildFragmentManager(), ChaptersFragment.TAG)); - viewBinding.butPrevChapter.setOnClickListener(v -> seekToPrevChapter()); - viewBinding.butNextChapter.setOnClickListener(v -> seekToNextChapter()); - return viewBinding.getRoot(); - } - - @Override - public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - configureForOrientation(getResources().getConfiguration()); - } - - private void loadMediaInfo(boolean includingChapters) { - if (disposable != null) { - disposable.dispose(); - } - disposable = Maybe.create(emitter -> { - Playable media = controller.getMedia(); - if (media != null) { - if (includingChapters) { - ChapterUtils.loadChapters(media, getContext(), false); - } - emitter.onSuccess(media); - } else { - emitter.onComplete(); - } - }).subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(media -> { - this.media = media; - displayMediaInfo(media); - if (media.getChapters() == null && !includingChapters) { - loadMediaInfo(true); - } - }, error -> Log.e(TAG, Log.getStackTraceString(error))); - } - - private void displayMediaInfo(@NonNull Playable media) { - String pubDateStr = DateFormatter.formatAbbrev(getActivity(), media.getPubDate()); - viewBinding.txtvPodcastTitle.setText(StringUtils.stripToEmpty(media.getFeedTitle()) - + "\u00A0" - + "・" - + "\u00A0" - + StringUtils.replace(StringUtils.stripToEmpty(pubDateStr), " ", "\u00A0")); - if (media instanceof FeedMedia) { - Intent openFeed = MainActivity.getIntentToOpenFeed(requireContext(), - ((FeedMedia) media).getItem().getFeedId()); - viewBinding.txtvPodcastTitle.setOnClickListener(v -> startActivity(openFeed)); - } else { - viewBinding.txtvPodcastTitle.setOnClickListener(null); - } - viewBinding.txtvPodcastTitle.setOnLongClickListener(v -> copyText(media.getFeedTitle())); - viewBinding.txtvEpisodeTitle.setText(media.getEpisodeTitle()); - viewBinding.txtvEpisodeTitle.setOnLongClickListener(v -> copyText(media.getEpisodeTitle())); - viewBinding.txtvEpisodeTitle.setOnClickListener(v -> { - int lines = viewBinding.txtvEpisodeTitle.getLineCount(); - int animUnit = 1500; - if (lines > viewBinding.txtvEpisodeTitle.getMaxLines()) { - int titleHeight = viewBinding.txtvEpisodeTitle.getHeight() - - viewBinding.txtvEpisodeTitle.getPaddingTop() - - viewBinding.txtvEpisodeTitle.getPaddingBottom(); - ObjectAnimator verticalMarquee = ObjectAnimator.ofInt( - viewBinding.txtvEpisodeTitle, "scrollY", 0, (lines - viewBinding.txtvEpisodeTitle.getMaxLines()) - * (titleHeight / viewBinding.txtvEpisodeTitle.getMaxLines())) - .setDuration(lines * animUnit); - ObjectAnimator fadeOut = ObjectAnimator.ofFloat( - viewBinding.txtvEpisodeTitle, "alpha", 0); - fadeOut.setStartDelay(animUnit); - fadeOut.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - viewBinding.txtvEpisodeTitle.scrollTo(0, 0); - } - }); - ObjectAnimator fadeBackIn = ObjectAnimator.ofFloat( - viewBinding.txtvEpisodeTitle, "alpha", 1); - AnimatorSet set = new AnimatorSet(); - set.playSequentially(verticalMarquee, fadeOut, fadeBackIn); - set.start(); - } - }); - - displayedChapterIndex = -1; - refreshChapterData(ChapterUtils.getCurrentChapterIndex(media, media.getPosition())); //calls displayCoverImage - updateChapterControlVisibility(); - } - - private void updateChapterControlVisibility() { - boolean chapterControlVisible = false; - if (media.getChapters() != null) { - chapterControlVisible = media.getChapters().size() > 0; - } else if (media instanceof FeedMedia) { - FeedMedia fm = ((FeedMedia) media); - // If an item has chapters but they are not loaded yet, still display the button. - chapterControlVisible = fm.getItem() != null && fm.getItem().hasChapters(); - } - int newVisibility = chapterControlVisible ? View.VISIBLE : View.GONE; - if (viewBinding.chapterButton.getVisibility() != newVisibility) { - viewBinding.chapterButton.setVisibility(newVisibility); - ObjectAnimator.ofFloat(viewBinding.chapterButton, - "alpha", - chapterControlVisible ? 0 : 1, - chapterControlVisible ? 1 : 0) - .start(); - } - } - - private void refreshChapterData(int chapterIndex) { - if (chapterIndex > -1) { - if (media.getPosition() > media.getDuration() || chapterIndex >= media.getChapters().size() - 1) { - displayedChapterIndex = media.getChapters().size() - 1; - viewBinding.butNextChapter.setVisibility(View.INVISIBLE); - } else { - displayedChapterIndex = chapterIndex; - viewBinding.butNextChapter.setVisibility(View.VISIBLE); - } - } - - displayCoverImage(); - } - - private Chapter getCurrentChapter() { - if (media == null || media.getChapters() == null || displayedChapterIndex == -1) { - return null; - } - return media.getChapters().get(displayedChapterIndex); - } - - private void seekToPrevChapter() { - Chapter curr = getCurrentChapter(); - - if (controller == null || curr == null || displayedChapterIndex == -1) { - return; - } - - if (displayedChapterIndex < 1) { - controller.seekTo(0); - } else if ((controller.getPosition() - 10000 * controller.getCurrentPlaybackSpeedMultiplier()) - < curr.getStart()) { - refreshChapterData(displayedChapterIndex - 1); - controller.seekTo((int) media.getChapters().get(displayedChapterIndex).getStart()); - } else { - controller.seekTo((int) curr.getStart()); - } - } - - private void seekToNextChapter() { - if (controller == null || media == null || media.getChapters() == null - || displayedChapterIndex == -1 || displayedChapterIndex + 1 >= media.getChapters().size()) { - return; - } - - refreshChapterData(displayedChapterIndex + 1); - controller.seekTo((int) media.getChapters().get(displayedChapterIndex).getStart()); - } - - @Override - public void onStart() { - super.onStart(); - controller = new PlaybackController(getActivity()) { - @Override - public void loadMediaInfo() { - CoverFragment.this.loadMediaInfo(false); - } - }; - controller.init(); - loadMediaInfo(false); - EventBus.getDefault().register(this); - } - - @Override - public void onStop() { - super.onStop(); - - if (disposable != null) { - disposable.dispose(); - } - controller.release(); - controller = null; - EventBus.getDefault().unregister(this); - } - - @Subscribe(threadMode = ThreadMode.MAIN) - public void onEventMainThread(PlaybackPositionEvent event) { - int newChapterIndex = ChapterUtils.getCurrentChapterIndex(media, event.getPosition()); - if (newChapterIndex > -1 && newChapterIndex != displayedChapterIndex) { - refreshChapterData(newChapterIndex); - } - } - - private void displayCoverImage() { - RequestOptions options = new RequestOptions() - .dontAnimate() - .transform(new FitCenter(), - new RoundedCorners((int) (16 * getResources().getDisplayMetrics().density))); - - RequestBuilder cover = Glide.with(this) - .load(media.getImageLocation()) - .error(Glide.with(this) - .load(ImageResourceUtils.getFallbackImageLocation(media)) - .apply(options)) - .apply(options); - - if (displayedChapterIndex == -1 || media == null || media.getChapters() == null - || TextUtils.isEmpty(media.getChapters().get(displayedChapterIndex).getImageUrl())) { - cover.into(viewBinding.imgvCover); - } else { - Glide.with(this) - .load(EmbeddedChapterImage.getModelFor(media, displayedChapterIndex)) - .apply(options) - .thumbnail(cover) - .error(cover) - .into(viewBinding.imgvCover); - } - } - - @Override - public void onConfigurationChanged(Configuration newConfig) { - super.onConfigurationChanged(newConfig); - configureForOrientation(newConfig); - } - - private void configureForOrientation(Configuration newConfig) { - boolean isPortrait = newConfig.orientation == Configuration.ORIENTATION_PORTRAIT; - - viewBinding.coverFragment.setOrientation(isPortrait ? LinearLayout.VERTICAL : LinearLayout.HORIZONTAL); - - if (isPortrait) { - viewBinding.coverHolder.setLayoutParams(new LinearLayout.LayoutParams(MATCH_PARENT, 0, 1)); - viewBinding.coverFragmentTextContainer.setLayoutParams( - new LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT)); - } else { - viewBinding.coverHolder.setLayoutParams(new LinearLayout.LayoutParams(0, MATCH_PARENT, 1)); - viewBinding.coverFragmentTextContainer.setLayoutParams(new LinearLayout.LayoutParams(0, MATCH_PARENT, 1)); - } - - ((ViewGroup) viewBinding.episodeDetails.getParent()).removeView(viewBinding.episodeDetails); - if (isPortrait) { - viewBinding.coverFragment.addView(viewBinding.episodeDetails); - } else { - viewBinding.coverFragmentTextContainer.addView(viewBinding.episodeDetails); - } - } - - void onPlayPause() { - if (controller == null) { - return; - } - controller.playPause(); - } - - private boolean copyText(String text) { - ClipboardManager clipboardManager = ContextCompat.getSystemService(requireContext(), ClipboardManager.class); - if (clipboardManager != null) { - clipboardManager.setPrimaryClip(ClipData.newPlainText("AntennaPod", text)); - } - if (Build.VERSION.SDK_INT <= 32) { - ((MainActivity) requireActivity()).showSnackbarAbovePlayer( - getResources().getString(R.string.copied_to_clipboard), Snackbar.LENGTH_SHORT); - } - return true; - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/DownloadLogFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/DownloadLogFragment.java deleted file mode 100644 index 54c6d1a9b..000000000 --- a/app/src/main/java/de/danoeh/antennapod/fragment/DownloadLogFragment.java +++ /dev/null @@ -1,123 +0,0 @@ -package de.danoeh.antennapod.fragment; - -import android.os.Bundle; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.widget.AdapterView; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import com.google.android.material.appbar.MaterialToolbar; -import com.google.android.material.bottomsheet.BottomSheetDialogFragment; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.adapter.DownloadLogAdapter; -import de.danoeh.antennapod.event.DownloadLogEvent; -import de.danoeh.antennapod.storage.database.DBReader; -import de.danoeh.antennapod.storage.database.DBWriter; -import de.danoeh.antennapod.databinding.DownloadLogFragmentBinding; -import de.danoeh.antennapod.dialog.DownloadLogDetailsDialog; -import de.danoeh.antennapod.model.download.DownloadResult; -import de.danoeh.antennapod.view.EmptyViewHandler; -import io.reactivex.Observable; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.Disposable; -import io.reactivex.schedulers.Schedulers; -import org.greenrobot.eventbus.EventBus; -import org.greenrobot.eventbus.Subscribe; - -import java.util.ArrayList; -import java.util.List; - -/** - * Shows the download log - */ -public class DownloadLogFragment extends BottomSheetDialogFragment - implements AdapterView.OnItemClickListener, MaterialToolbar.OnMenuItemClickListener { - private static final String TAG = "DownloadLogFragment"; - - private List downloadLog = new ArrayList<>(); - private DownloadLogAdapter adapter; - private Disposable disposable; - private DownloadLogFragmentBinding viewBinding; - - @Override - public void onStart() { - super.onStart(); - loadDownloadLog(); - } - - @Override - public void onStop() { - super.onStop(); - if (disposable != null) { - disposable.dispose(); - } - } - - @Nullable - @Override - public View onCreateView(@NonNull LayoutInflater inflater, - @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - viewBinding = DownloadLogFragmentBinding.inflate(inflater); - viewBinding.toolbar.inflateMenu(R.menu.download_log); - viewBinding.toolbar.setOnMenuItemClickListener(this); - - EmptyViewHandler emptyView = new EmptyViewHandler(getActivity()); - emptyView.setIcon(R.drawable.ic_download); - emptyView.setTitle(R.string.no_log_downloads_head_label); - emptyView.setMessage(R.string.no_log_downloads_label); - emptyView.attachToListView(viewBinding.list); - - adapter = new DownloadLogAdapter(getActivity()); - viewBinding.list.setAdapter(adapter); - viewBinding.list.setOnItemClickListener(this); - viewBinding.list.setNestedScrollingEnabled(true); - EventBus.getDefault().register(this); - return viewBinding.getRoot(); - } - - @Override - public void onDestroyView() { - EventBus.getDefault().unregister(this); - super.onDestroyView(); - } - - @Override - public void onItemClick(AdapterView parent, View view, int position, long id) { - final DownloadResult item = adapter.getItem(position); - if (item != null) { - new DownloadLogDetailsDialog(getContext(), item).show(); - } - } - - @Subscribe - public void onDownloadLogChanged(DownloadLogEvent event) { - loadDownloadLog(); - } - - @Override - public boolean onMenuItemClick(MenuItem item) { - if (item.getItemId() == R.id.clear_logs_item) { - DBWriter.clearDownloadLog(); - return true; - } - return false; - } - - private void loadDownloadLog() { - if (disposable != null) { - disposable.dispose(); - } - disposable = Observable.fromCallable(DBReader::getDownloadLog) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(result -> { - if (result != null) { - downloadLog = result; - adapter.setDownloadLog(downloadLog); - } - }, error -> Log.e(TAG, Log.getStackTraceString(error))); - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/EpisodesListFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/EpisodesListFragment.java deleted file mode 100644 index 0f9e21b6e..000000000 --- a/app/src/main/java/de/danoeh/antennapod/fragment/EpisodesListFragment.java +++ /dev/null @@ -1,463 +0,0 @@ -package de.danoeh.antennapod.fragment; - -import android.content.DialogInterface; -import android.os.Bundle; -import android.util.Log; -import android.view.ContextMenu; -import android.view.KeyEvent; -import android.view.LayoutInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ProgressBar; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.appcompat.widget.Toolbar; -import androidx.core.util.Pair; -import androidx.fragment.app.Fragment; -import androidx.recyclerview.widget.RecyclerView; -import androidx.recyclerview.widget.SimpleItemAnimator; -import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; - -import com.google.android.material.appbar.MaterialToolbar; -import com.google.android.material.snackbar.Snackbar; -import com.leinardi.android.speeddial.SpeedDialView; - -import de.danoeh.antennapod.net.download.serviceinterface.FeedUpdateManager; -import org.greenrobot.eventbus.EventBus; -import org.greenrobot.eventbus.Subscribe; -import org.greenrobot.eventbus.ThreadMode; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.activity.MainActivity; -import de.danoeh.antennapod.adapter.EpisodeItemListAdapter; -import de.danoeh.antennapod.core.dialog.ConfirmationDialog; -import de.danoeh.antennapod.core.menuhandler.MenuItemUtils; -import de.danoeh.antennapod.core.util.FeedItemUtil; -import de.danoeh.antennapod.event.EpisodeDownloadEvent; -import de.danoeh.antennapod.event.FeedItemEvent; -import de.danoeh.antennapod.event.FeedListUpdateEvent; -import de.danoeh.antennapod.event.FeedUpdateRunningEvent; -import de.danoeh.antennapod.event.PlayerStatusEvent; -import de.danoeh.antennapod.event.UnreadItemsUpdateEvent; -import de.danoeh.antennapod.event.playback.PlaybackPositionEvent; -import de.danoeh.antennapod.fragment.actions.EpisodeMultiSelectActionHandler; -import de.danoeh.antennapod.fragment.swipeactions.SwipeActions; -import de.danoeh.antennapod.menuhandler.FeedItemMenuHandler; -import de.danoeh.antennapod.model.feed.FeedItem; -import de.danoeh.antennapod.model.feed.FeedItemFilter; -import de.danoeh.antennapod.view.EmptyViewHandler; -import de.danoeh.antennapod.view.EpisodeItemListRecyclerView; -import de.danoeh.antennapod.view.LiftOnScrollListener; -import de.danoeh.antennapod.view.viewholder.EpisodeItemViewHolder; -import io.reactivex.Completable; -import io.reactivex.Observable; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.Disposable; -import io.reactivex.schedulers.Schedulers; - -/** - * Shows unread or recently published episodes - */ -public abstract class EpisodesListFragment extends Fragment - implements EpisodeItemListAdapter.OnSelectModeListener, Toolbar.OnMenuItemClickListener { - public static final String TAG = "EpisodesListFragment"; - private static final String KEY_UP_ARROW = "up_arrow"; - protected static final int EPISODES_PER_PAGE = 150; - protected int page = 1; - protected boolean isLoadingMore = false; - protected boolean hasMoreItems = false; - private boolean displayUpArrow; - - EpisodeItemListRecyclerView recyclerView; - EpisodeItemListAdapter listAdapter; - EmptyViewHandler emptyView; - SpeedDialView speedDialView; - MaterialToolbar toolbar; - SwipeRefreshLayout swipeRefreshLayout; - SwipeActions swipeActions; - private ProgressBar progressBar; - - @NonNull - List episodes = new ArrayList<>(); - - protected Disposable disposable; - protected TextView txtvInformation; - - @Override - public void onStart() { - super.onStart(); - EventBus.getDefault().register(this); - loadItems(); - } - - @Override - public void onResume() { - super.onResume(); - registerForContextMenu(recyclerView); - } - - @Override - public void onPause() { - super.onPause(); - recyclerView.saveScrollPosition(getPrefName()); - unregisterForContextMenu(recyclerView); - } - - @Override - public void onStop() { - super.onStop(); - EventBus.getDefault().unregister(this); - if (disposable != null) { - disposable.dispose(); - } - } - - @Override - public boolean onMenuItemClick(MenuItem item) { - final int itemId = item.getItemId(); - if (itemId == R.id.refresh_item) { - FeedUpdateManager.getInstance().runOnceOrAsk(requireContext()); - return true; - } else if (itemId == R.id.action_search) { - ((MainActivity) getActivity()).loadChildFragment(SearchFragment.newInstance()); - return true; - } - return false; - } - - @Override - public boolean onContextItemSelected(@NonNull MenuItem item) { - Log.d(TAG, "onContextItemSelected() called with: " + "item = [" + item + "]"); - if (!getUserVisibleHint() || !isVisible() || !isMenuVisible()) { - // The method is called on all fragments in a ViewPager, so this needs to be ignored in invisible ones. - // Apparently, none of the visibility check method works reliably on its own, so we just use all. - return false; - } else if (listAdapter.getLongPressedItem() == null) { - Log.i(TAG, "Selected item or listAdapter was null, ignoring selection"); - return super.onContextItemSelected(item); - } else if (listAdapter.onContextItemSelected(item)) { - return true; - } - FeedItem selectedItem = listAdapter.getLongPressedItem(); - return FeedItemMenuHandler.onMenuItemClicked(this, item.getItemId(), selectedItem); - } - - @NonNull - @Override - public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - super.onCreateView(inflater, container, savedInstanceState); - View root = inflater.inflate(R.layout.episodes_list_fragment, container, false); - txtvInformation = root.findViewById(R.id.txtvInformation); - toolbar = root.findViewById(R.id.toolbar); - toolbar.setOnMenuItemClickListener(this); - toolbar.setOnLongClickListener(v -> { - recyclerView.scrollToPosition(5); - recyclerView.post(() -> recyclerView.smoothScrollToPosition(0)); - return false; - }); - displayUpArrow = getParentFragmentManager().getBackStackEntryCount() != 0; - if (savedInstanceState != null) { - displayUpArrow = savedInstanceState.getBoolean(KEY_UP_ARROW); - } - ((MainActivity) getActivity()).setupToolbarToggle(toolbar, displayUpArrow); - - recyclerView = root.findViewById(R.id.recyclerView); - recyclerView.setRecycledViewPool(((MainActivity) getActivity()).getRecycledViewPool()); - setupLoadMoreScrollListener(); - recyclerView.addOnScrollListener(new LiftOnScrollListener(root.findViewById(R.id.appbar))); - - swipeActions = new SwipeActions(this, getFragmentTag()).attachTo(recyclerView); - swipeActions.setFilter(getFilter()); - - RecyclerView.ItemAnimator animator = recyclerView.getItemAnimator(); - if (animator instanceof SimpleItemAnimator) { - ((SimpleItemAnimator) animator).setSupportsChangeAnimations(false); - } - - swipeRefreshLayout = root.findViewById(R.id.swipeRefresh); - swipeRefreshLayout.setDistanceToTriggerSync(getResources().getInteger(R.integer.swipe_refresh_distance)); - swipeRefreshLayout.setOnRefreshListener(() -> FeedUpdateManager.getInstance().runOnceOrAsk(requireContext())); - - listAdapter = new EpisodeItemListAdapter((MainActivity) getActivity()) { - @Override - public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { - super.onCreateContextMenu(menu, v, menuInfo); - if (!inActionMode()) { - menu.findItem(R.id.multi_select).setVisible(true); - } - MenuItemUtils.setOnClickListeners(menu, EpisodesListFragment.this::onContextItemSelected); - } - }; - listAdapter.setOnSelectModeListener(this); - recyclerView.setAdapter(listAdapter); - progressBar = root.findViewById(R.id.progressBar); - progressBar.setVisibility(View.VISIBLE); - - emptyView = new EmptyViewHandler(getContext()); - emptyView.attachToRecyclerView(recyclerView); - emptyView.setIcon(R.drawable.ic_feed); - emptyView.setTitle(R.string.no_all_episodes_head_label); - emptyView.setMessage(R.string.no_all_episodes_label); - emptyView.updateAdapter(listAdapter); - emptyView.hide(); - - speedDialView = root.findViewById(R.id.fabSD); - speedDialView.setOverlayLayout(root.findViewById(R.id.fabSDOverlay)); - speedDialView.inflate(R.menu.episodes_apply_action_speeddial); - speedDialView.setOnChangeListener(new SpeedDialView.OnChangeListener() { - @Override - public boolean onMainActionSelected() { - return false; - } - - @Override - public void onToggleChanged(boolean open) { - if (open && listAdapter.getSelectedCount() == 0) { - ((MainActivity) getActivity()).showSnackbarAbovePlayer(R.string.no_items_selected, - Snackbar.LENGTH_SHORT); - speedDialView.close(); - } - } - }); - speedDialView.setOnActionSelectedListener(actionItem -> { - int confirmationString = 0; - if (listAdapter.getSelectedItems().size() >= 25 || listAdapter.shouldSelectLazyLoadedItems()) { - // Should ask for confirmation - if (actionItem.getId() == R.id.mark_read_batch) { - confirmationString = R.string.multi_select_mark_played_confirmation; - } else if (actionItem.getId() == R.id.mark_unread_batch) { - confirmationString = R.string.multi_select_mark_unplayed_confirmation; - } - } - if (confirmationString == 0) { - performMultiSelectAction(actionItem.getId()); - } else { - new ConfirmationDialog(getActivity(), R.string.multi_select, confirmationString) { - @Override - public void onConfirmButtonPressed(DialogInterface dialog) { - performMultiSelectAction(actionItem.getId()); - } - }.createNewDialog().show(); - } - return true; - }); - - return root; - } - - private void performMultiSelectAction(int actionItemId) { - EpisodeMultiSelectActionHandler handler = - new EpisodeMultiSelectActionHandler(((MainActivity) getActivity()), actionItemId); - Completable.fromAction( - () -> { - handler.handleAction(listAdapter.getSelectedItems()); - if (listAdapter.shouldSelectLazyLoadedItems()) { - int applyPage = page + 1; - List nextPage; - do { - nextPage = loadMoreData(applyPage); - handler.handleAction(nextPage); - applyPage++; - } while (nextPage.size() == EPISODES_PER_PAGE); - } - }) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(() -> listAdapter.endSelectMode(), - error -> Log.e(TAG, Log.getStackTraceString(error))); - } - - private void setupLoadMoreScrollListener() { - recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { - @Override - public void onScrolled(@NonNull RecyclerView view, int deltaX, int deltaY) { - super.onScrolled(view, deltaX, deltaY); - if (!isLoadingMore && hasMoreItems && recyclerView.isScrolledToBottom()) { - /* The end of the list has been reached. Load more data. */ - page++; - loadMoreItems(); - isLoadingMore = true; - } - } - }); - } - - private void loadMoreItems() { - if (disposable != null) { - disposable.dispose(); - } - isLoadingMore = true; - listAdapter.setDummyViews(1); - listAdapter.notifyItemInserted(listAdapter.getItemCount() - 1); - disposable = Observable.fromCallable(() -> loadMoreData(page)) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - data -> { - if (data.size() < EPISODES_PER_PAGE) { - hasMoreItems = false; - } - episodes.addAll(data); - listAdapter.setDummyViews(0); - listAdapter.updateItems(episodes); - if (listAdapter.shouldSelectLazyLoadedItems()) { - listAdapter.setSelected(episodes.size() - data.size(), episodes.size(), true); - } - }, error -> { - listAdapter.setDummyViews(0); - listAdapter.updateItems(Collections.emptyList()); - Log.e(TAG, Log.getStackTraceString(error)); - }, () -> { - // Make sure to not always load 2 pages at once - recyclerView.post(() -> isLoadingMore = false); - }); - } - - @Override - public void onDestroyView() { - super.onDestroyView(); - listAdapter.endSelectMode(); - } - - @Override - public void onStartSelectMode() { - speedDialView.setVisibility(View.VISIBLE); - } - - @Override - public void onEndSelectMode() { - speedDialView.close(); - speedDialView.setVisibility(View.GONE); - } - - @Subscribe(threadMode = ThreadMode.MAIN) - public void onEventMainThread(FeedItemEvent event) { - Log.d(TAG, "onEventMainThread() called with: " + "event = [" + event + "]"); - for (FeedItem item : event.items) { - int pos = FeedItemUtil.indexOfItemWithId(episodes, item.getId()); - if (pos >= 0) { - episodes.remove(pos); - if (getFilter().matches(item)) { - episodes.add(pos, item); - listAdapter.notifyItemChangedCompat(pos); - } else { - listAdapter.notifyItemRemoved(pos); - } - } - } - } - - @Subscribe(threadMode = ThreadMode.MAIN) - public void onEventMainThread(PlaybackPositionEvent event) { - for (int i = 0; i < listAdapter.getItemCount(); i++) { - EpisodeItemViewHolder holder = (EpisodeItemViewHolder) recyclerView.findViewHolderForAdapterPosition(i); - if (holder != null && holder.isCurrentlyPlayingItem()) { - holder.notifyPlaybackPositionUpdated(event); - break; - } - } - } - - @Subscribe(threadMode = ThreadMode.MAIN) - public void onKeyUp(KeyEvent event) { - if (!isAdded() || !isVisible() || !isMenuVisible()) { - return; - } - switch (event.getKeyCode()) { - case KeyEvent.KEYCODE_T: - recyclerView.smoothScrollToPosition(0); - break; - case KeyEvent.KEYCODE_B: - recyclerView.smoothScrollToPosition(listAdapter.getItemCount() - 1); - break; - default: - break; - } - } - - @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) - public void onEventMainThread(EpisodeDownloadEvent event) { - for (String downloadUrl : event.getUrls()) { - int pos = FeedItemUtil.indexOfItemWithDownloadUrl(episodes, downloadUrl); - if (pos >= 0) { - listAdapter.notifyItemChangedCompat(pos); - } - } - } - - @Subscribe(threadMode = ThreadMode.MAIN) - public void onPlayerStatusChanged(PlayerStatusEvent event) { - loadItems(); - } - - @Subscribe(threadMode = ThreadMode.MAIN) - public void onUnreadItemsChanged(UnreadItemsUpdateEvent event) { - loadItems(); - } - - @Subscribe(threadMode = ThreadMode.MAIN) - public void onFeedListChanged(FeedListUpdateEvent event) { - loadItems(); - } - - void loadItems() { - if (disposable != null) { - disposable.dispose(); - } - disposable = Observable.fromCallable(() -> new Pair<>(loadData(), loadTotalItemCount())) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - data -> { - final boolean restoreScrollPosition = episodes.isEmpty(); - episodes = data.first; - hasMoreItems = !(page == 1 && episodes.size() < EPISODES_PER_PAGE); - progressBar.setVisibility(View.GONE); - listAdapter.setDummyViews(0); - listAdapter.updateItems(episodes); - listAdapter.setTotalNumberOfItems(data.second); - if (restoreScrollPosition) { - recyclerView.restoreScrollPosition(getPrefName()); - } - updateToolbar(); - }, error -> { - listAdapter.setDummyViews(0); - listAdapter.updateItems(Collections.emptyList()); - Log.e(TAG, Log.getStackTraceString(error)); - }); - } - - @NonNull - protected abstract List loadData(); - - @NonNull - protected abstract List loadMoreData(int page); - - protected abstract int loadTotalItemCount(); - - protected abstract FeedItemFilter getFilter(); - - protected abstract String getFragmentTag(); - - protected abstract String getPrefName(); - - protected void updateToolbar() { - } - - @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) - public void onEventMainThread(FeedUpdateRunningEvent event) { - swipeRefreshLayout.setRefreshing(event.isFeedUpdateRunning); - } - - @Override - public void onSaveInstanceState(@NonNull Bundle outState) { - outState.putBoolean(KEY_UP_ARROW, displayUpArrow); - super.onSaveInstanceState(outState); - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/ExternalPlayerFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/ExternalPlayerFragment.java deleted file mode 100644 index 981f8f4b1..000000000 --- a/app/src/main/java/de/danoeh/antennapod/fragment/ExternalPlayerFragment.java +++ /dev/null @@ -1,222 +0,0 @@ -package de.danoeh.antennapod.fragment; - -import android.content.Intent; -import android.os.Bundle; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.ProgressBar; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.fragment.app.Fragment; -import com.bumptech.glide.Glide; -import com.bumptech.glide.request.RequestOptions; -import com.google.android.material.bottomsheet.BottomSheetBehavior; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.activity.MainActivity; -import de.danoeh.antennapod.event.playback.PlaybackPositionEvent; -import de.danoeh.antennapod.event.playback.PlaybackServiceEvent; -import de.danoeh.antennapod.model.playback.MediaType; -import de.danoeh.antennapod.model.playback.Playable; -import de.danoeh.antennapod.playback.base.PlayerStatus; -import de.danoeh.antennapod.playback.service.PlaybackController; -import de.danoeh.antennapod.playback.service.PlaybackService; -import de.danoeh.antennapod.ui.episodes.ImageResourceUtils; -import de.danoeh.antennapod.view.PlayButton; -import io.reactivex.Maybe; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.Disposable; -import io.reactivex.schedulers.Schedulers; -import org.greenrobot.eventbus.EventBus; -import org.greenrobot.eventbus.Subscribe; -import org.greenrobot.eventbus.ThreadMode; - -/** - * Fragment which is supposed to be displayed outside of the MediaplayerActivity. - */ -public class ExternalPlayerFragment extends Fragment { - public static final String TAG = "ExternalPlayerFragment"; - - private ImageView imgvCover; - private TextView txtvTitle; - private PlayButton butPlay; - private TextView feedName; - private ProgressBar progressBar; - private PlaybackController controller; - private Disposable disposable; - - public ExternalPlayerFragment() { - super(); - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - View root = inflater.inflate(R.layout.external_player_fragment, container, false); - imgvCover = root.findViewById(R.id.imgvCover); - txtvTitle = root.findViewById(R.id.txtvTitle); - butPlay = root.findViewById(R.id.butPlay); - feedName = root.findViewById(R.id.txtvAuthor); - progressBar = root.findViewById(R.id.episodeProgress); - - root.findViewById(R.id.fragmentLayout).setOnClickListener(v -> { - Log.d(TAG, "layoutInfo was clicked"); - - if (controller != null && controller.getMedia() != null) { - if (controller.getMedia().getMediaType() == MediaType.AUDIO) { - ((MainActivity) getActivity()).getBottomSheet().setState(BottomSheetBehavior.STATE_EXPANDED); - } else { - Intent intent = PlaybackService.getPlayerActivityIntent(getActivity(), controller.getMedia()); - startActivity(intent); - } - } - }); - return root; - } - - @Override - public void onViewCreated(@NonNull View view, Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - butPlay.setOnClickListener(v -> { - if (controller == null) { - return; - } - if (controller.getMedia() != null && controller.getMedia().getMediaType() == MediaType.VIDEO - && controller.getStatus() != PlayerStatus.PLAYING) { - controller.playPause(); - getContext().startActivity(PlaybackService - .getPlayerActivityIntent(getContext(), controller.getMedia())); - } else { - controller.playPause(); - } - }); - loadMediaInfo(); - } - - private PlaybackController setupPlaybackController() { - return new PlaybackController(getActivity()) { - @Override - protected void updatePlayButtonShowsPlay(boolean showPlay) { - butPlay.setIsShowPlay(showPlay); - } - - @Override - public void loadMediaInfo() { - ExternalPlayerFragment.this.loadMediaInfo(); - } - - @Override - public void onPlaybackEnd() { - ((MainActivity) getActivity()).setPlayerVisible(false); - } - }; - } - - @Override - public void onStart() { - super.onStart(); - controller = setupPlaybackController(); - controller.init(); - loadMediaInfo(); - EventBus.getDefault().register(this); - } - - @Override - public void onStop() { - super.onStop(); - if (controller != null) { - controller.release(); - controller = null; - } - EventBus.getDefault().unregister(this); - } - - @Subscribe(threadMode = ThreadMode.MAIN) - public void onPositionObserverUpdate(PlaybackPositionEvent event) { - if (controller == null) { - return; - } else if (controller.getPosition() == Playable.INVALID_TIME - || controller.getDuration() == Playable.INVALID_TIME) { - return; - } - progressBar.setProgress((int) - ((double) controller.getPosition() / controller.getDuration() * 100)); - } - - @Subscribe(threadMode = ThreadMode.MAIN) - public void onPlaybackServiceChanged(PlaybackServiceEvent event) { - if (event.action == PlaybackServiceEvent.Action.SERVICE_SHUT_DOWN) { - ((MainActivity) getActivity()).setPlayerVisible(false); - } - } - - @Override - public void onDestroy() { - super.onDestroy(); - Log.d(TAG, "Fragment is about to be destroyed"); - if (disposable != null) { - disposable.dispose(); - } - } - - @Override - public void onPause() { - super.onPause(); - if (controller != null) { - controller.pause(); - } - } - - private void loadMediaInfo() { - Log.d(TAG, "Loading media info"); - if (controller == null) { - Log.w(TAG, "loadMediaInfo was called while PlaybackController was null!"); - return; - } - - if (disposable != null) { - disposable.dispose(); - } - disposable = Maybe.fromCallable(() -> controller.getMedia()) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(this::updateUi, - error -> Log.e(TAG, Log.getStackTraceString(error)), - () -> ((MainActivity) getActivity()).setPlayerVisible(false)); - } - - private void updateUi(Playable media) { - if (media == null) { - return; - } - ((MainActivity) getActivity()).setPlayerVisible(true); - txtvTitle.setText(media.getEpisodeTitle()); - feedName.setText(media.getFeedTitle()); - onPositionObserverUpdate(new PlaybackPositionEvent(media.getPosition(), media.getDuration())); - - RequestOptions options = new RequestOptions() - .placeholder(R.color.light_gray) - .error(R.color.light_gray) - .fitCenter() - .dontAnimate(); - - Glide.with(this) - .load(ImageResourceUtils.getEpisodeListImageLocation(media)) - .error(Glide.with(this) - .load(ImageResourceUtils.getFallbackImageLocation(media)) - .apply(options)) - .apply(options) - .into(imgvCover); - - if (controller != null && controller.isPlayingVideoLocally()) { - ((MainActivity) getActivity()).getBottomSheet().setLocked(true); - ((MainActivity) getActivity()).getBottomSheet().setState(BottomSheetBehavior.STATE_COLLAPSED); - } else { - butPlay.setVisibility(View.VISIBLE); - ((MainActivity) getActivity()).getBottomSheet().setLocked(false); - } - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/FeedInfoFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/FeedInfoFragment.java deleted file mode 100644 index c93837851..000000000 --- a/app/src/main/java/de/danoeh/antennapod/fragment/FeedInfoFragment.java +++ /dev/null @@ -1,362 +0,0 @@ -package de.danoeh.antennapod.fragment; - -import android.content.ActivityNotFoundException; -import android.content.ClipData; -import android.content.Context; -import android.content.Intent; -import android.content.res.Configuration; -import android.graphics.LightingColorFilter; -import android.net.Uri; -import android.os.Build; -import android.os.Bundle; -import android.text.TextUtils; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.TextView; -import android.widget.Toast; -import androidx.activity.result.ActivityResultLauncher; -import androidx.activity.result.contract.ActivityResultContracts; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.content.res.AppCompatResources; -import androidx.documentfile.provider.DocumentFile; -import androidx.fragment.app.Fragment; -import com.bumptech.glide.Glide; -import com.bumptech.glide.request.RequestOptions; -import com.google.android.material.appbar.AppBarLayout; -import com.google.android.material.appbar.CollapsingToolbarLayout; -import com.google.android.material.appbar.MaterialToolbar; -import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import com.google.android.material.snackbar.Snackbar; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.activity.MainActivity; -import de.danoeh.antennapod.storage.database.DBReader; -import de.danoeh.antennapod.storage.database.FeedDatabaseWriter; -import de.danoeh.antennapod.core.util.IntentUtils; -import de.danoeh.antennapod.core.util.ShareUtils; -import de.danoeh.antennapod.core.util.syndication.HtmlToPlainText; -import de.danoeh.antennapod.dialog.EditUrlSettingsDialog; -import de.danoeh.antennapod.model.feed.Feed; -import de.danoeh.antennapod.model.feed.FeedFunding; -import de.danoeh.antennapod.ui.glide.FastBlurTransformation; -import de.danoeh.antennapod.ui.statistics.StatisticsFragment; -import de.danoeh.antennapod.ui.statistics.feed.FeedStatisticsFragment; -import de.danoeh.antennapod.view.ToolbarIconTintManager; -import io.reactivex.Completable; -import io.reactivex.Maybe; -import io.reactivex.MaybeOnSubscribe; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.Disposable; -import io.reactivex.schedulers.Schedulers; -import org.apache.commons.lang3.StringUtils; - -import java.util.ArrayList; -import java.util.Iterator; - -/** - * Displays information about a feed. - */ -public class FeedInfoFragment extends Fragment implements MaterialToolbar.OnMenuItemClickListener { - - private static final String EXTRA_FEED_ID = "de.danoeh.antennapod.extra.feedId"; - private static final String TAG = "FeedInfoActivity"; - private final ActivityResultLauncher addLocalFolderLauncher = - registerForActivityResult(new AddLocalFolder(), this::addLocalFolderResult); - - private Feed feed; - private Disposable disposable; - private ImageView imgvCover; - private TextView txtvTitle; - private TextView txtvDescription; - private TextView txtvFundingUrl; - private TextView lblSupport; - private TextView txtvUrl; - private TextView txtvAuthorHeader; - private ImageView imgvBackground; - private View infoContainer; - private View header; - private MaterialToolbar toolbar; - - public static FeedInfoFragment newInstance(Feed feed) { - FeedInfoFragment fragment = new FeedInfoFragment(); - Bundle arguments = new Bundle(); - arguments.putLong(EXTRA_FEED_ID, feed.getId()); - fragment.setArguments(arguments); - return fragment; - } - - private final View.OnClickListener copyUrlToClipboard = new View.OnClickListener() { - @Override - public void onClick(View v) { - if (feed != null && feed.getDownloadUrl() != null) { - String url = feed.getDownloadUrl(); - ClipData clipData = ClipData.newPlainText(url, url); - android.content.ClipboardManager cm = (android.content.ClipboardManager) getContext() - .getSystemService(Context.CLIPBOARD_SERVICE); - cm.setPrimaryClip(clipData); - if (Build.VERSION.SDK_INT <= 32) { - ((MainActivity) getActivity()).showSnackbarAbovePlayer(R.string.copied_to_clipboard, - Snackbar.LENGTH_SHORT); - } - } - } - }; - - @Nullable - @Override - public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, - @Nullable Bundle savedInstanceState) { - View root = inflater.inflate(R.layout.feedinfo, null); - toolbar = root.findViewById(R.id.toolbar); - toolbar.setTitle(""); - toolbar.inflateMenu(R.menu.feedinfo); - toolbar.setNavigationOnClickListener(v -> getParentFragmentManager().popBackStack()); - toolbar.setOnMenuItemClickListener(this); - refreshToolbarState(); - - AppBarLayout appBar = root.findViewById(R.id.appBar); - CollapsingToolbarLayout collapsingToolbar = root.findViewById(R.id.collapsing_toolbar); - ToolbarIconTintManager iconTintManager = new ToolbarIconTintManager(getContext(), toolbar, collapsingToolbar) { - @Override - protected void doTint(Context themedContext) { - toolbar.getMenu().findItem(R.id.visit_website_item) - .setIcon(AppCompatResources.getDrawable(themedContext, R.drawable.ic_web)); - toolbar.getMenu().findItem(R.id.share_item) - .setIcon(AppCompatResources.getDrawable(themedContext, R.drawable.ic_share)); - } - }; - iconTintManager.updateTint(); - appBar.addOnOffsetChangedListener(iconTintManager); - - imgvCover = root.findViewById(R.id.imgvCover); - txtvTitle = root.findViewById(R.id.txtvTitle); - txtvAuthorHeader = root.findViewById(R.id.txtvAuthor); - imgvBackground = root.findViewById(R.id.imgvBackground); - header = root.findViewById(R.id.headerContainer); - infoContainer = root.findViewById(R.id.infoContainer); - root.findViewById(R.id.butShowInfo).setVisibility(View.INVISIBLE); - root.findViewById(R.id.butShowSettings).setVisibility(View.INVISIBLE); - root.findViewById(R.id.butFilter).setVisibility(View.INVISIBLE); - // https://github.com/bumptech/glide/issues/529 - imgvBackground.setColorFilter(new LightingColorFilter(0xff828282, 0x000000)); - - txtvDescription = root.findViewById(R.id.txtvDescription); - txtvUrl = root.findViewById(R.id.txtvUrl); - lblSupport = root.findViewById(R.id.lblSupport); - txtvFundingUrl = root.findViewById(R.id.txtvFundingUrl); - - txtvUrl.setOnClickListener(copyUrlToClipboard); - - long feedId = getArguments().getLong(EXTRA_FEED_ID); - getParentFragmentManager().beginTransaction().replace(R.id.statisticsFragmentContainer, - FeedStatisticsFragment.newInstance(feedId, false), "feed_statistics_fragment") - .commitAllowingStateLoss(); - - root.findViewById(R.id.btnvOpenStatistics).setOnClickListener(view -> { - StatisticsFragment fragment = new StatisticsFragment(); - ((MainActivity) getActivity()).loadChildFragment(fragment, TransitionEffect.SLIDE); - }); - - return root; - } - - @Override - public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - long feedId = getArguments().getLong(EXTRA_FEED_ID); - disposable = Maybe.create((MaybeOnSubscribe) emitter -> { - Feed feed = DBReader.getFeed(feedId); - if (feed != null) { - emitter.onSuccess(feed); - } else { - emitter.onComplete(); - } - }) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(result -> { - feed = result; - showFeed(); - }, error -> Log.d(TAG, Log.getStackTraceString(error)), () -> { }); - } - - @Override - public void onConfigurationChanged(@NonNull Configuration newConfig) { - super.onConfigurationChanged(newConfig); - if (header == null || infoContainer == null) { - return; - } - int horizontalSpacing = (int) getResources().getDimension(R.dimen.additional_horizontal_spacing); - header.setPadding(horizontalSpacing, header.getPaddingTop(), horizontalSpacing, header.getPaddingBottom()); - infoContainer.setPadding(horizontalSpacing, infoContainer.getPaddingTop(), - horizontalSpacing, infoContainer.getPaddingBottom()); - } - - private void showFeed() { - Log.d(TAG, "Language is " + feed.getLanguage()); - Log.d(TAG, "Author is " + feed.getAuthor()); - Log.d(TAG, "URL is " + feed.getDownloadUrl()); - Glide.with(this) - .load(feed.getImageUrl()) - .apply(new RequestOptions() - .placeholder(R.color.light_gray) - .error(R.color.light_gray) - .fitCenter() - .dontAnimate()) - .into(imgvCover); - Glide.with(this) - .load(feed.getImageUrl()) - .apply(new RequestOptions() - .placeholder(R.color.image_readability_tint) - .error(R.color.image_readability_tint) - .transform(new FastBlurTransformation()) - .dontAnimate()) - .into(imgvBackground); - - txtvTitle.setText(feed.getTitle()); - txtvTitle.setMaxLines(6); - - String description = HtmlToPlainText.getPlainText(feed.getDescription()); - - txtvDescription.setText(description); - - if (!TextUtils.isEmpty(feed.getAuthor())) { - txtvAuthorHeader.setText(feed.getAuthor()); - } - - txtvUrl.setText(feed.getDownloadUrl()); - txtvUrl.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_paperclip, 0); - - if (feed.getPaymentLinks() == null || feed.getPaymentLinks().size() == 0) { - lblSupport.setVisibility(View.GONE); - txtvFundingUrl.setVisibility(View.GONE); - } else { - lblSupport.setVisibility(View.VISIBLE); - ArrayList fundingList = feed.getPaymentLinks(); - - // Filter for duplicates, but keep items in the order that they have in the feed. - Iterator i = fundingList.iterator(); - while (i.hasNext()) { - FeedFunding funding = i.next(); - for (FeedFunding other : fundingList) { - if (TextUtils.equals(other.url, funding.url)) { - if (other.content != null && funding.content != null - && other.content.length() > funding.content.length()) { - i.remove(); - break; - } - } - } - } - - StringBuilder str = new StringBuilder(); - for (FeedFunding funding : fundingList) { - str.append(funding.content.isEmpty() - ? getContext().getResources().getString(R.string.support_podcast) - : funding.content).append(" ").append(funding.url); - str.append("\n"); - } - str = new StringBuilder(StringUtils.trim(str.toString())); - txtvFundingUrl.setText(str.toString()); - } - - refreshToolbarState(); - } - - @Override - public void onDestroy() { - super.onDestroy(); - if (disposable != null) { - disposable.dispose(); - } - } - - private void refreshToolbarState() { - toolbar.getMenu().findItem(R.id.reconnect_local_folder).setVisible(feed != null && feed.isLocalFeed()); - toolbar.getMenu().findItem(R.id.share_item).setVisible(feed != null && !feed.isLocalFeed()); - toolbar.getMenu().findItem(R.id.visit_website_item).setVisible(feed != null && feed.getLink() != null - && IntentUtils.isCallable(getContext(), new Intent(Intent.ACTION_VIEW, Uri.parse(feed.getLink())))); - toolbar.getMenu().findItem(R.id.edit_feed_url_item).setVisible(feed != null && !feed.isLocalFeed()); - } - - @Override - public boolean onMenuItemClick(MenuItem item) { - if (feed == null) { - ((MainActivity) getActivity()).showSnackbarAbovePlayer( - R.string.please_wait_for_data, Toast.LENGTH_LONG); - return false; - } - if (item.getItemId() == R.id.visit_website_item) { - IntentUtils.openInBrowser(getContext(), feed.getLink()); - } else if (item.getItemId() == R.id.share_item) { - ShareUtils.shareFeedLink(getContext(), feed); - } else if (item.getItemId() == R.id.reconnect_local_folder) { - MaterialAlertDialogBuilder alert = new MaterialAlertDialogBuilder(getContext()); - alert.setMessage(R.string.reconnect_local_folder_warning); - alert.setPositiveButton(android.R.string.ok, (dialog, which) -> { - try { - addLocalFolderLauncher.launch(null); - } catch (ActivityNotFoundException e) { - Log.e(TAG, "No activity found. Should never happen..."); - } - }); - alert.setNegativeButton(android.R.string.cancel, null); - alert.show(); - } else if (item.getItemId() == R.id.edit_feed_url_item) { - new EditUrlSettingsDialog(getActivity(), feed) { - @Override - protected void setUrl(String url) { - feed.setDownloadUrl(url); - txtvUrl.setText(feed.getDownloadUrl()); - txtvUrl.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_paperclip, 0); - } - }.show(); - } else { - return false; - } - return true; - } - - private void addLocalFolderResult(final Uri uri) { - if (uri == null) { - return; - } - reconnectLocalFolder(uri); - } - - private void reconnectLocalFolder(Uri uri) { - if (feed == null) { - return; - } - - Completable.fromAction(() -> { - getActivity().getContentResolver() - .takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION); - DocumentFile documentFile = DocumentFile.fromTreeUri(getContext(), uri); - if (documentFile == null) { - throw new IllegalArgumentException("Unable to retrieve document tree"); - } - feed.setDownloadUrl(Feed.PREFIX_LOCAL_FOLDER + uri.toString()); - FeedDatabaseWriter.updateFeed(getContext(), feed, true); - }) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - () -> ((MainActivity) getActivity()) - .showSnackbarAbovePlayer(android.R.string.ok, Snackbar.LENGTH_SHORT), - error -> ((MainActivity) getActivity()) - .showSnackbarAbovePlayer(error.getLocalizedMessage(), Snackbar.LENGTH_LONG)); - } - - private static class AddLocalFolder extends ActivityResultContracts.OpenDocumentTree { - @NonNull - @Override - public Intent createIntent(@NonNull final Context context, @Nullable final Uri input) { - return super.createIntent(context, input) - .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - } - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/FeedItemlistFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/FeedItemlistFragment.java deleted file mode 100644 index 1884012ea..000000000 --- a/app/src/main/java/de/danoeh/antennapod/fragment/FeedItemlistFragment.java +++ /dev/null @@ -1,653 +0,0 @@ -package de.danoeh.antennapod.fragment; - -import android.content.Context; -import android.content.res.Configuration; -import android.graphics.LightingColorFilter; -import android.os.Bundle; -import android.util.Log; -import android.view.ContextMenu; -import android.view.KeyEvent; -import android.view.LayoutInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.widget.AdapterView; -import android.widget.Toast; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.content.res.AppCompatResources; -import androidx.fragment.app.Fragment; -import androidx.recyclerview.widget.RecyclerView; - -import com.bumptech.glide.Glide; -import com.bumptech.glide.request.RequestOptions; -import com.google.android.material.appbar.MaterialToolbar; -import com.google.android.material.snackbar.Snackbar; -import com.leinardi.android.speeddial.SpeedDialView; - -import de.danoeh.antennapod.net.download.serviceinterface.FeedUpdateManager; -import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.Validate; -import org.greenrobot.eventbus.EventBus; -import org.greenrobot.eventbus.Subscribe; -import org.greenrobot.eventbus.ThreadMode; - -import java.util.Collections; -import java.util.List; -import java.util.concurrent.ExecutionException; - -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.activity.MainActivity; -import de.danoeh.antennapod.adapter.EpisodeItemListAdapter; -import de.danoeh.antennapod.event.FeedEvent; -import de.danoeh.antennapod.core.menuhandler.MenuItemUtils; -import de.danoeh.antennapod.storage.database.DBReader; -import de.danoeh.antennapod.storage.database.DBWriter; -import de.danoeh.antennapod.storage.database.FeedItemPermutors; -import de.danoeh.antennapod.core.util.FeedItemUtil; -import de.danoeh.antennapod.core.util.IntentUtils; -import de.danoeh.antennapod.core.util.ShareUtils; -import de.danoeh.antennapod.core.util.gui.MoreContentListFooterUtil; -import de.danoeh.antennapod.databinding.FeedItemListFragmentBinding; -import de.danoeh.antennapod.databinding.MultiSelectSpeedDialBinding; -import de.danoeh.antennapod.dialog.DownloadLogDetailsDialog; -import de.danoeh.antennapod.dialog.FeedItemFilterDialog; -import de.danoeh.antennapod.dialog.ItemSortDialog; -import de.danoeh.antennapod.dialog.RemoveFeedDialog; -import de.danoeh.antennapod.dialog.RenameItemDialog; -import de.danoeh.antennapod.event.EpisodeDownloadEvent; -import de.danoeh.antennapod.event.FavoritesEvent; -import de.danoeh.antennapod.event.FeedItemEvent; -import de.danoeh.antennapod.event.FeedListUpdateEvent; -import de.danoeh.antennapod.event.FeedUpdateRunningEvent; -import de.danoeh.antennapod.event.PlayerStatusEvent; -import de.danoeh.antennapod.event.QueueEvent; -import de.danoeh.antennapod.event.UnreadItemsUpdateEvent; -import de.danoeh.antennapod.event.playback.PlaybackPositionEvent; -import de.danoeh.antennapod.fragment.actions.EpisodeMultiSelectActionHandler; -import de.danoeh.antennapod.fragment.swipeactions.SwipeActions; -import de.danoeh.antennapod.menuhandler.FeedItemMenuHandler; -import de.danoeh.antennapod.model.download.DownloadResult; -import de.danoeh.antennapod.model.feed.Feed; -import de.danoeh.antennapod.model.feed.FeedItem; -import de.danoeh.antennapod.model.feed.FeedItemFilter; -import de.danoeh.antennapod.model.feed.SortOrder; -import de.danoeh.antennapod.storage.preferences.UserPreferences; -import de.danoeh.antennapod.ui.glide.FastBlurTransformation; -import de.danoeh.antennapod.view.ToolbarIconTintManager; -import de.danoeh.antennapod.view.viewholder.EpisodeItemViewHolder; -import io.reactivex.Maybe; -import io.reactivex.Observable; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.Disposable; -import io.reactivex.schedulers.Schedulers; - -/** - * Displays a list of FeedItems. - */ -public class FeedItemlistFragment extends Fragment implements AdapterView.OnItemClickListener, - MaterialToolbar.OnMenuItemClickListener, EpisodeItemListAdapter.OnSelectModeListener { - public static final String TAG = "ItemlistFragment"; - private static final String ARGUMENT_FEED_ID = "argument.de.danoeh.antennapod.feed_id"; - private static final String KEY_UP_ARROW = "up_arrow"; - - private FeedItemListAdapter adapter; - private SwipeActions swipeActions; - private MoreContentListFooterUtil nextPageLoader; - private boolean displayUpArrow; - private long feedID; - private Feed feed; - private boolean headerCreated = false; - private Disposable disposable; - private FeedItemListFragmentBinding viewBinding; - private MultiSelectSpeedDialBinding speedDialBinding; - - /** - * Creates new ItemlistFragment which shows the Feeditems of a specific - * feed. Sets 'showFeedtitle' to false - * - * @param feedId The id of the feed to show - * @return the newly created instance of an ItemlistFragment - */ - public static FeedItemlistFragment newInstance(long feedId) { - FeedItemlistFragment i = new FeedItemlistFragment(); - Bundle b = new Bundle(); - b.putLong(ARGUMENT_FEED_ID, feedId); - i.setArguments(b); - return i; - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - Bundle args = getArguments(); - Validate.notNull(args); - feedID = args.getLong(ARGUMENT_FEED_ID); - } - - @Nullable - @Override - public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, - @Nullable Bundle savedInstanceState) { - viewBinding = FeedItemListFragmentBinding.inflate(inflater); - speedDialBinding = MultiSelectSpeedDialBinding.bind(viewBinding.getRoot()); - viewBinding.toolbar.inflateMenu(R.menu.feedlist); - viewBinding.toolbar.setOnMenuItemClickListener(this); - viewBinding.toolbar.setOnLongClickListener(v -> { - viewBinding.recyclerView.scrollToPosition(5); - viewBinding.recyclerView.post(() -> viewBinding.recyclerView.smoothScrollToPosition(0)); - viewBinding.appBar.setExpanded(true); - return false; - }); - displayUpArrow = getParentFragmentManager().getBackStackEntryCount() != 0; - if (savedInstanceState != null) { - displayUpArrow = savedInstanceState.getBoolean(KEY_UP_ARROW); - } - ((MainActivity) getActivity()).setupToolbarToggle(viewBinding.toolbar, displayUpArrow); - updateToolbar(); - - viewBinding.recyclerView.setRecycledViewPool(((MainActivity) getActivity()).getRecycledViewPool()); - adapter = new FeedItemListAdapter((MainActivity) getActivity()); - adapter.setOnSelectModeListener(this); - viewBinding.recyclerView.setAdapter(adapter); - swipeActions = new SwipeActions(this, TAG).attachTo(viewBinding.recyclerView); - viewBinding.progressBar.setVisibility(View.VISIBLE); - - ToolbarIconTintManager iconTintManager = new ToolbarIconTintManager( - getContext(), viewBinding.toolbar, viewBinding.collapsingToolbar) { - @Override - protected void doTint(Context themedContext) { - viewBinding.toolbar.getMenu().findItem(R.id.refresh_item) - .setIcon(AppCompatResources.getDrawable(themedContext, R.drawable.ic_refresh)); - viewBinding.toolbar.getMenu().findItem(R.id.action_search) - .setIcon(AppCompatResources.getDrawable(themedContext, R.drawable.ic_search)); - } - }; - iconTintManager.updateTint(); - viewBinding.appBar.addOnOffsetChangedListener(iconTintManager); - - nextPageLoader = new MoreContentListFooterUtil(viewBinding.moreContent.moreContentListFooter); - nextPageLoader.setClickListener(() -> { - if (feed != null) { - FeedUpdateManager.getInstance().runOnce(getContext(), feed, true); - } - }); - viewBinding.recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { - @Override - public void onScrolled(@NonNull RecyclerView view, int deltaX, int deltaY) { - super.onScrolled(view, deltaX, deltaY); - boolean hasMorePages = feed != null && feed.isPaged() && feed.getNextPageLink() != null; - boolean pageLoaderVisible = viewBinding.recyclerView.isScrolledToBottom() && hasMorePages; - nextPageLoader.getRoot().setVisibility(pageLoaderVisible ? View.VISIBLE : View.GONE); - viewBinding.recyclerView.setPadding( - viewBinding.recyclerView.getPaddingLeft(), 0, viewBinding.recyclerView.getPaddingRight(), - pageLoaderVisible ? nextPageLoader.getRoot().getMeasuredHeight() : 0); - } - }); - - EventBus.getDefault().register(this); - - viewBinding.swipeRefresh.setDistanceToTriggerSync(getResources().getInteger(R.integer.swipe_refresh_distance)); - viewBinding.swipeRefresh.setOnRefreshListener(() -> - FeedUpdateManager.getInstance().runOnceOrAsk(requireContext(), feed)); - - loadItems(); - - // Init action UI (via a FAB Speed Dial) - speedDialBinding.fabSD.setOverlayLayout(speedDialBinding.fabSDOverlay); - speedDialBinding.fabSD.inflate(R.menu.episodes_apply_action_speeddial); - speedDialBinding.fabSD.setOnChangeListener(new SpeedDialView.OnChangeListener() { - @Override - public boolean onMainActionSelected() { - return false; - } - - @Override - public void onToggleChanged(boolean open) { - if (open && adapter.getSelectedCount() == 0) { - ((MainActivity) getActivity()).showSnackbarAbovePlayer(R.string.no_items_selected, - Snackbar.LENGTH_SHORT); - speedDialBinding.fabSD.close(); - } - } - }); - speedDialBinding.fabSD.setOnActionSelectedListener(actionItem -> { - new EpisodeMultiSelectActionHandler(((MainActivity) getActivity()), actionItem.getId()) - .handleAction(adapter.getSelectedItems()); - adapter.endSelectMode(); - return true; - }); - return viewBinding.getRoot(); - } - - @Override - public void onDestroyView() { - super.onDestroyView(); - - EventBus.getDefault().unregister(this); - if (disposable != null) { - disposable.dispose(); - } - adapter.endSelectMode(); - } - - @Override - public void onSaveInstanceState(@NonNull Bundle outState) { - outState.putBoolean(KEY_UP_ARROW, displayUpArrow); - super.onSaveInstanceState(outState); - } - - private void updateToolbar() { - if (feed == null) { - return; - } - viewBinding.toolbar.getMenu().findItem(R.id.visit_website_item).setVisible(feed.getLink() != null); - viewBinding.toolbar.getMenu().findItem(R.id.refresh_complete_item).setVisible(feed.isPaged()); - if (StringUtils.isBlank(feed.getLink())) { - viewBinding.toolbar.getMenu().findItem(R.id.visit_website_item).setVisible(false); - } - if (feed.isLocalFeed()) { - viewBinding.toolbar.getMenu().findItem(R.id.share_item).setVisible(false); - } - } - - @Override - public void onConfigurationChanged(@NonNull Configuration newConfig) { - super.onConfigurationChanged(newConfig); - int horizontalSpacing = (int) getResources().getDimension(R.dimen.additional_horizontal_spacing); - viewBinding.header.headerContainer.setPadding( - horizontalSpacing, viewBinding.header.headerContainer.getPaddingTop(), - horizontalSpacing, viewBinding.header.headerContainer.getPaddingBottom()); - } - - @Override - public boolean onMenuItemClick(MenuItem item) { - if (feed == null) { - ((MainActivity) getActivity()).showSnackbarAbovePlayer( - R.string.please_wait_for_data, Toast.LENGTH_LONG); - return true; - } - if (item.getItemId() == R.id.visit_website_item) { - IntentUtils.openInBrowser(getContext(), feed.getLink()); - } else if (item.getItemId() == R.id.share_item) { - ShareUtils.shareFeedLink(getContext(), feed); - } else if (item.getItemId() == R.id.refresh_item) { - FeedUpdateManager.getInstance().runOnceOrAsk(getContext(), feed); - } else if (item.getItemId() == R.id.refresh_complete_item) { - new Thread(() -> { - feed.setNextPageLink(feed.getDownloadUrl()); - feed.setPageNr(0); - try { - DBWriter.resetPagedFeedPage(feed).get(); - FeedUpdateManager.getInstance().runOnce(getContext(), feed); - } catch (ExecutionException | InterruptedException e) { - throw new RuntimeException(e); - } - }).start(); - } else if (item.getItemId() == R.id.sort_items) { - SingleFeedSortDialog.newInstance(feed).show(getChildFragmentManager(), "SortDialog"); - } else if (item.getItemId() == R.id.rename_item) { - new RenameItemDialog(getActivity(), feed).show(); - } else if (item.getItemId() == R.id.remove_feed) { - RemoveFeedDialog.show(getContext(), feed, () -> { - ((MainActivity) getActivity()).loadFragment(UserPreferences.getDefaultPage(), null); - // Make sure fragment is hidden before actually starting to delete - getActivity().getSupportFragmentManager().executePendingTransactions(); - }); - } else if (item.getItemId() == R.id.action_search) { - ((MainActivity) getActivity()).loadChildFragment(SearchFragment.newInstance(feed.getId(), feed.getTitle())); - } else { - return false; - } - return true; - } - - @Override - public boolean onContextItemSelected(@NonNull MenuItem item) { - FeedItem selectedItem = adapter.getLongPressedItem(); - if (selectedItem == null) { - Log.i(TAG, "Selected item at current position was null, ignoring selection"); - return super.onContextItemSelected(item); - } - if (adapter.onContextItemSelected(item)) { - return true; - } - return FeedItemMenuHandler.onMenuItemClicked(this, item.getItemId(), selectedItem); - } - - @Override - public void onItemClick(AdapterView parent, View view, int position, long id) { - MainActivity activity = (MainActivity) getActivity(); - long[] ids = FeedItemUtil.getIds(feed.getItems()); - activity.loadChildFragment(ItemPagerFragment.newInstance(ids, position)); - } - - @Subscribe(threadMode = ThreadMode.MAIN) - public void onEvent(FeedEvent event) { - Log.d(TAG, "onEvent() called with: " + "event = [" + event + "]"); - if (event.feedId == feedID) { - loadItems(); - } - } - - @Subscribe(threadMode = ThreadMode.MAIN) - public void onEventMainThread(FeedItemEvent event) { - Log.d(TAG, "onEventMainThread() called with: " + "event = [" + event + "]"); - if (feed == null || feed.getItems() == null) { - return; - } - for (int i = 0, size = event.items.size(); i < size; i++) { - FeedItem item = event.items.get(i); - int pos = FeedItemUtil.indexOfItemWithId(feed.getItems(), item.getId()); - if (pos >= 0) { - feed.getItems().remove(pos); - feed.getItems().add(pos, item); - adapter.notifyItemChangedCompat(pos); - } - } - } - - @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) - public void onEventMainThread(EpisodeDownloadEvent event) { - if (feed == null) { - return; - } - for (String downloadUrl : event.getUrls()) { - int pos = FeedItemUtil.indexOfItemWithDownloadUrl(feed.getItems(), downloadUrl); - if (pos >= 0) { - adapter.notifyItemChangedCompat(pos); - } - } - } - - @Subscribe(threadMode = ThreadMode.MAIN) - public void onEventMainThread(PlaybackPositionEvent event) { - for (int i = 0; i < adapter.getItemCount(); i++) { - EpisodeItemViewHolder holder = (EpisodeItemViewHolder) - viewBinding.recyclerView.findViewHolderForAdapterPosition(i); - if (holder != null && holder.isCurrentlyPlayingItem()) { - holder.notifyPlaybackPositionUpdated(event); - break; - } - } - } - - @Subscribe(threadMode = ThreadMode.MAIN) - public void favoritesChanged(FavoritesEvent event) { - updateUi(); - } - - @Subscribe(threadMode = ThreadMode.MAIN) - public void onQueueChanged(QueueEvent event) { - updateUi(); - } - - @Override - public void onStartSelectMode() { - swipeActions.detach(); - if (feed.isLocalFeed()) { - speedDialBinding.fabSD.removeActionItemById(R.id.download_batch); - } - speedDialBinding.fabSD.removeActionItemById(R.id.remove_all_inbox_item); - speedDialBinding.fabSD.setVisibility(View.VISIBLE); - updateToolbar(); - } - - @Override - public void onEndSelectMode() { - speedDialBinding.fabSD.close(); - speedDialBinding.fabSD.setVisibility(View.GONE); - swipeActions.attachTo(viewBinding.recyclerView); - } - - private void updateUi() { - loadItems(); - } - - @Subscribe(threadMode = ThreadMode.MAIN) - public void onPlayerStatusChanged(PlayerStatusEvent event) { - updateUi(); - } - - @Subscribe(threadMode = ThreadMode.MAIN) - public void onUnreadItemsChanged(UnreadItemsUpdateEvent event) { - updateUi(); - } - - @Subscribe(threadMode = ThreadMode.MAIN) - public void onFeedListChanged(FeedListUpdateEvent event) { - if (feed != null && event.contains(feed)) { - updateUi(); - } - } - - @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) - public void onEventMainThread(FeedUpdateRunningEvent event) { - nextPageLoader.setLoadingState(event.isFeedUpdateRunning); - if (!event.isFeedUpdateRunning) { - nextPageLoader.getRoot().setVisibility(View.GONE); - } - viewBinding.swipeRefresh.setRefreshing(event.isFeedUpdateRunning); - } - - private void refreshHeaderView() { - setupHeaderView(); - if (viewBinding == null || feed == null) { - Log.e(TAG, "Unable to refresh header view"); - return; - } - loadFeedImage(); - if (feed.hasLastUpdateFailed()) { - viewBinding.header.txtvFailure.setVisibility(View.VISIBLE); - } else { - viewBinding.header.txtvFailure.setVisibility(View.GONE); - } - if (!feed.getPreferences().getKeepUpdated()) { - viewBinding.header.txtvUpdatesDisabled.setText(R.string.updates_disabled_label); - viewBinding.header.txtvUpdatesDisabled.setVisibility(View.VISIBLE); - } else { - viewBinding.header.txtvUpdatesDisabled.setVisibility(View.GONE); - } - viewBinding.header.txtvTitle.setText(feed.getTitle()); - viewBinding.header.txtvAuthor.setText(feed.getAuthor()); - if (feed.getItemFilter() != null) { - FeedItemFilter filter = feed.getItemFilter(); - if (filter.getValues().length > 0) { - viewBinding.header.txtvInformation.setText(R.string.filtered_label); - viewBinding.header.txtvInformation.setOnClickListener(l -> - FeedItemFilterDialog.newInstance(feed).show(getChildFragmentManager(), null)); - viewBinding.header.txtvInformation.setVisibility(View.VISIBLE); - } else { - viewBinding.header.txtvInformation.setVisibility(View.GONE); - } - } else { - viewBinding.header.txtvInformation.setVisibility(View.GONE); - } - } - - private void setupHeaderView() { - if (feed == null || headerCreated) { - return; - } - - // https://github.com/bumptech/glide/issues/529 - viewBinding.imgvBackground.setColorFilter(new LightingColorFilter(0xff666666, 0x000000)); - viewBinding.header.butShowInfo.setOnClickListener(v -> showFeedInfo()); - viewBinding.header.imgvCover.setOnClickListener(v -> showFeedInfo()); - viewBinding.header.butShowSettings.setOnClickListener(v -> { - if (feed != null) { - FeedSettingsFragment fragment = FeedSettingsFragment.newInstance(feed); - ((MainActivity) getActivity()).loadChildFragment(fragment, TransitionEffect.SLIDE); - } - }); - viewBinding.header.butFilter.setOnClickListener(v -> - FeedItemFilterDialog.newInstance(feed).show(getChildFragmentManager(), null)); - viewBinding.header.txtvFailure.setOnClickListener(v -> showErrorDetails()); - headerCreated = true; - } - - private void showErrorDetails() { - Maybe.fromCallable( - () -> { - List feedDownloadLog = DBReader.getFeedDownloadLog(feedID); - if (feedDownloadLog.size() == 0 || feedDownloadLog.get(0).isSuccessful()) { - return null; - } - return feedDownloadLog.get(0); - }) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - downloadStatus -> new DownloadLogDetailsDialog(getContext(), downloadStatus).show(), - error -> error.printStackTrace(), - () -> new DownloadLogFragment().show(getChildFragmentManager(), null)); - } - - private void showFeedInfo() { - if (feed != null) { - FeedInfoFragment fragment = FeedInfoFragment.newInstance(feed); - ((MainActivity) getActivity()).loadChildFragment(fragment, TransitionEffect.SLIDE); - } - } - - private void loadFeedImage() { - Glide.with(this) - .load(feed.getImageUrl()) - .apply(new RequestOptions() - .placeholder(R.color.image_readability_tint) - .error(R.color.image_readability_tint) - .transform(new FastBlurTransformation()) - .dontAnimate()) - .into(viewBinding.imgvBackground); - - Glide.with(this) - .load(feed.getImageUrl()) - .apply(new RequestOptions() - .placeholder(R.color.light_gray) - .error(R.color.light_gray) - .fitCenter() - .dontAnimate()) - .into(viewBinding.header.imgvCover); - } - - private void loadItems() { - if (disposable != null) { - disposable.dispose(); - } - disposable = Observable.fromCallable(this::loadData) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - result -> { - feed = result; - swipeActions.setFilter(feed.getItemFilter()); - refreshHeaderView(); - viewBinding.progressBar.setVisibility(View.GONE); - adapter.setDummyViews(0); - adapter.updateItems(feed.getItems()); - updateToolbar(); - }, error -> { - feed = null; - refreshHeaderView(); - adapter.setDummyViews(0); - adapter.updateItems(Collections.emptyList()); - updateToolbar(); - Log.e(TAG, Log.getStackTraceString(error)); - }); - } - - @Nullable - private Feed loadData() { - Feed feed = DBReader.getFeed(feedID, true); - if (feed == null) { - return null; - } - DBReader.loadAdditionalFeedItemListData(feed.getItems()); - if (feed.getSortOrder() != null) { - List feedItems = feed.getItems(); - FeedItemPermutors.getPermutor(feed.getSortOrder()).reorder(feedItems); - feed.setItems(feedItems); - } - return feed; - } - - @Subscribe(threadMode = ThreadMode.MAIN) - public void onKeyUp(KeyEvent event) { - if (!isAdded() || !isVisible() || !isMenuVisible()) { - return; - } - switch (event.getKeyCode()) { - case KeyEvent.KEYCODE_T: - viewBinding.recyclerView.smoothScrollToPosition(0); - break; - case KeyEvent.KEYCODE_B: - viewBinding.recyclerView.smoothScrollToPosition(adapter.getItemCount() - 1); - break; - default: - break; - } - } - - private class FeedItemListAdapter extends EpisodeItemListAdapter { - public FeedItemListAdapter(MainActivity mainActivity) { - super(mainActivity); - } - - @Override - protected void beforeBindViewHolder(EpisodeItemViewHolder holder, int pos) { - holder.coverHolder.setVisibility(View.GONE); - } - - @Override - public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { - super.onCreateContextMenu(menu, v, menuInfo); - if (!inActionMode()) { - menu.findItem(R.id.multi_select).setVisible(true); - } - MenuItemUtils.setOnClickListeners(menu, FeedItemlistFragment.this::onContextItemSelected); - } - } - - public static class SingleFeedSortDialog extends ItemSortDialog { - private static final String ARG_FEED_ID = "feedId"; - private static final String ARG_FEED_IS_LOCAL = "isLocal"; - private static final String ARG_SORT_ORDER = "sortOrder"; - - private static SingleFeedSortDialog newInstance(Feed feed) { - Bundle bundle = new Bundle(); - bundle.putLong(ARG_FEED_ID, feed.getId()); - bundle.putBoolean(ARG_FEED_IS_LOCAL, feed.isLocalFeed()); - if (feed.getSortOrder() == null) { - bundle.putString(ARG_SORT_ORDER, String.valueOf(SortOrder.DATE_NEW_OLD.code)); - } else { - bundle.putString(ARG_SORT_ORDER, String.valueOf(feed.getSortOrder().code)); - } - SingleFeedSortDialog dialog = new SingleFeedSortDialog(); - dialog.setArguments(bundle); - return dialog; - } - - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - sortOrder = SortOrder.fromCodeString(getArguments().getString(ARG_SORT_ORDER)); - } - - @Override - protected void onAddItem(int title, SortOrder ascending, SortOrder descending, boolean ascendingIsDefault) { - if (ascending == SortOrder.DATE_OLD_NEW || ascending == SortOrder.DURATION_SHORT_LONG - || ascending == SortOrder.EPISODE_TITLE_A_Z - || (getArguments().getBoolean(ARG_FEED_IS_LOCAL) && ascending == SortOrder.EPISODE_FILENAME_A_Z)) { - super.onAddItem(title, ascending, descending, ascendingIsDefault); - } - } - - @Override - protected void onSelectionChanged() { - super.onSelectionChanged(); - DBWriter.setFeedItemSortOrder(getArguments().getLong(ARG_FEED_ID), sortOrder); - } - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/FeedSettingsFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/FeedSettingsFragment.java deleted file mode 100644 index 69cfb0087..000000000 --- a/app/src/main/java/de/danoeh/antennapod/fragment/FeedSettingsFragment.java +++ /dev/null @@ -1,529 +0,0 @@ -package de.danoeh.antennapod.fragment; - -import android.Manifest; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.net.Uri; -import android.os.Build; -import android.os.Bundle; -import android.provider.Settings; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.Toast; -import androidx.activity.result.ActivityResultLauncher; -import androidx.activity.result.contract.ActivityResultContracts; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.content.ContextCompat; -import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import com.google.android.material.appbar.MaterialToolbar; -import androidx.fragment.app.Fragment; -import androidx.preference.ListPreference; -import androidx.preference.Preference; -import androidx.preference.PreferenceFragmentCompat; -import androidx.preference.SwitchPreferenceCompat; -import androidx.recyclerview.widget.RecyclerView; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.event.settings.SkipIntroEndingChangedEvent; -import de.danoeh.antennapod.event.settings.SpeedPresetChangedEvent; -import de.danoeh.antennapod.event.settings.VolumeAdaptionChangedEvent; -import de.danoeh.antennapod.databinding.PlaybackSpeedFeedSettingDialogBinding; -import de.danoeh.antennapod.model.feed.Feed; -import de.danoeh.antennapod.model.feed.FeedFilter; -import de.danoeh.antennapod.model.feed.FeedPreferences; -import de.danoeh.antennapod.model.feed.VolumeAdaptionSetting; -import de.danoeh.antennapod.net.download.serviceinterface.FeedUpdateManager; -import de.danoeh.antennapod.storage.preferences.UserPreferences; -import de.danoeh.antennapod.storage.database.DBReader; -import de.danoeh.antennapod.storage.database.DBWriter; -import de.danoeh.antennapod.dialog.EpisodeFilterDialog; -import de.danoeh.antennapod.dialog.FeedPreferenceSkipDialog; -import de.danoeh.antennapod.dialog.TagSettingsDialog; -import de.danoeh.antennapod.ui.preferences.screen.synchronization.AuthenticationDialog; -import io.reactivex.Maybe; -import io.reactivex.MaybeOnSubscribe; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.Disposable; -import io.reactivex.schedulers.Schedulers; - -import org.greenrobot.eventbus.EventBus; - -import java.util.Collections; -import java.util.Locale; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Future; - -public class FeedSettingsFragment extends Fragment { - private static final String TAG = "FeedSettingsFragment"; - private static final String EXTRA_FEED_ID = "de.danoeh.antennapod.extra.feedId"; - - private Disposable disposable; - - public static FeedSettingsFragment newInstance(Feed feed) { - FeedSettingsFragment fragment = new FeedSettingsFragment(); - Bundle arguments = new Bundle(); - arguments.putLong(EXTRA_FEED_ID, feed.getId()); - fragment.setArguments(arguments); - return fragment; - } - - @Nullable - @Override - public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - View root = inflater.inflate(R.layout.feedsettings, container, false); - long feedId = getArguments().getLong(EXTRA_FEED_ID); - - MaterialToolbar toolbar = root.findViewById(R.id.toolbar); - toolbar.setNavigationOnClickListener(v -> getParentFragmentManager().popBackStack()); - - getParentFragmentManager().beginTransaction() - .replace(R.id.settings_fragment_container, - FeedSettingsPreferenceFragment.newInstance(feedId), "settings_fragment") - .commitAllowingStateLoss(); - - disposable = Maybe.create((MaybeOnSubscribe) emitter -> { - Feed feed = DBReader.getFeed(feedId); - if (feed != null) { - emitter.onSuccess(feed); - } else { - emitter.onComplete(); - } - }) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(result -> toolbar.setSubtitle(result.getTitle()), - error -> Log.d(TAG, Log.getStackTraceString(error)), - () -> { }); - - - return root; - } - - @Override - public void onDestroy() { - super.onDestroy(); - if (disposable != null) { - disposable.dispose(); - } - } - - public static class FeedSettingsPreferenceFragment extends PreferenceFragmentCompat { - private static final CharSequence PREF_EPISODE_FILTER = "episodeFilter"; - private static final CharSequence PREF_SCREEN = "feedSettingsScreen"; - private static final CharSequence PREF_AUTHENTICATION = "authentication"; - private static final CharSequence PREF_AUTO_DELETE = "autoDelete"; - private static final CharSequence PREF_CATEGORY_AUTO_DOWNLOAD = "autoDownloadCategory"; - private static final CharSequence PREF_NEW_EPISODES_ACTION = "feedNewEpisodesAction"; - private static final String PREF_FEED_PLAYBACK_SPEED = "feedPlaybackSpeed"; - private static final String PREF_AUTO_SKIP = "feedAutoSkip"; - private static final String PREF_TAGS = "tags"; - - private Feed feed; - private Disposable disposable; - private FeedPreferences feedPreferences; - - public static FeedSettingsPreferenceFragment newInstance(long feedId) { - FeedSettingsPreferenceFragment fragment = new FeedSettingsPreferenceFragment(); - Bundle arguments = new Bundle(); - arguments.putLong(EXTRA_FEED_ID, feedId); - fragment.setArguments(arguments); - return fragment; - } - - boolean notificationPermissionDenied = false; - private final ActivityResultLauncher requestPermissionLauncher = - registerForActivityResult(new ActivityResultContracts.RequestPermission(), isGranted -> { - if (isGranted) { - return; - } - if (notificationPermissionDenied) { - Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); - Uri uri = Uri.fromParts("package", getContext().getPackageName(), null); - intent.setData(uri); - startActivity(intent); - return; - } - Toast.makeText(getContext(), R.string.notification_permission_denied, Toast.LENGTH_LONG).show(); - notificationPermissionDenied = true; - }); - - @Override - public RecyclerView onCreateRecyclerView(LayoutInflater inflater, ViewGroup parent, Bundle state) { - final RecyclerView view = super.onCreateRecyclerView(inflater, parent, state); - // To prevent transition animation because of summary update - view.setItemAnimator(null); - view.setLayoutAnimation(null); - return view; - } - - @Override - public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { - addPreferencesFromResource(R.xml.feed_settings); - // To prevent displaying partially loaded data - findPreference(PREF_SCREEN).setVisible(false); - - long feedId = getArguments().getLong(EXTRA_FEED_ID); - disposable = Maybe.create((MaybeOnSubscribe) emitter -> { - Feed feed = DBReader.getFeed(feedId); - if (feed != null) { - emitter.onSuccess(feed); - } else { - emitter.onComplete(); - } - }) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(result -> { - feed = result; - feedPreferences = feed.getPreferences(); - - setupAutoDownloadGlobalPreference(); - setupAutoDownloadPreference(); - setupKeepUpdatedPreference(); - setupAutoDeletePreference(); - setupVolumeAdaptationPreferences(); - setupNewEpisodesAction(); - setupAuthentificationPreference(); - setupEpisodeFilterPreference(); - setupPlaybackSpeedPreference(); - setupFeedAutoSkipPreference(); - setupEpisodeNotificationPreference(); - setupTags(); - - updateAutoDeleteSummary(); - updateVolumeAdaptationValue(); - updateAutoDownloadEnabled(); - updateNewEpisodesAction(); - - if (feed.isLocalFeed()) { - findPreference(PREF_AUTHENTICATION).setVisible(false); - findPreference(PREF_CATEGORY_AUTO_DOWNLOAD).setVisible(false); - } - - findPreference(PREF_SCREEN).setVisible(true); - }, error -> Log.d(TAG, Log.getStackTraceString(error)), () -> { }); - } - - @Override - public void onDestroy() { - super.onDestroy(); - if (disposable != null) { - disposable.dispose(); - } - } - - private void setupFeedAutoSkipPreference() { - findPreference(PREF_AUTO_SKIP).setOnPreferenceClickListener(preference -> { - new FeedPreferenceSkipDialog(getContext(), - feedPreferences.getFeedSkipIntro(), - feedPreferences.getFeedSkipEnding()) { - @Override - protected void onConfirmed(int skipIntro, int skipEnding) { - feedPreferences.setFeedSkipIntro(skipIntro); - feedPreferences.setFeedSkipEnding(skipEnding); - DBWriter.setFeedPreferences(feedPreferences); - EventBus.getDefault().post( - new SkipIntroEndingChangedEvent(feedPreferences.getFeedSkipIntro(), - feedPreferences.getFeedSkipEnding(), - feed.getId())); - } - }.show(); - return false; - }); - } - - private void setupPlaybackSpeedPreference() { - Preference feedPlaybackSpeedPreference = findPreference(PREF_FEED_PLAYBACK_SPEED); - feedPlaybackSpeedPreference.setOnPreferenceClickListener(preference -> { - PlaybackSpeedFeedSettingDialogBinding viewBinding = - PlaybackSpeedFeedSettingDialogBinding.inflate(getLayoutInflater()); - viewBinding.seekBar.setProgressChangedListener(speed -> - viewBinding.currentSpeedLabel.setText(String.format(Locale.getDefault(), "%.2fx", speed))); - viewBinding.useGlobalCheckbox.setOnCheckedChangeListener((buttonView, isChecked) -> { - viewBinding.seekBar.setEnabled(!isChecked); - viewBinding.seekBar.setAlpha(isChecked ? 0.4f : 1f); - viewBinding.currentSpeedLabel.setAlpha(isChecked ? 0.4f : 1f); - - viewBinding.skipSilenceFeed.setEnabled(!isChecked); - viewBinding.skipSilenceFeed.setAlpha(isChecked ? 0.4f : 1f); - }); - float speed = feedPreferences.getFeedPlaybackSpeed(); - FeedPreferences.SkipSilence skipSilence = feedPreferences.getFeedSkipSilence(); - boolean isGlobal = speed == FeedPreferences.SPEED_USE_GLOBAL; - viewBinding.useGlobalCheckbox.setChecked(isGlobal); - viewBinding.seekBar.updateSpeed(isGlobal ? 1 : speed); - viewBinding.skipSilenceFeed.setChecked(!isGlobal - && skipSilence == FeedPreferences.SkipSilence.AGGRESSIVE); - new MaterialAlertDialogBuilder(getContext()) - .setTitle(R.string.playback_speed) - .setView(viewBinding.getRoot()) - .setPositiveButton(android.R.string.ok, (dialog, which) -> { - float newSpeed = viewBinding.useGlobalCheckbox.isChecked() - ? FeedPreferences.SPEED_USE_GLOBAL : viewBinding.seekBar.getCurrentSpeed(); - feedPreferences.setFeedPlaybackSpeed(newSpeed); - FeedPreferences.SkipSilence newSkipSilence; - if (viewBinding.useGlobalCheckbox.isChecked()) { - newSkipSilence = FeedPreferences.SkipSilence.GLOBAL; - } else if (viewBinding.skipSilenceFeed.isChecked()) { - newSkipSilence = FeedPreferences.SkipSilence.AGGRESSIVE; - } else { - newSkipSilence = FeedPreferences.SkipSilence.OFF; - } - feedPreferences.setFeedSkipSilence(newSkipSilence); - DBWriter.setFeedPreferences(feedPreferences); - EventBus.getDefault().post(new SpeedPresetChangedEvent( - feedPreferences.getFeedPlaybackSpeed(), - feed.getId(), feedPreferences.getFeedSkipSilence())); - }) - .setNegativeButton(R.string.cancel_label, null) - .show(); - return true; - }); - } - - private void setupEpisodeFilterPreference() { - findPreference(PREF_EPISODE_FILTER).setOnPreferenceClickListener(preference -> { - new EpisodeFilterDialog(getContext(), feedPreferences.getFilter()) { - @Override - protected void onConfirmed(FeedFilter filter) { - feedPreferences.setFilter(filter); - DBWriter.setFeedPreferences(feedPreferences); - } - }.show(); - return false; - }); - } - - private void setupAuthentificationPreference() { - findPreference(PREF_AUTHENTICATION).setOnPreferenceClickListener(preference -> { - new AuthenticationDialog(getContext(), - R.string.authentication_label, true, - feedPreferences.getUsername(), feedPreferences.getPassword()) { - @Override - protected void onConfirmed(String username, String password) { - feedPreferences.setUsername(username); - feedPreferences.setPassword(password); - Future setPreferencesFuture = DBWriter.setFeedPreferences(feedPreferences); - - new Thread(() -> { - try { - setPreferencesFuture.get(); - } catch (InterruptedException | ExecutionException e) { - e.printStackTrace(); - } - FeedUpdateManager.getInstance().runOnce(getContext(), feed); - }, "RefreshAfterCredentialChange").start(); - } - }.show(); - return false; - }); - } - - private void setupAutoDeletePreference() { - findPreference(PREF_AUTO_DELETE).setOnPreferenceChangeListener((preference, newValue) -> { - switch ((String) newValue) { - case "global": - feedPreferences.setAutoDeleteAction(FeedPreferences.AutoDeleteAction.GLOBAL); - break; - case "always": - feedPreferences.setAutoDeleteAction(FeedPreferences.AutoDeleteAction.ALWAYS); - break; - case "never": - feedPreferences.setAutoDeleteAction(FeedPreferences.AutoDeleteAction.NEVER); - break; - default: - } - DBWriter.setFeedPreferences(feedPreferences); - updateAutoDeleteSummary(); - return false; - }); - } - - private void updateAutoDeleteSummary() { - ListPreference autoDeletePreference = findPreference(PREF_AUTO_DELETE); - - switch (feedPreferences.getAutoDeleteAction()) { - case GLOBAL: - autoDeletePreference.setSummary(R.string.global_default); - autoDeletePreference.setValue("global"); - break; - case ALWAYS: - autoDeletePreference.setSummary(R.string.feed_auto_download_always); - autoDeletePreference.setValue("always"); - break; - case NEVER: - autoDeletePreference.setSummary(R.string.feed_auto_download_never); - autoDeletePreference.setValue("never"); - break; - } - } - - private void setupVolumeAdaptationPreferences() { - ListPreference volumeAdaptationPreference = findPreference("volumeReduction"); - volumeAdaptationPreference.setOnPreferenceChangeListener((preference, newValue) -> { - switch ((String) newValue) { - case "off": - feedPreferences.setVolumeAdaptionSetting(VolumeAdaptionSetting.OFF); - break; - case "light": - feedPreferences.setVolumeAdaptionSetting(VolumeAdaptionSetting.LIGHT_REDUCTION); - break; - case "heavy": - feedPreferences.setVolumeAdaptionSetting(VolumeAdaptionSetting.HEAVY_REDUCTION); - break; - case "light_boost": - feedPreferences.setVolumeAdaptionSetting(VolumeAdaptionSetting.LIGHT_BOOST); - break; - case "medium_boost": - feedPreferences.setVolumeAdaptionSetting(VolumeAdaptionSetting.MEDIUM_BOOST); - break; - case "heavy_boost": - feedPreferences.setVolumeAdaptionSetting(VolumeAdaptionSetting.HEAVY_BOOST); - break; - default: - } - DBWriter.setFeedPreferences(feedPreferences); - updateVolumeAdaptationValue(); - EventBus.getDefault().post( - new VolumeAdaptionChangedEvent(feedPreferences.getVolumeAdaptionSetting(), feed.getId())); - return false; - }); - } - - private void updateVolumeAdaptationValue() { - ListPreference volumeAdaptationPreference = findPreference("volumeReduction"); - - switch (feedPreferences.getVolumeAdaptionSetting()) { - case OFF: - volumeAdaptationPreference.setValue("off"); - break; - case LIGHT_REDUCTION: - volumeAdaptationPreference.setValue("light"); - break; - case HEAVY_REDUCTION: - volumeAdaptationPreference.setValue("heavy"); - break; - case LIGHT_BOOST: - volumeAdaptationPreference.setValue("light_boost"); - break; - case MEDIUM_BOOST: - volumeAdaptationPreference.setValue("medium_boost"); - break; - case HEAVY_BOOST: - volumeAdaptationPreference.setValue("heavy_boost"); - break; - } - } - - private void setupNewEpisodesAction() { - findPreference(PREF_NEW_EPISODES_ACTION).setOnPreferenceChangeListener((preference, newValue) -> { - int code = Integer.parseInt((String) newValue); - feedPreferences.setNewEpisodesAction(FeedPreferences.NewEpisodesAction.fromCode(code)); - DBWriter.setFeedPreferences(feedPreferences); - updateNewEpisodesAction(); - return false; - }); - } - - private void updateNewEpisodesAction() { - ListPreference newEpisodesAction = findPreference(PREF_NEW_EPISODES_ACTION); - newEpisodesAction.setValue("" + feedPreferences.getNewEpisodesAction().code); - - switch (feedPreferences.getNewEpisodesAction()) { - case GLOBAL: - newEpisodesAction.setSummary(R.string.global_default); - break; - case ADD_TO_INBOX: - newEpisodesAction.setSummary(R.string.feed_new_episodes_action_add_to_inbox); - break; - case ADD_TO_QUEUE: - newEpisodesAction.setSummary(R.string.feed_new_episodes_action_add_to_queue); - break; - case NOTHING: - newEpisodesAction.setSummary(R.string.feed_new_episodes_action_nothing); - break; - default: - } - } - - private void setupKeepUpdatedPreference() { - SwitchPreferenceCompat pref = findPreference("keepUpdated"); - - pref.setChecked(feedPreferences.getKeepUpdated()); - pref.setOnPreferenceChangeListener((preference, newValue) -> { - boolean checked = newValue == Boolean.TRUE; - feedPreferences.setKeepUpdated(checked); - DBWriter.setFeedPreferences(feedPreferences); - pref.setChecked(checked); - return false; - }); - } - - private void setupAutoDownloadGlobalPreference() { - if (!UserPreferences.isEnableAutodownload()) { - SwitchPreferenceCompat autodl = findPreference("autoDownload"); - autodl.setChecked(false); - autodl.setEnabled(false); - autodl.setSummary(R.string.auto_download_disabled_globally); - findPreference(PREF_EPISODE_FILTER).setEnabled(false); - } - } - - private void setupAutoDownloadPreference() { - SwitchPreferenceCompat pref = findPreference("autoDownload"); - - pref.setEnabled(UserPreferences.isEnableAutodownload()); - if (UserPreferences.isEnableAutodownload()) { - pref.setChecked(feedPreferences.getAutoDownload()); - } else { - pref.setChecked(false); - pref.setSummary(R.string.auto_download_disabled_globally); - } - - pref.setOnPreferenceChangeListener((preference, newValue) -> { - boolean checked = newValue == Boolean.TRUE; - - feedPreferences.setAutoDownload(checked); - DBWriter.setFeedPreferences(feedPreferences); - updateAutoDownloadEnabled(); - pref.setChecked(checked); - return false; - }); - } - - private void updateAutoDownloadEnabled() { - if (feed != null && feed.getPreferences() != null) { - boolean enabled = feed.getPreferences().getAutoDownload() && UserPreferences.isEnableAutodownload(); - findPreference(PREF_EPISODE_FILTER).setEnabled(enabled); - } - } - - private void setupTags() { - findPreference(PREF_TAGS).setOnPreferenceClickListener(preference -> { - TagSettingsDialog.newInstance(Collections.singletonList(feedPreferences)) - .show(getChildFragmentManager(), TagSettingsDialog.TAG); - return true; - }); - } - - private void setupEpisodeNotificationPreference() { - SwitchPreferenceCompat pref = findPreference("episodeNotification"); - - pref.setChecked(feedPreferences.getShowEpisodeNotification()); - pref.setOnPreferenceChangeListener((preference, newValue) -> { - if (Build.VERSION.SDK_INT >= 33 && ContextCompat.checkSelfPermission(getContext(), - Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { - requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS); - return false; - } - boolean checked = newValue == Boolean.TRUE; - feedPreferences.setShowEpisodeNotification(checked); - DBWriter.setFeedPreferences(feedPreferences); - pref.setChecked(checked); - return false; - }); - } - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/InboxFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/InboxFragment.java deleted file mode 100644 index e8016675b..000000000 --- a/app/src/main/java/de/danoeh/antennapod/fragment/InboxFragment.java +++ /dev/null @@ -1,153 +0,0 @@ -package de.danoeh.antennapod.fragment; - -import android.content.Context; -import android.content.SharedPreferences; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.widget.CheckBox; -import android.widget.Toast; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.activity.MainActivity; -import de.danoeh.antennapod.storage.database.DBReader; -import de.danoeh.antennapod.storage.database.DBWriter; -import de.danoeh.antennapod.dialog.ItemSortDialog; -import de.danoeh.antennapod.event.FeedListUpdateEvent; -import de.danoeh.antennapod.model.feed.FeedItem; -import de.danoeh.antennapod.model.feed.FeedItemFilter; -import de.danoeh.antennapod.model.feed.SortOrder; -import de.danoeh.antennapod.storage.preferences.UserPreferences; -import org.greenrobot.eventbus.EventBus; - -import java.util.List; - -/** - * Like 'EpisodesFragment' except that it only shows new episodes and - * supports swiping to mark as read. - */ -public class InboxFragment extends EpisodesListFragment { - public static final String TAG = "NewEpisodesFragment"; - private static final String PREF_NAME = "PrefNewEpisodesFragment"; - private static final String PREF_DO_NOT_PROMPT_REMOVE_ALL_FROM_INBOX = "prefDoNotPromptRemovalAllFromInbox"; - private SharedPreferences prefs; - - @NonNull - @Override - public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - final View root = super.onCreateView(inflater, container, savedInstanceState); - toolbar.inflateMenu(R.menu.inbox); - toolbar.setTitle(R.string.inbox_label); - prefs = getActivity().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); - updateToolbar(); - emptyView.setIcon(R.drawable.ic_inbox); - emptyView.setTitle(R.string.no_inbox_head_label); - emptyView.setMessage(R.string.no_inbox_label); - speedDialView.removeActionItemById(R.id.mark_unread_batch); - speedDialView.removeActionItemById(R.id.remove_from_queue_batch); - speedDialView.removeActionItemById(R.id.delete_batch); - return root; - } - - @Override - protected FeedItemFilter getFilter() { - return new FeedItemFilter(FeedItemFilter.NEW); - } - - @Override - protected String getFragmentTag() { - return TAG; - } - - @Override - protected String getPrefName() { - return PREF_NAME; - } - - @Override - public boolean onMenuItemClick(MenuItem item) { - if (super.onMenuItemClick(item)) { - return true; - } - if (item.getItemId() == R.id.remove_all_inbox_item) { - if (prefs.getBoolean(PREF_DO_NOT_PROMPT_REMOVE_ALL_FROM_INBOX, false)) { - removeAllFromInbox(); - } else { - showRemoveAllDialog(); - } - return true; - } else if (item.getItemId() == R.id.inbox_sort) { - new InboxSortDialog().show(getChildFragmentManager(), "SortDialog"); - return true; - } - return false; - } - - @NonNull - @Override - protected List loadData() { - return DBReader.getEpisodes(0, page * EPISODES_PER_PAGE, - new FeedItemFilter(FeedItemFilter.NEW), UserPreferences.getInboxSortedOrder()); - } - - @NonNull - @Override - protected List loadMoreData(int page) { - return DBReader.getEpisodes((page - 1) * EPISODES_PER_PAGE, EPISODES_PER_PAGE, - new FeedItemFilter(FeedItemFilter.NEW), UserPreferences.getInboxSortedOrder()); - } - - @Override - protected int loadTotalItemCount() { - return DBReader.getTotalEpisodeCount(new FeedItemFilter(FeedItemFilter.NEW)); - } - - private void removeAllFromInbox() { - DBWriter.removeAllNewFlags(); - ((MainActivity) getActivity()).showSnackbarAbovePlayer(R.string.removed_all_inbox_msg, Toast.LENGTH_SHORT); - } - - private void showRemoveAllDialog() { - MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(getContext()); - builder.setTitle(R.string.remove_all_inbox_label); - builder.setMessage(R.string.remove_all_inbox_confirmation_msg); - - View view = View.inflate(getContext(), R.layout.checkbox_do_not_show_again, null); - CheckBox checkNeverAskAgain = view.findViewById(R.id.checkbox_do_not_show_again); - builder.setView(view); - - builder.setPositiveButton(R.string.confirm_label, (dialog, which) -> { - dialog.dismiss(); - removeAllFromInbox(); - prefs.edit().putBoolean(PREF_DO_NOT_PROMPT_REMOVE_ALL_FROM_INBOX, checkNeverAskAgain.isChecked()).apply(); - }); - builder.setNegativeButton(R.string.cancel_label, null); - builder.show(); - } - - public static class InboxSortDialog extends ItemSortDialog { - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - sortOrder = UserPreferences.getInboxSortedOrder(); - } - - @Override - protected void onAddItem(int title, SortOrder ascending, SortOrder descending, boolean ascendingIsDefault) { - if (ascending == SortOrder.DATE_OLD_NEW || ascending == SortOrder.DURATION_SHORT_LONG) { - super.onAddItem(title, ascending, descending, ascendingIsDefault); - } - } - - @Override - protected void onSelectionChanged() { - super.onSelectionChanged(); - UserPreferences.setInboxSortedOrder(sortOrder); - EventBus.getDefault().post(new FeedListUpdateEvent(0)); - } - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/ItemDescriptionFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/ItemDescriptionFragment.java deleted file mode 100644 index 6ab9dc671..000000000 --- a/app/src/main/java/de/danoeh/antennapod/fragment/ItemDescriptionFragment.java +++ /dev/null @@ -1,189 +0,0 @@ -package de.danoeh.antennapod.fragment; - -import android.app.Activity; -import android.content.Context; -import android.content.SharedPreferences; -import android.os.Bundle; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; - -import androidx.fragment.app.Fragment; - -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.playback.service.PlaybackController; -import de.danoeh.antennapod.storage.database.DBReader; -import de.danoeh.antennapod.core.util.gui.ShownotesCleaner; -import de.danoeh.antennapod.model.feed.FeedMedia; -import de.danoeh.antennapod.model.playback.Playable; -import de.danoeh.antennapod.view.ShownotesWebView; -import io.reactivex.Maybe; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.Disposable; -import io.reactivex.schedulers.Schedulers; - -/** - * Displays the description of a Playable object in a Webview. - */ -public class ItemDescriptionFragment extends Fragment { - private static final String TAG = "ItemDescriptionFragment"; - - private static final String PREF = "ItemDescriptionFragmentPrefs"; - private static final String PREF_SCROLL_Y = "prefScrollY"; - private static final String PREF_PLAYABLE_ID = "prefPlayableId"; - - private ShownotesWebView webvDescription; - private Disposable webViewLoader; - private PlaybackController controller; - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - Log.d(TAG, "Creating view"); - View root = inflater.inflate(R.layout.item_description_fragment, container, false); - webvDescription = root.findViewById(R.id.webview); - webvDescription.setTimecodeSelectedListener(time -> { - if (controller != null) { - controller.seekTo(time); - } - }); - webvDescription.setPageFinishedListener(() -> { - // Restoring the scroll position might not always work - webvDescription.postDelayed(ItemDescriptionFragment.this::restoreFromPreference, 50); - }); - - root.addOnLayoutChangeListener(new View.OnLayoutChangeListener() { - @Override - public void onLayoutChange(View v, int left, int top, int right, - int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) { - if (root.getMeasuredHeight() != webvDescription.getMinimumHeight()) { - webvDescription.setMinimumHeight(root.getMeasuredHeight()); - } - root.removeOnLayoutChangeListener(this); - } - }); - registerForContextMenu(webvDescription); - return root; - } - - @Override - public void onDestroy() { - super.onDestroy(); - Log.d(TAG, "Fragment destroyed"); - if (webvDescription != null) { - webvDescription.removeAllViews(); - webvDescription.destroy(); - } - } - - @Override - public boolean onContextItemSelected(MenuItem item) { - return webvDescription.onContextItemSelected(item); - } - - private void load() { - Log.d(TAG, "load()"); - if (webViewLoader != null) { - webViewLoader.dispose(); - } - Context context = getContext(); - if (context == null) { - return; - } - webViewLoader = Maybe.create(emitter -> { - Playable media = controller.getMedia(); - if (media == null) { - emitter.onComplete(); - return; - } - if (media instanceof FeedMedia) { - FeedMedia feedMedia = ((FeedMedia) media); - if (feedMedia.getItem() == null) { - feedMedia.setItem(DBReader.getFeedItem(feedMedia.getItemId())); - } - DBReader.loadDescriptionOfFeedItem(feedMedia.getItem()); - } - ShownotesCleaner shownotesCleaner = new ShownotesCleaner( - context, media.getDescription(), media.getDuration()); - emitter.onSuccess(shownotesCleaner.processShownotes()); - }) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(data -> { - webvDescription.loadDataWithBaseURL("https://127.0.0.1", data, "text/html", - "utf-8", "about:blank"); - Log.d(TAG, "Webview loaded"); - }, error -> Log.e(TAG, Log.getStackTraceString(error))); - } - - @Override - public void onPause() { - super.onPause(); - savePreference(); - } - - private void savePreference() { - Log.d(TAG, "Saving preferences"); - SharedPreferences prefs = getActivity().getSharedPreferences(PREF, Activity.MODE_PRIVATE); - SharedPreferences.Editor editor = prefs.edit(); - if (controller != null && controller.getMedia() != null && webvDescription != null) { - Log.d(TAG, "Saving scroll position: " + webvDescription.getScrollY()); - editor.putInt(PREF_SCROLL_Y, webvDescription.getScrollY()); - editor.putString(PREF_PLAYABLE_ID, controller.getMedia().getIdentifier() - .toString()); - } else { - Log.d(TAG, "savePreferences was called while media or webview was null"); - editor.putInt(PREF_SCROLL_Y, -1); - editor.putString(PREF_PLAYABLE_ID, ""); - } - editor.apply(); - } - - private boolean restoreFromPreference() { - Log.d(TAG, "Restoring from preferences"); - Activity activity = getActivity(); - if (activity != null) { - SharedPreferences prefs = activity.getSharedPreferences(PREF, Activity.MODE_PRIVATE); - String id = prefs.getString(PREF_PLAYABLE_ID, ""); - int scrollY = prefs.getInt(PREF_SCROLL_Y, -1); - if (controller != null && scrollY != -1 && controller.getMedia() != null - && id.equals(controller.getMedia().getIdentifier().toString()) - && webvDescription != null) { - Log.d(TAG, "Restored scroll Position: " + scrollY); - webvDescription.scrollTo(webvDescription.getScrollX(), scrollY); - return true; - } - } - return false; - } - - public void scrollToTop() { - webvDescription.scrollTo(0, 0); - savePreference(); - } - - @Override - public void onStart() { - super.onStart(); - controller = new PlaybackController(getActivity()) { - @Override - public void loadMediaInfo() { - load(); - } - }; - controller.init(); - load(); - } - - @Override - public void onStop() { - super.onStop(); - - if (webViewLoader != null) { - webViewLoader.dispose(); - } - controller.release(); - controller = null; - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/ItemFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/ItemFragment.java deleted file mode 100644 index 935d1f06d..000000000 --- a/app/src/main/java/de/danoeh/antennapod/fragment/ItemFragment.java +++ /dev/null @@ -1,435 +0,0 @@ -package de.danoeh.antennapod.fragment; - -import android.content.Context; -import android.os.Build; -import android.os.Bundle; -import android.text.Layout; -import android.text.TextUtils; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.widget.Button; -import android.widget.ImageView; -import android.widget.ProgressBar; -import android.widget.TextView; -import androidx.annotation.Nullable; -import androidx.fragment.app.Fragment; -import com.bumptech.glide.Glide; -import com.bumptech.glide.load.resource.bitmap.FitCenter; -import com.bumptech.glide.load.resource.bitmap.RoundedCorners; -import com.bumptech.glide.request.RequestOptions; -import com.google.android.material.snackbar.Snackbar; -import com.skydoves.balloon.ArrowOrientation; -import com.skydoves.balloon.ArrowOrientationRules; -import com.skydoves.balloon.Balloon; -import com.skydoves.balloon.BalloonAnimation; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.activity.MainActivity; -import de.danoeh.antennapod.adapter.actionbutton.CancelDownloadActionButton; -import de.danoeh.antennapod.adapter.actionbutton.DeleteActionButton; -import de.danoeh.antennapod.adapter.actionbutton.DownloadActionButton; -import de.danoeh.antennapod.adapter.actionbutton.ItemActionButton; -import de.danoeh.antennapod.adapter.actionbutton.MarkAsPlayedActionButton; -import de.danoeh.antennapod.adapter.actionbutton.PauseActionButton; -import de.danoeh.antennapod.adapter.actionbutton.PlayActionButton; -import de.danoeh.antennapod.adapter.actionbutton.PlayLocalActionButton; -import de.danoeh.antennapod.adapter.actionbutton.StreamActionButton; -import de.danoeh.antennapod.adapter.actionbutton.VisitWebsiteActionButton; -import de.danoeh.antennapod.event.EpisodeDownloadEvent; -import de.danoeh.antennapod.playback.service.PlaybackStatus; -import de.danoeh.antennapod.event.FeedItemEvent; -import de.danoeh.antennapod.event.PlayerStatusEvent; -import de.danoeh.antennapod.event.UnreadItemsUpdateEvent; -import de.danoeh.antennapod.model.feed.FeedItem; -import de.danoeh.antennapod.model.feed.FeedMedia; -import de.danoeh.antennapod.playback.service.PlaybackController; -import de.danoeh.antennapod.storage.preferences.UsageStatistics; -import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterface; -import de.danoeh.antennapod.storage.preferences.UserPreferences; -import de.danoeh.antennapod.storage.database.DBReader; -import de.danoeh.antennapod.ui.common.Converter; -import de.danoeh.antennapod.ui.common.DateFormatter; -import de.danoeh.antennapod.ui.common.CircularProgressBar; -import de.danoeh.antennapod.ui.common.ThemeUtils; -import de.danoeh.antennapod.core.util.gui.ShownotesCleaner; -import de.danoeh.antennapod.ui.episodes.ImageResourceUtils; -import de.danoeh.antennapod.view.ShownotesWebView; -import io.reactivex.Observable; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.Disposable; -import io.reactivex.schedulers.Schedulers; -import org.greenrobot.eventbus.EventBus; -import org.greenrobot.eventbus.Subscribe; -import org.greenrobot.eventbus.ThreadMode; - -import java.util.Locale; -import java.util.Objects; - -/** - * Displays information about a FeedItem and actions. - */ -public class ItemFragment extends Fragment { - - private static final String TAG = "ItemFragment"; - private static final String ARG_FEEDITEM = "feeditem"; - - /** - * Creates a new instance of an ItemFragment - * - * @param feeditem The ID of the FeedItem to show - * @return The ItemFragment instance - */ - public static ItemFragment newInstance(long feeditem) { - ItemFragment fragment = new ItemFragment(); - Bundle args = new Bundle(); - args.putLong(ARG_FEEDITEM, feeditem); - fragment.setArguments(args); - return fragment; - } - - private boolean itemsLoaded = false; - private long itemId; - private FeedItem item; - private String webviewData; - - private ViewGroup root; - private ShownotesWebView webvDescription; - private TextView txtvPodcast; - private TextView txtvTitle; - private TextView txtvDuration; - private TextView txtvPublished; - private ImageView imgvCover; - private CircularProgressBar progbarDownload; - private ProgressBar progbarLoading; - private TextView butAction1Text; - private TextView butAction2Text; - private ImageView butAction1Icon; - private ImageView butAction2Icon; - private View butAction1; - private View butAction2; - private ItemActionButton actionButton1; - private ItemActionButton actionButton2; - private View noMediaLabel; - - private Disposable disposable; - private PlaybackController controller; - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - itemId = getArguments().getLong(ARG_FEEDITEM); - } - - @Override - public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - super.onCreateView(inflater, container, savedInstanceState); - View layout = inflater.inflate(R.layout.feeditem_fragment, container, false); - - root = layout.findViewById(R.id.content_root); - - txtvPodcast = layout.findViewById(R.id.txtvPodcast); - txtvPodcast.setOnClickListener(v -> openPodcast()); - txtvTitle = layout.findViewById(R.id.txtvTitle); - if (Build.VERSION.SDK_INT >= 23) { - txtvTitle.setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_FULL); - } - txtvDuration = layout.findViewById(R.id.txtvDuration); - txtvPublished = layout.findViewById(R.id.txtvPublished); - txtvTitle.setEllipsize(TextUtils.TruncateAt.END); - webvDescription = layout.findViewById(R.id.webvDescription); - webvDescription.setTimecodeSelectedListener(time -> { - if (controller != null && item.getMedia() != null && controller.getMedia() != null - && Objects.equals(item.getMedia().getIdentifier(), controller.getMedia().getIdentifier())) { - controller.seekTo(time); - } else { - ((MainActivity) getActivity()).showSnackbarAbovePlayer(R.string.play_this_to_seek_position, - Snackbar.LENGTH_LONG); - } - }); - registerForContextMenu(webvDescription); - - imgvCover = layout.findViewById(R.id.imgvCover); - imgvCover.setOnClickListener(v -> openPodcast()); - progbarDownload = layout.findViewById(R.id.circularProgressBar); - progbarLoading = layout.findViewById(R.id.progbarLoading); - butAction1 = layout.findViewById(R.id.butAction1); - butAction2 = layout.findViewById(R.id.butAction2); - butAction1Icon = layout.findViewById(R.id.butAction1Icon); - butAction2Icon = layout.findViewById(R.id.butAction2Icon); - butAction1Text = layout.findViewById(R.id.butAction1Text); - butAction2Text = layout.findViewById(R.id.butAction2Text); - noMediaLabel = layout.findViewById(R.id.noMediaLabel); - - butAction1.setOnClickListener(v -> { - if (actionButton1 instanceof StreamActionButton && !UserPreferences.isStreamOverDownload() - && UsageStatistics.hasSignificantBiasTo(UsageStatistics.ACTION_STREAM)) { - showOnDemandConfigBalloon(true); - return; - } else if (actionButton1 == null) { - return; // Not loaded yet - } - actionButton1.onClick(getContext()); - }); - butAction2.setOnClickListener(v -> { - if (actionButton2 instanceof DownloadActionButton && UserPreferences.isStreamOverDownload() - && UsageStatistics.hasSignificantBiasTo(UsageStatistics.ACTION_DOWNLOAD)) { - showOnDemandConfigBalloon(false); - return; - } else if (actionButton2 == null) { - return; // Not loaded yet - } - actionButton2.onClick(getContext()); - }); - return layout; - } - - private void showOnDemandConfigBalloon(boolean offerStreaming) { - final boolean isLocaleRtl = TextUtils.getLayoutDirectionFromLocale(Locale.getDefault()) - == View.LAYOUT_DIRECTION_RTL; - final Balloon balloon = new Balloon.Builder(getContext()) - .setArrowOrientation(ArrowOrientation.TOP) - .setArrowOrientationRules(ArrowOrientationRules.ALIGN_FIXED) - .setArrowPosition(0.25f + ((isLocaleRtl ^ offerStreaming) ? 0f : 0.5f)) - .setWidthRatio(1.0f) - .setMarginLeft(8) - .setMarginRight(8) - .setBackgroundColor(ThemeUtils.getColorFromAttr(getContext(), R.attr.colorSecondary)) - .setBalloonAnimation(BalloonAnimation.OVERSHOOT) - .setLayout(R.layout.popup_bubble_view) - .setDismissWhenTouchOutside(true) - .setLifecycleOwner(this) - .build(); - final Button positiveButton = balloon.getContentView().findViewById(R.id.balloon_button_positive); - final Button negativeButton = balloon.getContentView().findViewById(R.id.balloon_button_negative); - final TextView message = balloon.getContentView().findViewById(R.id.balloon_message); - message.setText(offerStreaming - ? R.string.on_demand_config_stream_text : R.string.on_demand_config_download_text); - positiveButton.setOnClickListener(v1 -> { - UserPreferences.setStreamOverDownload(offerStreaming); - // Update all visible lists to reflect new streaming action button - EventBus.getDefault().post(new UnreadItemsUpdateEvent()); - ((MainActivity) getActivity()).showSnackbarAbovePlayer( - R.string.on_demand_config_setting_changed, Snackbar.LENGTH_SHORT); - balloon.dismiss(); - }); - negativeButton.setOnClickListener(v1 -> { - UsageStatistics.doNotAskAgain(UsageStatistics.ACTION_STREAM); // Type does not matter. Both are silenced. - balloon.dismiss(); - }); - balloon.showAlignBottom(butAction1, 0, (int) (-12 * getResources().getDisplayMetrics().density)); - } - - @Override - public void onStart() { - super.onStart(); - EventBus.getDefault().register(this); - controller = new PlaybackController(getActivity()) { - @Override - public void loadMediaInfo() { - // Do nothing - } - }; - controller.init(); - load(); - } - - @Override - public void onResume() { - super.onResume(); - if (itemsLoaded) { - progbarLoading.setVisibility(View.GONE); - updateAppearance(); - } - } - - @Override - public void onStop() { - super.onStop(); - EventBus.getDefault().unregister(this); - controller.release(); - } - - @Override - public void onDestroyView() { - super.onDestroyView(); - if (disposable != null) { - disposable.dispose(); - } - if (webvDescription != null && root != null) { - root.removeView(webvDescription); - webvDescription.destroy(); - } - } - - private void onFragmentLoaded() { - if (webviewData != null && !itemsLoaded) { - webvDescription.loadDataWithBaseURL("https://127.0.0.1", webviewData, "text/html", "utf-8", "about:blank"); - } - updateAppearance(); - } - - private void updateAppearance() { - if (item == null) { - Log.d(TAG, "updateAppearance item is null"); - return; - } - txtvPodcast.setText(item.getFeed().getTitle()); - txtvTitle.setText(item.getTitle()); - - if (item.getPubDate() != null) { - String pubDateStr = DateFormatter.formatAbbrev(getActivity(), item.getPubDate()); - txtvPublished.setText(pubDateStr); - txtvPublished.setContentDescription(DateFormatter.formatForAccessibility(item.getPubDate())); - } - - RequestOptions options = new RequestOptions() - .error(R.color.light_gray) - .transform(new FitCenter(), - new RoundedCorners((int) (8 * getResources().getDisplayMetrics().density))) - .dontAnimate(); - - Glide.with(this) - .load(item.getImageLocation()) - .error(Glide.with(this) - .load(ImageResourceUtils.getFallbackImageLocation(item)) - .apply(options)) - .apply(options) - .into(imgvCover); - updateButtons(); - } - - private void updateButtons() { - progbarDownload.setVisibility(View.GONE); - if (item.hasMedia()) { - if (DownloadServiceInterface.get().isDownloadingEpisode(item.getMedia().getDownloadUrl())) { - progbarDownload.setVisibility(View.VISIBLE); - progbarDownload.setPercentage(0.01f * Math.max(1, - DownloadServiceInterface.get().getProgress(item.getMedia().getDownloadUrl())), item); - progbarDownload.setIndeterminate( - DownloadServiceInterface.get().isEpisodeQueued(item.getMedia().getDownloadUrl())); - } - } - - FeedMedia media = item.getMedia(); - if (media == null) { - actionButton1 = new MarkAsPlayedActionButton(item); - actionButton2 = new VisitWebsiteActionButton(item); - noMediaLabel.setVisibility(View.VISIBLE); - } else { - noMediaLabel.setVisibility(View.GONE); - if (media.getDuration() > 0) { - txtvDuration.setText(Converter.getDurationStringLong(media.getDuration())); - txtvDuration.setContentDescription( - Converter.getDurationStringLocalized(getContext(), media.getDuration())); - } - if (PlaybackStatus.isCurrentlyPlaying(media)) { - actionButton1 = new PauseActionButton(item); - } else if (item.getFeed().isLocalFeed()) { - actionButton1 = new PlayLocalActionButton(item); - } else if (media.isDownloaded()) { - actionButton1 = new PlayActionButton(item); - } else { - actionButton1 = new StreamActionButton(item); - } - if (DownloadServiceInterface.get().isDownloadingEpisode(media.getDownloadUrl())) { - actionButton2 = new CancelDownloadActionButton(item); - } else if (!media.isDownloaded()) { - actionButton2 = new DownloadActionButton(item); - } else { - actionButton2 = new DeleteActionButton(item); - } - } - - butAction1Text.setText(actionButton1.getLabel()); - butAction1Text.setTransformationMethod(null); - butAction1Icon.setImageResource(actionButton1.getDrawable()); - butAction1.setVisibility(actionButton1.getVisibility()); - - butAction2Text.setText(actionButton2.getLabel()); - butAction2Text.setTransformationMethod(null); - butAction2Icon.setImageResource(actionButton2.getDrawable()); - butAction2.setVisibility(actionButton2.getVisibility()); - } - - @Override - public boolean onContextItemSelected(MenuItem item) { - return webvDescription.onContextItemSelected(item); - } - - private void openPodcast() { - if (item == null) { - return; - } - Fragment fragment = FeedItemlistFragment.newInstance(item.getFeedId()); - ((MainActivity) getActivity()).loadChildFragment(fragment); - } - - @Subscribe(threadMode = ThreadMode.MAIN) - public void onEventMainThread(FeedItemEvent event) { - Log.d(TAG, "onEventMainThread() called with: " + "event = [" + event + "]"); - for (FeedItem item : event.items) { - if (this.item.getId() == item.getId()) { - load(); - return; - } - } - } - - @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) - public void onEventMainThread(EpisodeDownloadEvent event) { - if (item == null || item.getMedia() == null) { - return; - } - if (!event.getUrls().contains(item.getMedia().getDownloadUrl())) { - return; - } - if (itemsLoaded && getActivity() != null) { - updateButtons(); - } - } - - @Subscribe(threadMode = ThreadMode.MAIN) - public void onPlayerStatusChanged(PlayerStatusEvent event) { - updateButtons(); - } - - @Subscribe(threadMode = ThreadMode.MAIN) - public void onUnreadItemsChanged(UnreadItemsUpdateEvent event) { - load(); - } - - private void load() { - if (disposable != null) { - disposable.dispose(); - } - if (!itemsLoaded) { - progbarLoading.setVisibility(View.VISIBLE); - } - disposable = Observable.fromCallable(this::loadInBackground) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(result -> { - progbarLoading.setVisibility(View.GONE); - item = result; - onFragmentLoaded(); - itemsLoaded = true; - }, error -> Log.e(TAG, Log.getStackTraceString(error))); - } - - @Nullable - private FeedItem loadInBackground() { - FeedItem feedItem = DBReader.getFeedItem(itemId); - Context context = getContext(); - if (feedItem != null && context != null) { - int duration = feedItem.getMedia() != null ? feedItem.getMedia().getDuration() : Integer.MAX_VALUE; - DBReader.loadDescriptionOfFeedItem(feedItem); - ShownotesCleaner t = new ShownotesCleaner(context, feedItem.getDescription(), duration); - webviewData = t.processShownotes(); - } - return feedItem; - } - -} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/ItemPagerFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/ItemPagerFragment.java deleted file mode 100644 index 637d4c7ba..000000000 --- a/app/src/main/java/de/danoeh/antennapod/fragment/ItemPagerFragment.java +++ /dev/null @@ -1,188 +0,0 @@ -package de.danoeh.antennapod.fragment; - -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import com.google.android.material.appbar.MaterialToolbar; -import androidx.fragment.app.Fragment; -import androidx.viewpager2.adapter.FragmentStateAdapter; -import androidx.viewpager2.widget.ViewPager2; - -import org.greenrobot.eventbus.EventBus; -import org.greenrobot.eventbus.Subscribe; -import org.greenrobot.eventbus.ThreadMode; - -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.activity.MainActivity; -import de.danoeh.antennapod.event.FeedItemEvent; -import de.danoeh.antennapod.model.feed.FeedItem; -import de.danoeh.antennapod.storage.database.DBReader; -import de.danoeh.antennapod.menuhandler.FeedItemMenuHandler; -import io.reactivex.Observable; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.Disposable; -import io.reactivex.schedulers.Schedulers; - -/** - * Displays information about a list of FeedItems. - */ -public class ItemPagerFragment extends Fragment implements MaterialToolbar.OnMenuItemClickListener { - private static final String ARG_FEEDITEMS = "feeditems"; - private static final String ARG_FEEDITEM_POS = "feeditem_pos"; - private static final String KEY_PAGER_ID = "pager_id"; - private ViewPager2 pager; - - /** - * Creates a new instance of an ItemPagerFragment. - * - * @param feeditems The IDs of the FeedItems that belong to the same list - * @param feedItemPos The position of the FeedItem that is currently shown - * @return The ItemFragment instance - */ - public static ItemPagerFragment newInstance(long[] feeditems, int feedItemPos) { - ItemPagerFragment fragment = new ItemPagerFragment(); - Bundle args = new Bundle(); - args.putLongArray(ARG_FEEDITEMS, feeditems); - args.putInt(ARG_FEEDITEM_POS, Math.max(0, feedItemPos)); - fragment.setArguments(args); - return fragment; - } - - private long[] feedItems; - private FeedItem item; - private Disposable disposable; - private MaterialToolbar toolbar; - - @Override - public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, - @Nullable Bundle savedInstanceState) { - super.onCreateView(inflater, container, savedInstanceState); - View layout = inflater.inflate(R.layout.feeditem_pager_fragment, container, false); - toolbar = layout.findViewById(R.id.toolbar); - toolbar.setTitle(""); - toolbar.inflateMenu(R.menu.feeditem_options); - toolbar.setNavigationOnClickListener(v -> getParentFragmentManager().popBackStack()); - toolbar.setOnMenuItemClickListener(this); - - feedItems = getArguments().getLongArray(ARG_FEEDITEMS); - final int feedItemPos = Math.max(0, getArguments().getInt(ARG_FEEDITEM_POS)); - - pager = layout.findViewById(R.id.pager); - // FragmentStatePagerAdapter documentation: - // > When using FragmentStatePagerAdapter the host ViewPager must have a valid ID set. - // When opening multiple ItemPagerFragments by clicking "item" -> "visit podcast" -> "item" -> etc, - // the ID is no longer unique and FragmentStatePagerAdapter does not display any pages. - int newId = View.generateViewId(); - if (savedInstanceState != null && savedInstanceState.getInt(KEY_PAGER_ID, 0) != 0) { - // Restore state by using the same ID as before. ID collisions are prevented in MainActivity. - newId = savedInstanceState.getInt(KEY_PAGER_ID, 0); - } - pager.setId(newId); - pager.setAdapter(new ItemPagerAdapter(this)); - pager.setCurrentItem(feedItemPos, false); - pager.setOffscreenPageLimit(1); - loadItem(feedItems[feedItemPos]); - pager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() { - @Override - public void onPageSelected(int position) { - loadItem(feedItems[position]); - } - }); - - EventBus.getDefault().register(this); - return layout; - } - - @Override - public void onSaveInstanceState(@NonNull Bundle outState) { - super.onSaveInstanceState(outState); - outState.putInt(KEY_PAGER_ID, pager.getId()); - } - - @Override - public void onDestroyView() { - super.onDestroyView(); - EventBus.getDefault().unregister(this); - if (disposable != null) { - disposable.dispose(); - } - } - - private void loadItem(long itemId) { - if (disposable != null) { - disposable.dispose(); - } - - disposable = Observable.fromCallable(() -> DBReader.getFeedItem(itemId)) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(result -> { - item = result; - refreshToolbarState(); - }, Throwable::printStackTrace); - } - - public void refreshToolbarState() { - if (item == null) { - return; - } - if (item.hasMedia()) { - FeedItemMenuHandler.onPrepareMenu(toolbar.getMenu(), item); - } else { - // these are already available via button1 and button2 - FeedItemMenuHandler.onPrepareMenu(toolbar.getMenu(), item, - R.id.mark_read_item, R.id.visit_website_item); - } - } - - @Override - public boolean onMenuItemClick(MenuItem menuItem) { - if (menuItem.getItemId() == R.id.open_podcast) { - openPodcast(); - return true; - } - return FeedItemMenuHandler.onMenuItemClicked(this, menuItem.getItemId(), item); - } - - @Subscribe(threadMode = ThreadMode.MAIN) - public void onEventMainThread(FeedItemEvent event) { - for (FeedItem item : event.items) { - if (this.item != null && this.item.getId() == item.getId()) { - this.item = item; - refreshToolbarState(); - return; - } - } - } - - private void openPodcast() { - if (item == null) { - return; - } - Fragment fragment = FeedItemlistFragment.newInstance(item.getFeedId()); - ((MainActivity) getActivity()).loadChildFragment(fragment); - } - - private class ItemPagerAdapter extends FragmentStateAdapter { - - ItemPagerAdapter(@NonNull Fragment fragment) { - super(fragment); - } - - @NonNull - @Override - public Fragment createFragment(int position) { - return ItemFragment.newInstance(feedItems[position]); - } - - @Override - public int getItemCount() { - return feedItems.length; - } - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/NavDrawerFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/NavDrawerFragment.java deleted file mode 100644 index 49ef099f9..000000000 --- a/app/src/main/java/de/danoeh/antennapod/fragment/NavDrawerFragment.java +++ /dev/null @@ -1,479 +0,0 @@ -package de.danoeh.antennapod.fragment; - -import android.app.Activity; -import android.content.Context; -import android.content.DialogInterface; -import android.content.Intent; -import android.content.SharedPreferences; -import android.content.res.ColorStateList; -import android.graphics.Color; -import android.os.Build; -import android.os.Bundle; -import android.util.Log; -import android.view.ContextMenu; -import android.view.LayoutInflater; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ProgressBar; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; -import androidx.core.graphics.Insets; -import androidx.core.util.Pair; -import androidx.core.view.ViewCompat; -import androidx.core.view.WindowInsetsCompat; -import androidx.fragment.app.Fragment; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; - -import com.google.android.material.bottomsheet.BottomSheetBehavior; -import com.google.android.material.shape.MaterialShapeDrawable; -import com.google.android.material.shape.ShapeAppearanceModel; - -import de.danoeh.antennapod.core.storage.EpisodeCleanupAlgorithmFactory; -import org.apache.commons.lang3.StringUtils; -import org.greenrobot.eventbus.EventBus; -import org.greenrobot.eventbus.Subscribe; -import org.greenrobot.eventbus.ThreadMode; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.activity.MainActivity; -import de.danoeh.antennapod.activity.PreferenceActivity; -import de.danoeh.antennapod.adapter.NavListAdapter; -import de.danoeh.antennapod.core.dialog.ConfirmationDialog; -import de.danoeh.antennapod.core.menuhandler.MenuItemUtils; -import de.danoeh.antennapod.storage.database.DBReader; -import de.danoeh.antennapod.storage.database.DBWriter; -import de.danoeh.antennapod.storage.database.NavDrawerData; -import de.danoeh.antennapod.dialog.DrawerPreferencesDialog; -import de.danoeh.antennapod.dialog.RemoveFeedDialog; -import de.danoeh.antennapod.dialog.RenameItemDialog; -import de.danoeh.antennapod.dialog.SubscriptionsFilterDialog; -import de.danoeh.antennapod.dialog.TagSettingsDialog; -import de.danoeh.antennapod.event.FeedListUpdateEvent; -import de.danoeh.antennapod.event.QueueEvent; -import de.danoeh.antennapod.event.UnreadItemsUpdateEvent; -import de.danoeh.antennapod.model.feed.Feed; -import de.danoeh.antennapod.storage.preferences.UserPreferences; -import de.danoeh.antennapod.ui.appstartintent.MainActivityStarter; -import de.danoeh.antennapod.ui.common.ThemeUtils; -import de.danoeh.antennapod.ui.home.HomeFragment; -import io.reactivex.Observable; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.Disposable; -import io.reactivex.schedulers.Schedulers; - -public class NavDrawerFragment extends Fragment implements SharedPreferences.OnSharedPreferenceChangeListener { - @VisibleForTesting - public static final String PREF_LAST_FRAGMENT_TAG = "prefLastFragmentTag"; - private static final String PREF_OPEN_FOLDERS = "prefOpenFolders"; - @VisibleForTesting - public static final String PREF_NAME = "NavDrawerPrefs"; - public static final String TAG = "NavDrawerFragment"; - - public static final String[] NAV_DRAWER_TAGS = { - HomeFragment.TAG, - QueueFragment.TAG, - InboxFragment.TAG, - AllEpisodesFragment.TAG, - SubscriptionFragment.TAG, - CompletedDownloadsFragment.TAG, - PlaybackHistoryFragment.TAG, - AddFeedFragment.TAG, - NavListAdapter.SUBSCRIPTION_LIST_TAG - }; - - private NavDrawerData navDrawerData; - private int reclaimableSpace = 0; - private List flatItemList; - private NavDrawerData.DrawerItem contextPressedItem = null; - private NavListAdapter navAdapter; - private Disposable disposable; - private ProgressBar progressBar; - private Set openFolders = new HashSet<>(); - - @Override - public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, - @Nullable Bundle savedInstanceState) { - super.onCreateView(inflater, container, savedInstanceState); - View root = inflater.inflate(R.layout.nav_list, container, false); - setupDrawerRoundBackground(root); - ViewCompat.setOnApplyWindowInsetsListener(root, (view, insets) -> { - Insets bars = insets.getInsets(WindowInsetsCompat.Type.systemBars()); - view.setPadding(bars.left, bars.top, bars.right, 0); - float navigationBarHeight = 0; - Activity activity = getActivity(); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && activity != null) { - navigationBarHeight = getActivity().getWindow().getNavigationBarDividerColor() == Color.TRANSPARENT - ? 0 : 1 * getResources().getDisplayMetrics().density; // Assuming the divider is 1dp in height - } - float bottomInset = Math.max(0f, Math.round(bars.bottom - navigationBarHeight)); - ((ViewGroup.MarginLayoutParams) view.getLayoutParams()).bottomMargin = (int) bottomInset; - return insets; - }); - - SharedPreferences preferences = getContext().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); - openFolders = new HashSet<>(preferences.getStringSet(PREF_OPEN_FOLDERS, new HashSet<>())); // Must not modify - - progressBar = root.findViewById(R.id.progressBar); - RecyclerView navList = root.findViewById(R.id.nav_list); - navAdapter = new NavListAdapter(itemAccess, getActivity()); - navAdapter.setHasStableIds(true); - navList.setAdapter(navAdapter); - navList.setLayoutManager(new LinearLayoutManager(getContext())); - - root.findViewById(R.id.nav_settings).setOnClickListener(v -> - startActivity(new Intent(getActivity(), PreferenceActivity.class))); - - preferences.registerOnSharedPreferenceChangeListener(this); - return root; - } - - private void setupDrawerRoundBackground(View root) { - // Akin to this logic: - // https://github.com/material-components/material-components-android/blob/8938da8c/lib/java/com/google/android/material/navigation/NavigationView.java#L405 - ShapeAppearanceModel.Builder shapeBuilder = ShapeAppearanceModel.builder(); - float cornerSize = getResources().getDimension(R.dimen.drawer_corner_size); - boolean isRtl = getResources().getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; - if (isRtl) { - shapeBuilder.setTopLeftCornerSize(cornerSize).setBottomLeftCornerSize(cornerSize); - } else { - shapeBuilder.setTopRightCornerSize(cornerSize).setBottomRightCornerSize(cornerSize); - } - MaterialShapeDrawable drawable = new MaterialShapeDrawable(shapeBuilder.build()); - int themeColor = ThemeUtils.getColorFromAttr(root.getContext(), android.R.attr.colorBackground); - drawable.setFillColor(ColorStateList.valueOf(themeColor)); - root.setBackground(drawable); - } - - @Override - public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - EventBus.getDefault().register(this); - } - - @Override - public void onDestroyView() { - super.onDestroyView(); - EventBus.getDefault().unregister(this); - if (disposable != null) { - disposable.dispose(); - } - getContext().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) - .unregisterOnSharedPreferenceChangeListener(this); - } - - @Override - public void onCreateContextMenu(@NonNull ContextMenu menu, @NonNull View v, ContextMenu.ContextMenuInfo menuInfo) { - super.onCreateContextMenu(menu, v, menuInfo); - MenuInflater inflater = getActivity().getMenuInflater(); - menu.setHeaderTitle(contextPressedItem.getTitle()); - if (contextPressedItem.type == NavDrawerData.DrawerItem.Type.FEED) { - inflater.inflate(R.menu.nav_feed_context, menu); - // episodes are not loaded, so we cannot check if the podcast has new or unplayed ones! - } else { - inflater.inflate(R.menu.nav_folder_context, menu); - } - MenuItemUtils.setOnClickListeners(menu, this::onContextItemSelected); - } - - @Override - public boolean onContextItemSelected(@NonNull MenuItem item) { - NavDrawerData.DrawerItem pressedItem = contextPressedItem; - contextPressedItem = null; - if (pressedItem == null) { - return false; - } - if (pressedItem.type == NavDrawerData.DrawerItem.Type.FEED) { - return onFeedContextMenuClicked(((NavDrawerData.FeedDrawerItem) pressedItem).feed, item); - } else { - return onTagContextMenuClicked(pressedItem, item); - } - } - - private boolean onFeedContextMenuClicked(Feed feed, MenuItem item) { - final int itemId = item.getItemId(); - if (itemId == R.id.remove_all_inbox_item) { - ConfirmationDialog removeAllNewFlagsConfirmationDialog = new ConfirmationDialog(getContext(), - R.string.remove_all_inbox_label, - R.string.remove_all_inbox_confirmation_msg) { - @Override - public void onConfirmButtonPressed(DialogInterface dialog) { - dialog.dismiss(); - DBWriter.removeFeedNewFlag(feed.getId()); - } - }; - removeAllNewFlagsConfirmationDialog.createNewDialog().show(); - return true; - } else if (itemId == R.id.edit_tags) { - TagSettingsDialog.newInstance(Collections.singletonList(feed.getPreferences())) - .show(getChildFragmentManager(), TagSettingsDialog.TAG); - return true; - } else if (itemId == R.id.rename_item) { - new RenameItemDialog(getActivity(), feed).show(); - return true; - } else if (itemId == R.id.remove_feed) { - RemoveFeedDialog.show(getContext(), feed, () -> { - if (String.valueOf(feed.getId()).equals(getLastNavFragment(getContext()))) { - ((MainActivity) getActivity()).loadFragment(UserPreferences.getDefaultPage(), null); - // Make sure fragment is hidden before actually starting to delete - getActivity().getSupportFragmentManager().executePendingTransactions(); - } - }); - return true; - } - return super.onContextItemSelected(item); - } - - private boolean onTagContextMenuClicked(NavDrawerData.DrawerItem drawerItem, MenuItem item) { - final int itemId = item.getItemId(); - if (itemId == R.id.rename_folder_item) { - new RenameItemDialog(getActivity(), drawerItem).show(); - return true; - } - return super.onContextItemSelected(item); - } - - @Subscribe(threadMode = ThreadMode.MAIN) - public void onUnreadItemsChanged(UnreadItemsUpdateEvent event) { - loadData(); - } - - - @Subscribe(threadMode = ThreadMode.MAIN) - public void onFeedListChanged(FeedListUpdateEvent event) { - loadData(); - } - - @Subscribe(threadMode = ThreadMode.MAIN) - public void onQueueChanged(QueueEvent event) { - Log.d(TAG, "onQueueChanged(" + event + ")"); - // we are only interested in the number of queue items, not download status or position - if (event.action == QueueEvent.Action.DELETED_MEDIA - || event.action == QueueEvent.Action.SORTED - || event.action == QueueEvent.Action.MOVED) { - return; - } - loadData(); - } - - @Override - public void onResume() { - super.onResume(); - loadData(); - } - - private final NavListAdapter.ItemAccess itemAccess = new NavListAdapter.ItemAccess() { - @Override - public int getCount() { - if (flatItemList != null) { - return flatItemList.size(); - } else { - return 0; - } - } - - @Override - public NavDrawerData.DrawerItem getItem(int position) { - if (flatItemList != null && 0 <= position && position < flatItemList.size()) { - return flatItemList.get(position); - } else { - return null; - } - } - - @Override - public boolean isSelected(int position) { - String lastNavFragment = getLastNavFragment(getContext()); - if (position < navAdapter.getSubscriptionOffset()) { - return navAdapter.getFragmentTags().get(position).equals(lastNavFragment); - } else if (StringUtils.isNumeric(lastNavFragment)) { // last fragment was not a list, but a feed - long feedId = Long.parseLong(lastNavFragment); - if (navDrawerData != null) { - NavDrawerData.DrawerItem itemToCheck = flatItemList.get( - position - navAdapter.getSubscriptionOffset()); - if (itemToCheck.type == NavDrawerData.DrawerItem.Type.FEED) { - // When the same feed is displayed multiple times, it should be highlighted multiple times. - return ((NavDrawerData.FeedDrawerItem) itemToCheck).feed.getId() == feedId; - } - } - } - return false; - } - - @Override - public int getQueueSize() { - return (navDrawerData != null) ? navDrawerData.queueSize : 0; - } - - @Override - public int getNumberOfNewItems() { - return (navDrawerData != null) ? navDrawerData.numNewItems : 0; - } - - @Override - public int getNumberOfDownloadedItems() { - return (navDrawerData != null) ? navDrawerData.numDownloadedItems : 0; - } - - @Override - public int getReclaimableItems() { - return reclaimableSpace; - } - - @Override - public int getFeedCounterSum() { - if (navDrawerData == null) { - return 0; - } - int sum = 0; - for (int counter : navDrawerData.feedCounters.values()) { - sum += counter; - } - return sum; - } - - @Override - public void onItemClick(int position) { - int viewType = navAdapter.getItemViewType(position); - if (viewType != NavListAdapter.VIEW_TYPE_SECTION_DIVIDER) { - if (position < navAdapter.getSubscriptionOffset()) { - String tag = navAdapter.getFragmentTags().get(position); - ((MainActivity) getActivity()).loadFragment(tag, null); - ((MainActivity) getActivity()).getBottomSheet().setState(BottomSheetBehavior.STATE_COLLAPSED); - } else { - int pos = position - navAdapter.getSubscriptionOffset(); - NavDrawerData.DrawerItem clickedItem = flatItemList.get(pos); - - if (clickedItem.type == NavDrawerData.DrawerItem.Type.FEED) { - long feedId = ((NavDrawerData.FeedDrawerItem) clickedItem).feed.getId(); - ((MainActivity) getActivity()).loadFeedFragmentById(feedId, null); - ((MainActivity) getActivity()).getBottomSheet() - .setState(BottomSheetBehavior.STATE_COLLAPSED); - } else { - NavDrawerData.TagDrawerItem folder = ((NavDrawerData.TagDrawerItem) clickedItem); - if (openFolders.contains(folder.getTitle())) { - openFolders.remove(folder.getTitle()); - } else { - openFolders.add(folder.getTitle()); - } - - getContext().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) - .edit() - .putStringSet(PREF_OPEN_FOLDERS, openFolders) - .apply(); - - disposable = Observable.fromCallable(() -> makeFlatDrawerData(navDrawerData.items, 0)) - .subscribeOn(Schedulers.computation()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - result -> { - flatItemList = result; - navAdapter.notifyDataSetChanged(); - }, error -> Log.e(TAG, Log.getStackTraceString(error))); - } - } - } else if (UserPreferences.getSubscriptionsFilter().isEnabled() - && navAdapter.showSubscriptionList) { - new SubscriptionsFilterDialog().show(getChildFragmentManager(), "filter"); - } - } - - @Override - public boolean onItemLongClick(int position) { - if (position < navAdapter.getFragmentTags().size()) { - DrawerPreferencesDialog.show(getContext(), () -> { - navAdapter.notifyDataSetChanged(); - if (UserPreferences.getHiddenDrawerItems().contains(getLastNavFragment(getContext()))) { - new MainActivityStarter(getContext()) - .withFragmentLoaded(UserPreferences.getDefaultPage()) - .withDrawerOpen() - .start(); - } - }); - return true; - } else { - contextPressedItem = flatItemList.get(position - navAdapter.getSubscriptionOffset()); - return false; - } - } - - @Override - public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { - NavDrawerFragment.this.onCreateContextMenu(menu, v, menuInfo); - } - }; - - private void loadData() { - disposable = Observable.fromCallable( - () -> { - NavDrawerData data = DBReader.getNavDrawerData(UserPreferences.getSubscriptionsFilter(), - UserPreferences.getFeedOrder(), UserPreferences.getFeedCounterSetting()); - reclaimableSpace = EpisodeCleanupAlgorithmFactory.build().getReclaimableItems(); - return new Pair<>(data, makeFlatDrawerData(data.items, 0)); - }) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - result -> { - navDrawerData = result.first; - flatItemList = result.second; - navAdapter.notifyDataSetChanged(); - progressBar.setVisibility(View.GONE); // Stays hidden once there is something in the list - }, error -> { - Log.e(TAG, Log.getStackTraceString(error)); - progressBar.setVisibility(View.GONE); - }); - } - - private List makeFlatDrawerData(List items, int layer) { - List flatItems = new ArrayList<>(); - for (NavDrawerData.DrawerItem item : items) { - item.setLayer(layer); - flatItems.add(item); - if (item.type == NavDrawerData.DrawerItem.Type.TAG) { - NavDrawerData.TagDrawerItem folder = ((NavDrawerData.TagDrawerItem) item); - folder.setOpen(openFolders.contains(folder.getTitle())); - if (folder.isOpen()) { - flatItems.addAll(makeFlatDrawerData(((NavDrawerData.TagDrawerItem) item).children, layer + 1)); - } - } - } - return flatItems; - } - - public static void saveLastNavFragment(Context context, String tag) { - Log.d(TAG, "saveLastNavFragment(tag: " + tag + ")"); - SharedPreferences prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); - SharedPreferences.Editor edit = prefs.edit(); - if (tag != null) { - edit.putString(PREF_LAST_FRAGMENT_TAG, tag); - } else { - edit.remove(PREF_LAST_FRAGMENT_TAG); - } - edit.apply(); - } - - public static String getLastNavFragment(Context context) { - SharedPreferences prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); - String lastFragment = prefs.getString(PREF_LAST_FRAGMENT_TAG, HomeFragment.TAG); - Log.d(TAG, "getLastNavFragment() -> " + lastFragment); - return lastFragment; - } - - @Override - public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { - if (PREF_LAST_FRAGMENT_TAG.equals(key)) { - navAdapter.notifyDataSetChanged(); // Update selection - } - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/PlaybackHistoryFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/PlaybackHistoryFragment.java deleted file mode 100644 index beac47f7f..000000000 --- a/app/src/main/java/de/danoeh/antennapod/fragment/PlaybackHistoryFragment.java +++ /dev/null @@ -1,109 +0,0 @@ -package de.danoeh.antennapod.fragment; - -import android.content.DialogInterface; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import androidx.annotation.NonNull; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.core.dialog.ConfirmationDialog; -import de.danoeh.antennapod.storage.database.DBReader; -import de.danoeh.antennapod.storage.database.DBWriter; -import de.danoeh.antennapod.event.playback.PlaybackHistoryEvent; -import de.danoeh.antennapod.model.feed.FeedItem; -import de.danoeh.antennapod.model.feed.FeedItemFilter; -import de.danoeh.antennapod.model.feed.SortOrder; -import org.greenrobot.eventbus.Subscribe; -import org.greenrobot.eventbus.ThreadMode; - -import java.util.List; - -public class PlaybackHistoryFragment extends EpisodesListFragment { - public static final String TAG = "PlaybackHistoryFragment"; - private static final FeedItemFilter FILTER_HISTORY = new FeedItemFilter(FeedItemFilter.IS_IN_HISTORY); - - @NonNull - @Override - public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - final View root = super.onCreateView(inflater, container, savedInstanceState); - toolbar.inflateMenu(R.menu.playback_history); - toolbar.setTitle(R.string.playback_history_label); - updateToolbar(); - emptyView.setIcon(R.drawable.ic_history); - emptyView.setTitle(R.string.no_history_head_label); - emptyView.setMessage(R.string.no_history_label); - return root; - } - - @Override - protected FeedItemFilter getFilter() { - return FeedItemFilter.unfiltered(); - } - - @Override - protected String getFragmentTag() { - return TAG; - } - - @Override - protected String getPrefName() { - return TAG; - } - - @Override - public boolean onMenuItemClick(MenuItem item) { - if (super.onMenuItemClick(item)) { - return true; - } - if (item.getItemId() == R.id.clear_history_item) { - - ConfirmationDialog conDialog = new ConfirmationDialog( - getActivity(), - R.string.clear_history_label, - R.string.clear_playback_history_msg) { - - @Override - public void onConfirmButtonPressed(DialogInterface dialog) { - dialog.dismiss(); - DBWriter.clearPlaybackHistory(); - } - }; - conDialog.createNewDialog().show(); - - return true; - } - return false; - } - - @Override - protected void updateToolbar() { - // Not calling super, as we do not have a refresh button that could be updated - toolbar.getMenu().findItem(R.id.clear_history_item).setVisible(!episodes.isEmpty()); - } - - @Subscribe(threadMode = ThreadMode.MAIN) - public void onHistoryUpdated(PlaybackHistoryEvent event) { - loadItems(); - updateToolbar(); - } - - @NonNull - @Override - protected List loadData() { - return DBReader.getEpisodes(0, page * EPISODES_PER_PAGE, FILTER_HISTORY, SortOrder.COMPLETION_DATE_NEW_OLD); - } - - @NonNull - @Override - protected List loadMoreData(int page) { - return DBReader.getEpisodes((page - 1) * EPISODES_PER_PAGE, EPISODES_PER_PAGE, FILTER_HISTORY, - SortOrder.COMPLETION_DATE_NEW_OLD); - } - - @Override - protected int loadTotalItemCount() { - return DBReader.getTotalEpisodeCount(FILTER_HISTORY); - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java deleted file mode 100644 index 126d0d748..000000000 --- a/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java +++ /dev/null @@ -1,639 +0,0 @@ -package de.danoeh.antennapod.fragment; - -import android.content.Context; -import android.content.DialogInterface; -import android.content.SharedPreferences; -import android.os.Bundle; -import android.util.Log; -import android.view.ContextMenu; -import android.view.KeyEvent; -import android.view.LayoutInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.widget.CheckBox; -import android.widget.ProgressBar; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.fragment.app.Fragment; -import androidx.recyclerview.widget.ItemTouchHelper; -import androidx.recyclerview.widget.RecyclerView; -import androidx.recyclerview.widget.SimpleItemAnimator; -import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; - -import com.google.android.material.appbar.MaterialToolbar; -import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import com.google.android.material.snackbar.Snackbar; -import com.leinardi.android.speeddial.SpeedDialView; - -import de.danoeh.antennapod.net.download.serviceinterface.FeedUpdateManager; -import de.danoeh.antennapod.ui.episodes.PlaybackSpeedUtils; -import org.greenrobot.eventbus.EventBus; -import org.greenrobot.eventbus.Subscribe; -import org.greenrobot.eventbus.ThreadMode; - -import java.util.List; - -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.activity.MainActivity; -import de.danoeh.antennapod.adapter.EpisodeItemListAdapter; -import de.danoeh.antennapod.adapter.QueueRecyclerAdapter; -import de.danoeh.antennapod.core.dialog.ConfirmationDialog; -import de.danoeh.antennapod.core.menuhandler.MenuItemUtils; -import de.danoeh.antennapod.storage.database.DBReader; -import de.danoeh.antennapod.storage.database.DBWriter; -import de.danoeh.antennapod.ui.common.Converter; -import de.danoeh.antennapod.core.util.FeedItemUtil; -import de.danoeh.antennapod.dialog.ItemSortDialog; -import de.danoeh.antennapod.event.EpisodeDownloadEvent; -import de.danoeh.antennapod.event.FeedItemEvent; -import de.danoeh.antennapod.event.FeedUpdateRunningEvent; -import de.danoeh.antennapod.event.PlayerStatusEvent; -import de.danoeh.antennapod.event.QueueEvent; -import de.danoeh.antennapod.event.UnreadItemsUpdateEvent; -import de.danoeh.antennapod.event.playback.PlaybackPositionEvent; -import de.danoeh.antennapod.fragment.actions.EpisodeMultiSelectActionHandler; -import de.danoeh.antennapod.fragment.swipeactions.SwipeActions; -import de.danoeh.antennapod.menuhandler.FeedItemMenuHandler; -import de.danoeh.antennapod.model.feed.FeedItem; -import de.danoeh.antennapod.model.feed.FeedItemFilter; -import de.danoeh.antennapod.model.feed.SortOrder; -import de.danoeh.antennapod.storage.preferences.UserPreferences; -import de.danoeh.antennapod.view.EmptyViewHandler; -import de.danoeh.antennapod.view.EpisodeItemListRecyclerView; -import de.danoeh.antennapod.view.LiftOnScrollListener; -import de.danoeh.antennapod.view.viewholder.EpisodeItemViewHolder; -import io.reactivex.Observable; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.Disposable; -import io.reactivex.schedulers.Schedulers; - -/** - * Shows all items in the queue. - */ -public class QueueFragment extends Fragment implements MaterialToolbar.OnMenuItemClickListener, - EpisodeItemListAdapter.OnSelectModeListener { - public static final String TAG = "QueueFragment"; - private static final String KEY_UP_ARROW = "up_arrow"; - - private TextView infoBar; - private EpisodeItemListRecyclerView recyclerView; - private QueueRecyclerAdapter recyclerAdapter; - private EmptyViewHandler emptyView; - private MaterialToolbar toolbar; - private SwipeRefreshLayout swipeRefreshLayout; - private boolean displayUpArrow; - - private List queue; - - private static final String PREFS = "QueueFragment"; - private static final String PREF_SHOW_LOCK_WARNING = "show_lock_warning"; - - private Disposable disposable; - private SwipeActions swipeActions; - private SharedPreferences prefs; - - private SpeedDialView speedDialView; - private ProgressBar progressBar; - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - prefs = getActivity().getSharedPreferences(PREFS, Context.MODE_PRIVATE); - } - - @Override - public void onStart() { - super.onStart(); - if (queue != null) { - recyclerView.restoreScrollPosition(QueueFragment.TAG); - } - loadItems(true); - EventBus.getDefault().register(this); - } - - @Override - public void onPause() { - super.onPause(); - recyclerView.saveScrollPosition(QueueFragment.TAG); - } - - @Override - public void onStop() { - super.onStop(); - EventBus.getDefault().unregister(this); - if (disposable != null) { - disposable.dispose(); - } - } - - @Subscribe(threadMode = ThreadMode.MAIN) - public void onEventMainThread(QueueEvent event) { - Log.d(TAG, "onEventMainThread() called with: " + "event = [" + event + "]"); - if (queue == null) { - return; - } else if (recyclerAdapter == null) { - loadItems(true); - return; - } - switch(event.action) { - case ADDED: - queue.add(event.position, event.item); - recyclerAdapter.notifyItemInserted(event.position); - break; - case SET_QUEUE: - case SORTED: //Deliberate fall-through - queue = event.items; - recyclerAdapter.updateItems(event.items); - break; - case REMOVED: - case IRREVERSIBLE_REMOVED: - int position = FeedItemUtil.indexOfItemWithId(queue, event.item.getId()); - queue.remove(position); - recyclerAdapter.notifyItemRemoved(position); - break; - case CLEARED: - queue.clear(); - recyclerAdapter.updateItems(queue); - break; - case MOVED: - return; - } - recyclerAdapter.updateDragDropEnabled(); - refreshToolbarState(); - recyclerView.saveScrollPosition(QueueFragment.TAG); - refreshInfoBar(); - } - - @Subscribe(threadMode = ThreadMode.MAIN) - public void onEventMainThread(FeedItemEvent event) { - Log.d(TAG, "onEventMainThread() called with: " + "event = [" + event + "]"); - if (queue == null) { - return; - } else if (recyclerAdapter == null) { - loadItems(true); - return; - } - for (int i = 0, size = event.items.size(); i < size; i++) { - FeedItem item = event.items.get(i); - int pos = FeedItemUtil.indexOfItemWithId(queue, item.getId()); - if (pos >= 0) { - queue.remove(pos); - queue.add(pos, item); - recyclerAdapter.notifyItemChangedCompat(pos); - refreshInfoBar(); - } - } - } - - @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) - public void onEventMainThread(EpisodeDownloadEvent event) { - if (queue == null) { - return; - } - for (String downloadUrl : event.getUrls()) { - int pos = FeedItemUtil.indexOfItemWithDownloadUrl(queue, downloadUrl); - if (pos >= 0) { - recyclerAdapter.notifyItemChangedCompat(pos); - } - } - } - - @Subscribe(threadMode = ThreadMode.MAIN) - public void onEventMainThread(PlaybackPositionEvent event) { - if (recyclerAdapter != null) { - for (int i = 0; i < recyclerAdapter.getItemCount(); i++) { - EpisodeItemViewHolder holder = (EpisodeItemViewHolder) - recyclerView.findViewHolderForAdapterPosition(i); - if (holder != null && holder.isCurrentlyPlayingItem()) { - holder.notifyPlaybackPositionUpdated(event); - break; - } - } - } - } - - @Subscribe(threadMode = ThreadMode.MAIN) - public void onPlayerStatusChanged(PlayerStatusEvent event) { - loadItems(false); - refreshToolbarState(); - } - - @Subscribe(threadMode = ThreadMode.MAIN) - public void onUnreadItemsChanged(UnreadItemsUpdateEvent event) { - // Sent when playback position is reset - loadItems(false); - refreshToolbarState(); - } - - @Subscribe(threadMode = ThreadMode.MAIN) - public void onKeyUp(KeyEvent event) { - if (!isAdded() || !isVisible() || !isMenuVisible()) { - return; - } - switch (event.getKeyCode()) { - case KeyEvent.KEYCODE_T: - recyclerView.smoothScrollToPosition(0); - break; - case KeyEvent.KEYCODE_B: - recyclerView.smoothScrollToPosition(recyclerAdapter.getItemCount() - 1); - break; - default: - break; - } - } - - @Override - public void onDestroyView() { - super.onDestroyView(); - if (recyclerAdapter != null) { - recyclerAdapter.endSelectMode(); - } - recyclerAdapter = null; - if (toolbar != null) { - toolbar.setOnMenuItemClickListener(null); - toolbar.setOnLongClickListener(null); - } - } - - private void refreshToolbarState() { - boolean keepSorted = UserPreferences.isQueueKeepSorted(); - toolbar.getMenu().findItem(R.id.queue_lock).setChecked(UserPreferences.isQueueLocked()); - toolbar.getMenu().findItem(R.id.queue_lock).setVisible(!keepSorted); - } - - @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) - public void onEventMainThread(FeedUpdateRunningEvent event) { - swipeRefreshLayout.setRefreshing(event.isFeedUpdateRunning); - } - - @Override - public boolean onMenuItemClick(MenuItem item) { - final int itemId = item.getItemId(); - if (itemId == R.id.queue_lock) { - toggleQueueLock(); - return true; - } else if (itemId == R.id.queue_sort) { - new QueueSortDialog().show(getChildFragmentManager().beginTransaction(), "SortDialog"); - return true; - } else if (itemId == R.id.refresh_item) { - FeedUpdateManager.getInstance().runOnceOrAsk(requireContext()); - return true; - } else if (itemId == R.id.clear_queue) { - // make sure the user really wants to clear the queue - ConfirmationDialog conDialog = new ConfirmationDialog(getActivity(), - R.string.clear_queue_label, - R.string.clear_queue_confirmation_msg) { - - @Override - public void onConfirmButtonPressed( - DialogInterface dialog) { - dialog.dismiss(); - DBWriter.clearQueue(); - } - }; - conDialog.createNewDialog().show(); - return true; - } else if (itemId == R.id.action_search) { - ((MainActivity) getActivity()).loadChildFragment(SearchFragment.newInstance()); - return true; - } - return false; - } - - private void toggleQueueLock() { - boolean isLocked = UserPreferences.isQueueLocked(); - if (isLocked) { - setQueueLocked(false); - } else { - boolean shouldShowLockWarning = prefs.getBoolean(PREF_SHOW_LOCK_WARNING, true); - if (!shouldShowLockWarning) { - setQueueLocked(true); - } else { - MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(getContext()); - builder.setTitle(R.string.lock_queue); - builder.setMessage(R.string.queue_lock_warning); - - View view = View.inflate(getContext(), R.layout.checkbox_do_not_show_again, null); - CheckBox checkDoNotShowAgain = view.findViewById(R.id.checkbox_do_not_show_again); - builder.setView(view); - - builder.setPositiveButton(R.string.lock_queue, (dialog, which) -> { - prefs.edit().putBoolean(PREF_SHOW_LOCK_WARNING, !checkDoNotShowAgain.isChecked()).apply(); - setQueueLocked(true); - }); - builder.setNegativeButton(R.string.cancel_label, null); - builder.show(); - } - } - } - - private void setQueueLocked(boolean locked) { - UserPreferences.setQueueLocked(locked); - refreshToolbarState(); - if (recyclerAdapter != null) { - recyclerAdapter.updateDragDropEnabled(); - } - if (queue.size() == 0) { - if (locked) { - ((MainActivity) getActivity()).showSnackbarAbovePlayer(R.string.queue_locked, Snackbar.LENGTH_SHORT); - } else { - ((MainActivity) getActivity()).showSnackbarAbovePlayer(R.string.queue_unlocked, Snackbar.LENGTH_SHORT); - } - } - } - - @Override - public boolean onContextItemSelected(MenuItem item) { - Log.d(TAG, "onContextItemSelected() called with: " + "item = [" + item + "]"); - if (!isVisible() || recyclerAdapter == null) { - return false; - } - FeedItem selectedItem = recyclerAdapter.getLongPressedItem(); - if (selectedItem == null) { - Log.i(TAG, "Selected item was null, ignoring selection"); - return super.onContextItemSelected(item); - } - - int position = FeedItemUtil.indexOfItemWithId(queue, selectedItem.getId()); - if (position < 0) { - Log.i(TAG, "Selected item no longer exist, ignoring selection"); - return super.onContextItemSelected(item); - } - if (recyclerAdapter.onContextItemSelected(item)) { - return true; - } - - final int itemId = item.getItemId(); - if (itemId == R.id.move_to_top_item) { - queue.add(0, queue.remove(position)); - recyclerAdapter.notifyItemMoved(position, 0); - DBWriter.moveQueueItemToTop(selectedItem.getId(), true); - return true; - } else if (itemId == R.id.move_to_bottom_item) { - queue.add(queue.size() - 1, queue.remove(position)); - recyclerAdapter.notifyItemMoved(position, queue.size() - 1); - DBWriter.moveQueueItemToBottom(selectedItem.getId(), true); - return true; - } - return FeedItemMenuHandler.onMenuItemClicked(this, item.getItemId(), selectedItem); - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - super.onCreateView(inflater, container, savedInstanceState); - View root = inflater.inflate(R.layout.queue_fragment, container, false); - toolbar = root.findViewById(R.id.toolbar); - toolbar.setOnMenuItemClickListener(this); - toolbar.setOnLongClickListener(v -> { - recyclerView.scrollToPosition(5); - recyclerView.post(() -> recyclerView.smoothScrollToPosition(0)); - return false; - }); - displayUpArrow = getParentFragmentManager().getBackStackEntryCount() != 0; - if (savedInstanceState != null) { - displayUpArrow = savedInstanceState.getBoolean(KEY_UP_ARROW); - } - ((MainActivity) getActivity()).setupToolbarToggle(toolbar, displayUpArrow); - toolbar.inflateMenu(R.menu.queue); - refreshToolbarState(); - progressBar = root.findViewById(R.id.progressBar); - progressBar.setVisibility(View.VISIBLE); - - infoBar = root.findViewById(R.id.info_bar); - recyclerView = root.findViewById(R.id.recyclerView); - RecyclerView.ItemAnimator animator = recyclerView.getItemAnimator(); - if (animator instanceof SimpleItemAnimator) { - ((SimpleItemAnimator) animator).setSupportsChangeAnimations(false); - } - recyclerView.setRecycledViewPool(((MainActivity) getActivity()).getRecycledViewPool()); - registerForContextMenu(recyclerView); - recyclerView.addOnScrollListener(new LiftOnScrollListener(root.findViewById(R.id.appbar))); - - swipeActions = new QueueSwipeActions(); - swipeActions.setFilter(new FeedItemFilter(FeedItemFilter.QUEUED)); - swipeActions.attachTo(recyclerView); - - recyclerAdapter = new QueueRecyclerAdapter((MainActivity) getActivity(), swipeActions) { - @Override - public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { - super.onCreateContextMenu(menu, v, menuInfo); - MenuItemUtils.setOnClickListeners(menu, QueueFragment.this::onContextItemSelected); - } - }; - recyclerAdapter.setOnSelectModeListener(this); - recyclerView.setAdapter(recyclerAdapter); - - swipeRefreshLayout = root.findViewById(R.id.swipeRefresh); - swipeRefreshLayout.setDistanceToTriggerSync(getResources().getInteger(R.integer.swipe_refresh_distance)); - swipeRefreshLayout.setOnRefreshListener(() -> FeedUpdateManager.getInstance().runOnceOrAsk(requireContext())); - - emptyView = new EmptyViewHandler(getContext()); - emptyView.attachToRecyclerView(recyclerView); - emptyView.setIcon(R.drawable.ic_playlist_play); - emptyView.setTitle(R.string.no_items_header_label); - emptyView.setMessage(R.string.no_items_label); - emptyView.updateAdapter(recyclerAdapter); - - speedDialView = root.findViewById(R.id.fabSD); - speedDialView.setOverlayLayout(root.findViewById(R.id.fabSDOverlay)); - speedDialView.inflate(R.menu.episodes_apply_action_speeddial); - speedDialView.removeActionItemById(R.id.mark_read_batch); - speedDialView.removeActionItemById(R.id.mark_unread_batch); - speedDialView.removeActionItemById(R.id.add_to_queue_batch); - speedDialView.removeActionItemById(R.id.remove_all_inbox_item); - speedDialView.setOnChangeListener(new SpeedDialView.OnChangeListener() { - @Override - public boolean onMainActionSelected() { - return false; - } - - @Override - public void onToggleChanged(boolean open) { - if (open && recyclerAdapter.getSelectedCount() == 0) { - ((MainActivity) getActivity()).showSnackbarAbovePlayer(R.string.no_items_selected, - Snackbar.LENGTH_SHORT); - speedDialView.close(); - } - } - }); - speedDialView.setOnActionSelectedListener(actionItem -> { - new EpisodeMultiSelectActionHandler(((MainActivity) getActivity()), actionItem.getId()) - .handleAction(recyclerAdapter.getSelectedItems()); - recyclerAdapter.endSelectMode(); - return true; - }); - return root; - } - - @Override - public void onSaveInstanceState(@NonNull Bundle outState) { - outState.putBoolean(KEY_UP_ARROW, displayUpArrow); - super.onSaveInstanceState(outState); - } - - private void refreshInfoBar() { - String info = getResources().getQuantityString(R.plurals.num_episodes, queue.size(), queue.size()); - if (!queue.isEmpty()) { - long timeLeft = 0; - for (FeedItem item : queue) { - float playbackSpeed = 1; - if (UserPreferences.timeRespectsSpeed()) { - playbackSpeed = PlaybackSpeedUtils.getCurrentPlaybackSpeed(item.getMedia()); - } - if (item.getMedia() != null) { - long itemTimeLeft = item.getMedia().getDuration() - item.getMedia().getPosition(); - timeLeft += itemTimeLeft / playbackSpeed; - } - } - info += " • "; - info += getString(R.string.time_left_label); - info += Converter.getDurationStringLocalized(getResources(), timeLeft, false); - } - infoBar.setText(info); - } - - private void loadItems(final boolean restoreScrollPosition) { - Log.d(TAG, "loadItems()"); - if (disposable != null) { - disposable.dispose(); - } - if (queue == null) { - emptyView.hide(); - } - disposable = Observable.fromCallable(DBReader::getQueue) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(items -> { - queue = items; - progressBar.setVisibility(View.GONE); - recyclerAdapter.setDummyViews(0); - recyclerAdapter.updateItems(queue); - if (restoreScrollPosition) { - recyclerView.restoreScrollPosition(QueueFragment.TAG); - } - refreshInfoBar(); - }, error -> Log.e(TAG, Log.getStackTraceString(error))); - } - - @Override - public void onStartSelectMode() { - swipeActions.detach(); - speedDialView.setVisibility(View.VISIBLE); - refreshToolbarState(); - infoBar.setVisibility(View.GONE); - } - - @Override - public void onEndSelectMode() { - speedDialView.close(); - speedDialView.setVisibility(View.GONE); - infoBar.setVisibility(View.VISIBLE); - swipeActions.attachTo(recyclerView); - } - - public static class QueueSortDialog extends ItemSortDialog { - @Nullable - @Override - public View onCreateView(@NonNull LayoutInflater inflater, - @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - if (UserPreferences.isQueueKeepSorted()) { - sortOrder = UserPreferences.getQueueKeepSortedOrder(); - } - final View view = super.onCreateView(inflater, container, savedInstanceState); - viewBinding.keepSortedCheckbox.setVisibility(View.VISIBLE); - viewBinding.keepSortedCheckbox.setChecked(UserPreferences.isQueueKeepSorted()); - // Disable until something gets selected - viewBinding.keepSortedCheckbox.setEnabled(UserPreferences.isQueueKeepSorted()); - return view; - } - - @Override - protected void onAddItem(int title, SortOrder ascending, SortOrder descending, boolean ascendingIsDefault) { - if (ascending != SortOrder.EPISODE_FILENAME_A_Z && ascending != SortOrder.SIZE_SMALL_LARGE) { - super.onAddItem(title, ascending, descending, ascendingIsDefault); - } - } - - @Override - protected void onSelectionChanged() { - super.onSelectionChanged(); - viewBinding.keepSortedCheckbox.setEnabled(sortOrder != SortOrder.RANDOM); - if (sortOrder == SortOrder.RANDOM) { - viewBinding.keepSortedCheckbox.setChecked(false); - } - UserPreferences.setQueueKeepSorted(viewBinding.keepSortedCheckbox.isChecked()); - UserPreferences.setQueueKeepSortedOrder(sortOrder); - DBWriter.reorderQueue(sortOrder, true); - } - } - - private class QueueSwipeActions extends SwipeActions { - - // Position tracking whilst dragging - int dragFrom = -1; - int dragTo = -1; - - public QueueSwipeActions() { - super(ItemTouchHelper.UP | ItemTouchHelper.DOWN, QueueFragment.this, TAG); - } - - @Override - public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, - @NonNull RecyclerView.ViewHolder target) { - int fromPosition = viewHolder.getBindingAdapterPosition(); - int toPosition = target.getBindingAdapterPosition(); - - // Update tracked position - if (dragFrom == -1) { - dragFrom = fromPosition; - } - dragTo = toPosition; - - int from = viewHolder.getBindingAdapterPosition(); - int to = target.getBindingAdapterPosition(); - Log.d(TAG, "move(" + from + ", " + to + ") in memory"); - if (queue == null || from >= queue.size() || to >= queue.size() || from < 0 || to < 0) { - return false; - } - queue.add(to, queue.remove(from)); - recyclerAdapter.notifyItemMoved(from, to); - return true; - } - - @Override - public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) { - if (disposable != null) { - disposable.dispose(); - } - - //SwipeActions - super.onSwiped(viewHolder, direction); - } - - @Override - public boolean isLongPressDragEnabled() { - return false; - } - - @Override - public void clearView(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) { - super.clearView(recyclerView, viewHolder); - // Check if drag finished - if (dragFrom != -1 && dragTo != -1 && dragFrom != dragTo) { - reallyMoved(dragFrom, dragTo); - } - - dragFrom = dragTo = -1; - } - - private void reallyMoved(int from, int to) { - // Write drag operation to database - Log.d(TAG, "Write to database move(" + from + ", " + to + ")"); - DBWriter.moveQueueItem(from, to, true); - } - - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/SearchFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/SearchFragment.java deleted file mode 100644 index 73fce9f1f..000000000 --- a/app/src/main/java/de/danoeh/antennapod/fragment/SearchFragment.java +++ /dev/null @@ -1,460 +0,0 @@ -package de.danoeh.antennapod.fragment; - - -import android.content.Context; -import android.os.Bundle; -import android.os.Handler; -import android.os.Looper; -import android.util.Log; -import android.util.Pair; -import android.view.ContextMenu; -import android.view.LayoutInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.view.inputmethod.InputMethodManager; -import android.widget.ProgressBar; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.widget.SearchView; -import com.google.android.material.appbar.MaterialToolbar; -import androidx.fragment.app.Fragment; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; - -import com.google.android.material.chip.Chip; -import com.google.android.material.snackbar.Snackbar; -import com.leinardi.android.speeddial.SpeedDialView; - -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.activity.MainActivity; -import de.danoeh.antennapod.adapter.EpisodeItemListAdapter; -import de.danoeh.antennapod.adapter.HorizontalFeedListAdapter; -import de.danoeh.antennapod.core.menuhandler.MenuItemUtils; -import de.danoeh.antennapod.databinding.MultiSelectSpeedDialBinding; -import de.danoeh.antennapod.event.EpisodeDownloadEvent; -import de.danoeh.antennapod.event.FeedItemEvent; -import de.danoeh.antennapod.event.playback.PlaybackPositionEvent; -import de.danoeh.antennapod.event.PlayerStatusEvent; -import de.danoeh.antennapod.event.UnreadItemsUpdateEvent; -import de.danoeh.antennapod.fragment.actions.EpisodeMultiSelectActionHandler; -import de.danoeh.antennapod.model.feed.Feed; -import de.danoeh.antennapod.model.feed.FeedItem; -import de.danoeh.antennapod.core.util.FeedItemUtil; -import de.danoeh.antennapod.menuhandler.FeedItemMenuHandler; -import de.danoeh.antennapod.net.discovery.CombinedSearcher; -import de.danoeh.antennapod.storage.database.DBReader; -import de.danoeh.antennapod.ui.appstartintent.OnlineFeedviewActivityStarter; -import de.danoeh.antennapod.ui.discovery.OnlineSearchFragment; -import de.danoeh.antennapod.view.EmptyViewHandler; -import de.danoeh.antennapod.view.EpisodeItemListRecyclerView; -import de.danoeh.antennapod.view.LiftOnScrollListener; -import de.danoeh.antennapod.view.viewholder.EpisodeItemViewHolder; -import io.reactivex.Observable; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.Disposable; -import io.reactivex.schedulers.Schedulers; -import org.greenrobot.eventbus.EventBus; -import org.greenrobot.eventbus.Subscribe; -import org.greenrobot.eventbus.ThreadMode; - -import java.util.Collections; -import java.util.List; -import de.danoeh.antennapod.menuhandler.FeedMenuHandler; -import de.danoeh.antennapod.event.FeedListUpdateEvent; - - -/** - * Performs a search operation on all feeds or one specific feed and displays the search result. - */ -public class SearchFragment extends Fragment implements EpisodeItemListAdapter.OnSelectModeListener { - private static final String TAG = "SearchFragment"; - private static final String ARG_QUERY = "query"; - private static final String ARG_FEED = "feed"; - private static final String ARG_FEED_NAME = "feedName"; - private static final int SEARCH_DEBOUNCE_INTERVAL = 1500; - - private EpisodeItemListAdapter adapter; - private HorizontalFeedListAdapter adapterFeeds; - private Disposable disposable; - private ProgressBar progressBar; - private EmptyViewHandler emptyViewHandler; - private EpisodeItemListRecyclerView recyclerView; - private List results; - private Chip chip; - private SearchView searchView; - private Handler automaticSearchDebouncer; - private long lastQueryChange = 0; - private MultiSelectSpeedDialBinding speedDialBinding; - private boolean isOtherViewInFoucus = false; - - - /** - * Create a new SearchFragment that searches all feeds. - */ - public static SearchFragment newInstance() { - SearchFragment fragment = new SearchFragment(); - Bundle args = new Bundle(); - args.putLong(ARG_FEED, 0); - fragment.setArguments(args); - return fragment; - } - - /** - * Create a new SearchFragment that searches all feeds with pre-defined query. - */ - public static SearchFragment newInstance(String query) { - SearchFragment fragment = newInstance(); - fragment.getArguments().putString(ARG_QUERY, query); - return fragment; - } - - /** - * Create a new SearchFragment that searches one specific feed. - */ - public static SearchFragment newInstance(long feed, String feedTitle) { - SearchFragment fragment = newInstance(); - fragment.getArguments().putLong(ARG_FEED, feed); - fragment.getArguments().putString(ARG_FEED_NAME, feedTitle); - return fragment; - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - automaticSearchDebouncer = new Handler(Looper.getMainLooper()); - } - - @Override - public void onStop() { - super.onStop(); - if (disposable != null) { - disposable.dispose(); - } - } - - @Nullable - @Override - public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, - @Nullable Bundle savedInstanceState) { - View layout = inflater.inflate(R.layout.search_fragment, container, false); - setupToolbar(layout.findViewById(R.id.toolbar)); - speedDialBinding = MultiSelectSpeedDialBinding.bind(layout); - progressBar = layout.findViewById(R.id.progressBar); - recyclerView = layout.findViewById(R.id.recyclerView); - recyclerView.setRecycledViewPool(((MainActivity) getActivity()).getRecycledViewPool()); - registerForContextMenu(recyclerView); - adapter = new EpisodeItemListAdapter((MainActivity) getActivity()) { - @Override - public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { - super.onCreateContextMenu(menu, v, menuInfo); - if (!inActionMode()) { - menu.findItem(R.id.multi_select).setVisible(true); - } - MenuItemUtils.setOnClickListeners(menu, SearchFragment.this::onContextItemSelected); - } - }; - adapter.setOnSelectModeListener(this); - recyclerView.setAdapter(adapter); - recyclerView.addOnScrollListener(new LiftOnScrollListener(layout.findViewById(R.id.appbar))); - - RecyclerView recyclerViewFeeds = layout.findViewById(R.id.recyclerViewFeeds); - LinearLayoutManager layoutManagerFeeds = new LinearLayoutManager(getActivity()); - layoutManagerFeeds.setOrientation(RecyclerView.HORIZONTAL); - recyclerViewFeeds.setLayoutManager(layoutManagerFeeds); - adapterFeeds = new HorizontalFeedListAdapter((MainActivity) getActivity()) { - @Override - public void onCreateContextMenu(ContextMenu contextMenu, View view, - ContextMenu.ContextMenuInfo contextMenuInfo) { - super.onCreateContextMenu(contextMenu, view, contextMenuInfo); - MenuItemUtils.setOnClickListeners(contextMenu, SearchFragment.this::onContextItemSelected); - } - }; - recyclerViewFeeds.setAdapter(adapterFeeds); - - emptyViewHandler = new EmptyViewHandler(getContext()); - emptyViewHandler.attachToRecyclerView(recyclerView); - emptyViewHandler.setIcon(R.drawable.ic_search); - emptyViewHandler.setTitle(R.string.search_status_no_results); - emptyViewHandler.setMessage(R.string.type_to_search); - EventBus.getDefault().register(this); - - chip = layout.findViewById(R.id.feed_title_chip); - chip.setOnCloseIconClickListener(v -> { - getArguments().putLong(ARG_FEED, 0); - searchWithProgressBar(); - }); - chip.setVisibility((getArguments().getLong(ARG_FEED, 0) == 0) ? View.GONE : View.VISIBLE); - chip.setText(getArguments().getString(ARG_FEED_NAME, "")); - if (getArguments().getString(ARG_QUERY, null) != null) { - search(); - } - searchView.setOnQueryTextFocusChangeListener((view, hasFocus) -> { - if (hasFocus && !isOtherViewInFoucus) { - showInputMethod(view.findFocus()); - } - }); - recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { - @Override - public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) { - super.onScrollStateChanged(recyclerView, newState); - if (newState == RecyclerView.SCROLL_STATE_DRAGGING) { - InputMethodManager imm = (InputMethodManager) - getActivity().getSystemService(Context.INPUT_METHOD_SERVICE); - imm.hideSoftInputFromWindow(recyclerView.getWindowToken(), 0); - } - } - }); - speedDialBinding.fabSD.setOverlayLayout(speedDialBinding.fabSDOverlay); - speedDialBinding.fabSD.inflate(R.menu.episodes_apply_action_speeddial); - speedDialBinding.fabSD.setOnChangeListener(new SpeedDialView.OnChangeListener() { - @Override - public boolean onMainActionSelected() { - return false; - } - - @Override - public void onToggleChanged(boolean open) { - if (open && adapter.getSelectedCount() == 0) { - ((MainActivity) getActivity()) - .showSnackbarAbovePlayer(R.string.no_items_selected, Snackbar.LENGTH_SHORT); - speedDialBinding.fabSD.close(); - } - } - }); - speedDialBinding.fabSD.setOnActionSelectedListener(actionItem -> { - new EpisodeMultiSelectActionHandler((MainActivity) getActivity(), actionItem.getId()) - .handleAction(adapter.getSelectedItems()); - adapter.endSelectMode(); - return true; - }); - - return layout; - } - - @Override - public void onDestroyView() { - super.onDestroyView(); - EventBus.getDefault().unregister(this); - } - - private void setupToolbar(MaterialToolbar toolbar) { - toolbar.setTitle(R.string.search_label); - toolbar.setNavigationOnClickListener(v -> getParentFragmentManager().popBackStack()); - toolbar.inflateMenu(R.menu.search); - - MenuItem item = toolbar.getMenu().findItem(R.id.action_search); - item.expandActionView(); - searchView = (SearchView) item.getActionView(); - searchView.setQueryHint(getString(R.string.search_label)); - searchView.setQuery(getArguments().getString(ARG_QUERY), true); - searchView.requestFocus(); - searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { - @Override - public boolean onQueryTextSubmit(String s) { - searchView.clearFocus(); - searchWithProgressBar(); - return true; - } - - @Override - public boolean onQueryTextChange(String s) { - automaticSearchDebouncer.removeCallbacksAndMessages(null); - if (s.isEmpty() || s.endsWith(" ") || (lastQueryChange != 0 - && System.currentTimeMillis() > lastQueryChange + SEARCH_DEBOUNCE_INTERVAL)) { - search(); - } else { - automaticSearchDebouncer.postDelayed(() -> { - search(); - lastQueryChange = 0; // Don't search instantly with first symbol after some pause - }, SEARCH_DEBOUNCE_INTERVAL / 2); - } - lastQueryChange = System.currentTimeMillis(); - return false; - } - }); - item.setOnActionExpandListener(new MenuItem.OnActionExpandListener() { - @Override - public boolean onMenuItemActionExpand(MenuItem item) { - return true; - } - - @Override - public boolean onMenuItemActionCollapse(MenuItem item) { - getParentFragmentManager().popBackStack(); - return true; - } - }); - } - - @Override - public boolean onContextItemSelected(@NonNull MenuItem item) { - Feed selectedFeedItem = adapterFeeds.getLongPressedItem(); - if (selectedFeedItem != null - && FeedMenuHandler.onMenuItemClicked(this, item.getItemId(), selectedFeedItem, () -> { })) { - return true; - } - FeedItem selectedItem = adapter.getLongPressedItem(); - if (selectedItem != null) { - if (adapter.onContextItemSelected(item)) { - return true; - } - if (FeedItemMenuHandler.onMenuItemClicked(this, item.getItemId(), selectedItem)) { - return true; - } - } - return super.onContextItemSelected(item); - } - - @Subscribe(threadMode = ThreadMode.MAIN) - public void onFeedListChanged(FeedListUpdateEvent event) { - search(); - } - - @Subscribe(threadMode = ThreadMode.MAIN) - public void onUnreadItemsChanged(UnreadItemsUpdateEvent event) { - search(); - } - - @Subscribe(threadMode = ThreadMode.MAIN) - public void onEventMainThread(FeedItemEvent event) { - Log.d(TAG, "onEventMainThread() called with: " + "event = [" + event + "]"); - if (results == null) { - return; - } else if (adapter == null) { - search(); - return; - } - for (int i = 0, size = event.items.size(); i < size; i++) { - FeedItem item = event.items.get(i); - int pos = FeedItemUtil.indexOfItemWithId(results, item.getId()); - if (pos >= 0) { - results.remove(pos); - results.add(pos, item); - adapter.notifyItemChangedCompat(pos); - } - } - } - - @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) - public void onEventMainThread(EpisodeDownloadEvent event) { - if (results == null) { - return; - } - for (String downloadUrl : event.getUrls()) { - int pos = FeedItemUtil.indexOfItemWithDownloadUrl(results, downloadUrl); - if (pos >= 0) { - adapter.notifyItemChangedCompat(pos); - } - } - } - - @Subscribe(threadMode = ThreadMode.MAIN) - public void onEventMainThread(PlaybackPositionEvent event) { - if (adapter != null) { - for (int i = 0; i < adapter.getItemCount(); i++) { - EpisodeItemViewHolder holder = (EpisodeItemViewHolder) recyclerView.findViewHolderForAdapterPosition(i); - if (holder != null && holder.isCurrentlyPlayingItem()) { - holder.notifyPlaybackPositionUpdated(event); - break; - } - } - } - } - - @Subscribe(threadMode = ThreadMode.MAIN) - public void onPlayerStatusChanged(PlayerStatusEvent event) { - search(); - } - - private void searchWithProgressBar() { - progressBar.setVisibility(View.VISIBLE); - emptyViewHandler.hide(); - search(); - } - - private void search() { - if (disposable != null) { - disposable.dispose(); - } - adapterFeeds.setEndButton(R.string.search_online, this::searchOnline); - chip.setVisibility((getArguments().getLong(ARG_FEED, 0) == 0) ? View.GONE : View.VISIBLE); - disposable = Observable.fromCallable(this::performSearch) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(results -> { - progressBar.setVisibility(View.GONE); - this.results = results.first; - adapter.updateItems(results.first); - if (getArguments().getLong(ARG_FEED, 0) == 0) { - adapterFeeds.updateData(results.second); - } else { - adapterFeeds.updateData(Collections.emptyList()); - } - - if (searchView.getQuery().toString().isEmpty()) { - emptyViewHandler.setMessage(R.string.type_to_search); - } else { - emptyViewHandler.setMessage(getString(R.string.no_results_for_query, searchView.getQuery())); - } - }, error -> Log.e(TAG, Log.getStackTraceString(error))); - } - - @NonNull - private Pair, List> performSearch() { - String query = searchView.getQuery().toString(); - if (query.isEmpty()) { - return new Pair<>(Collections.emptyList(), Collections.emptyList()); - } - long feed = getArguments().getLong(ARG_FEED); - List items = DBReader.searchFeedItems(feed, query); - List feeds = DBReader.searchFeeds(query); - return new Pair<>(items, feeds); - } - - private void showInputMethod(View view) { - InputMethodManager imm = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE); - if (imm != null) { - imm.showSoftInput(view, 0); - } - } - - private void searchOnline() { - searchView.clearFocus(); - InputMethodManager in = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE); - in.hideSoftInputFromWindow(searchView.getWindowToken(), 0); - String query = searchView.getQuery().toString(); - if (query.matches("http[s]?://.*")) { - startActivity(new OnlineFeedviewActivityStarter(getContext(), query).getIntent()); - return; - } - ((MainActivity) getActivity()).loadChildFragment( - OnlineSearchFragment.newInstance(CombinedSearcher.class, query)); - } - - @Override - public void onStartSelectMode() { - searchViewFocusOff(); - speedDialBinding.fabSD.removeActionItemById(R.id.remove_from_inbox_batch); - speedDialBinding.fabSD.removeActionItemById(R.id.remove_from_queue_batch); - speedDialBinding.fabSD.removeActionItemById(R.id.delete_batch); - speedDialBinding.fabSD.setVisibility(View.VISIBLE); - } - - @Override - public void onEndSelectMode() { - speedDialBinding.fabSD.close(); - speedDialBinding.fabSD.setVisibility(View.GONE); - searchViewFocusOn(); - } - - private void searchViewFocusOff() { - isOtherViewInFoucus = true; - searchView.clearFocus(); - } - - private void searchViewFocusOn() { - isOtherViewInFoucus = false; - searchView.requestFocus(); - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/SubscriptionFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/SubscriptionFragment.java deleted file mode 100644 index dcc78f152..000000000 --- a/app/src/main/java/de/danoeh/antennapod/fragment/SubscriptionFragment.java +++ /dev/null @@ -1,367 +0,0 @@ -package de.danoeh.antennapod.fragment; - -import android.content.Context; -import android.content.SharedPreferences; -import android.os.Bundle; -import android.util.Log; -import android.view.ContextMenu; -import android.view.LayoutInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.widget.LinearLayout; -import android.widget.ProgressBar; - -import androidx.annotation.NonNull; -import androidx.fragment.app.Fragment; -import androidx.recyclerview.widget.GridLayoutManager; -import androidx.recyclerview.widget.RecyclerView; -import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; - -import com.google.android.material.appbar.MaterialToolbar; -import com.google.android.material.floatingactionbutton.FloatingActionButton; -import com.leinardi.android.speeddial.SpeedDialView; - -import de.danoeh.antennapod.net.download.serviceinterface.FeedUpdateManager; -import org.greenrobot.eventbus.EventBus; -import org.greenrobot.eventbus.Subscribe; -import org.greenrobot.eventbus.ThreadMode; - -import java.util.ArrayList; -import java.util.List; -import java.util.Locale; - -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.activity.MainActivity; -import de.danoeh.antennapod.adapter.SubscriptionsRecyclerAdapter; -import de.danoeh.antennapod.core.menuhandler.MenuItemUtils; -import de.danoeh.antennapod.storage.database.DBReader; -import de.danoeh.antennapod.storage.database.NavDrawerData; -import de.danoeh.antennapod.dialog.FeedSortDialog; -import de.danoeh.antennapod.dialog.RenameItemDialog; -import de.danoeh.antennapod.dialog.SubscriptionsFilterDialog; -import de.danoeh.antennapod.event.FeedListUpdateEvent; -import de.danoeh.antennapod.event.FeedUpdateRunningEvent; -import de.danoeh.antennapod.event.UnreadItemsUpdateEvent; -import de.danoeh.antennapod.fragment.actions.FeedMultiSelectActionHandler; -import de.danoeh.antennapod.menuhandler.FeedMenuHandler; -import de.danoeh.antennapod.model.feed.Feed; -import de.danoeh.antennapod.storage.preferences.UserPreferences; -import de.danoeh.antennapod.ui.statistics.StatisticsFragment; -import de.danoeh.antennapod.view.EmptyViewHandler; -import de.danoeh.antennapod.view.LiftOnScrollListener; -import io.reactivex.Observable; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.Disposable; -import io.reactivex.schedulers.Schedulers; - -/** - * Fragment for displaying feed subscriptions - */ -public class SubscriptionFragment extends Fragment - implements MaterialToolbar.OnMenuItemClickListener, - SubscriptionsRecyclerAdapter.OnSelectModeListener { - public static final String TAG = "SubscriptionFragment"; - private static final String PREFS = "SubscriptionFragment"; - private static final String PREF_NUM_COLUMNS = "columns"; - private static final String KEY_UP_ARROW = "up_arrow"; - private static final String ARGUMENT_FOLDER = "folder"; - - private static final int MIN_NUM_COLUMNS = 2; - private static final int[] COLUMN_CHECKBOX_IDS = { - R.id.subscription_num_columns_2, - R.id.subscription_num_columns_3, - R.id.subscription_num_columns_4, - R.id.subscription_num_columns_5}; - - private RecyclerView subscriptionRecycler; - private SubscriptionsRecyclerAdapter subscriptionAdapter; - private EmptyViewHandler emptyView; - private LinearLayout feedsFilteredMsg; - private MaterialToolbar toolbar; - private SwipeRefreshLayout swipeRefreshLayout; - private ProgressBar progressBar; - private String displayedFolder = null; - private boolean displayUpArrow; - - private Disposable disposable; - private SharedPreferences prefs; - - private FloatingActionButton subscriptionAddButton; - - private SpeedDialView speedDialView; - - private List listItems; - - public static SubscriptionFragment newInstance(String folderTitle) { - SubscriptionFragment fragment = new SubscriptionFragment(); - Bundle args = new Bundle(); - args.putString(ARGUMENT_FOLDER, folderTitle); - fragment.setArguments(args); - return fragment; - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - prefs = requireActivity().getSharedPreferences(PREFS, Context.MODE_PRIVATE); - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - View root = inflater.inflate(R.layout.fragment_subscriptions, container, false); - toolbar = root.findViewById(R.id.toolbar); - toolbar.setOnMenuItemClickListener(this); - toolbar.setOnLongClickListener(v -> { - subscriptionRecycler.scrollToPosition(5); - subscriptionRecycler.post(() -> subscriptionRecycler.smoothScrollToPosition(0)); - return false; - }); - displayUpArrow = getParentFragmentManager().getBackStackEntryCount() != 0; - if (savedInstanceState != null) { - displayUpArrow = savedInstanceState.getBoolean(KEY_UP_ARROW); - } - ((MainActivity) getActivity()).setupToolbarToggle(toolbar, displayUpArrow); - toolbar.inflateMenu(R.menu.subscriptions); - for (int i = 0; i < COLUMN_CHECKBOX_IDS.length; i++) { - // Do this in Java to localize numbers - toolbar.getMenu().findItem(COLUMN_CHECKBOX_IDS[i]) - .setTitle(String.format(Locale.getDefault(), "%d", i + MIN_NUM_COLUMNS)); - } - refreshToolbarState(); - - if (getArguments() != null) { - displayedFolder = getArguments().getString(ARGUMENT_FOLDER, null); - if (displayedFolder != null) { - toolbar.setTitle(displayedFolder); - } - } - - subscriptionRecycler = root.findViewById(R.id.subscriptions_grid); - subscriptionRecycler.addItemDecoration(new SubscriptionsRecyclerAdapter.GridDividerItemDecorator()); - registerForContextMenu(subscriptionRecycler); - subscriptionRecycler.addOnScrollListener(new LiftOnScrollListener(root.findViewById(R.id.appbar))); - subscriptionAdapter = new SubscriptionsRecyclerAdapter((MainActivity) getActivity()) { - @Override - public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { - super.onCreateContextMenu(menu, v, menuInfo); - MenuItemUtils.setOnClickListeners(menu, SubscriptionFragment.this::onContextItemSelected); - } - }; - setColumnNumber(prefs.getInt(PREF_NUM_COLUMNS, getDefaultNumOfColumns())); - subscriptionAdapter.setOnSelectModeListener(this); - subscriptionRecycler.setAdapter(subscriptionAdapter); - setupEmptyView(); - - progressBar = root.findViewById(R.id.progressBar); - progressBar.setVisibility(View.VISIBLE); - - subscriptionAddButton = root.findViewById(R.id.subscriptions_add); - subscriptionAddButton.setOnClickListener(view -> { - if (getActivity() instanceof MainActivity) { - ((MainActivity) getActivity()).loadChildFragment(new AddFeedFragment()); - } - }); - - feedsFilteredMsg = root.findViewById(R.id.feeds_filtered_message); - feedsFilteredMsg.setOnClickListener((l) -> - new SubscriptionsFilterDialog().show(getChildFragmentManager(), "filter")); - - swipeRefreshLayout = root.findViewById(R.id.swipeRefresh); - swipeRefreshLayout.setDistanceToTriggerSync(getResources().getInteger(R.integer.swipe_refresh_distance)); - swipeRefreshLayout.setOnRefreshListener(() -> FeedUpdateManager.getInstance().runOnceOrAsk(requireContext())); - - speedDialView = root.findViewById(R.id.fabSD); - speedDialView.setOverlayLayout(root.findViewById(R.id.fabSDOverlay)); - speedDialView.inflate(R.menu.nav_feed_action_speeddial); - speedDialView.setOnActionSelectedListener(actionItem -> { - new FeedMultiSelectActionHandler((MainActivity) getActivity(), subscriptionAdapter.getSelectedItems()) - .handleAction(actionItem.getId()); - return true; - }); - - return root; - } - - @Override - public void onSaveInstanceState(@NonNull Bundle outState) { - outState.putBoolean(KEY_UP_ARROW, displayUpArrow); - super.onSaveInstanceState(outState); - } - - private void refreshToolbarState() { - int columns = prefs.getInt(PREF_NUM_COLUMNS, getDefaultNumOfColumns()); - toolbar.getMenu().findItem(COLUMN_CHECKBOX_IDS[columns - MIN_NUM_COLUMNS]).setChecked(true); - } - - @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) - public void onEventMainThread(FeedUpdateRunningEvent event) { - swipeRefreshLayout.setRefreshing(event.isFeedUpdateRunning); - } - - @Override - public boolean onMenuItemClick(MenuItem item) { - final int itemId = item.getItemId(); - if (itemId == R.id.refresh_item) { - FeedUpdateManager.getInstance().runOnceOrAsk(requireContext()); - return true; - } else if (itemId == R.id.subscriptions_filter) { - new SubscriptionsFilterDialog().show(getChildFragmentManager(), "filter"); - return true; - } else if (itemId == R.id.subscriptions_sort) { - FeedSortDialog.showDialog(requireContext()); - return true; - } else if (itemId == R.id.subscription_num_columns_2) { - setColumnNumber(2); - return true; - } else if (itemId == R.id.subscription_num_columns_3) { - setColumnNumber(3); - return true; - } else if (itemId == R.id.subscription_num_columns_4) { - setColumnNumber(4); - return true; - } else if (itemId == R.id.subscription_num_columns_5) { - setColumnNumber(5); - return true; - } else if (itemId == R.id.action_search) { - ((MainActivity) getActivity()).loadChildFragment(SearchFragment.newInstance()); - return true; - } else if (itemId == R.id.action_statistics) { - ((MainActivity) getActivity()).loadChildFragment(new StatisticsFragment()); - return true; - } - return false; - } - - private void setColumnNumber(int columns) { - GridLayoutManager gridLayoutManager = new GridLayoutManager(getContext(), - columns, RecyclerView.VERTICAL, false); - subscriptionAdapter.setColumnCount(columns); - subscriptionRecycler.setLayoutManager(gridLayoutManager); - prefs.edit().putInt(PREF_NUM_COLUMNS, columns).apply(); - refreshToolbarState(); - } - - private void setupEmptyView() { - emptyView = new EmptyViewHandler(getContext()); - emptyView.setIcon(R.drawable.ic_subscriptions); - emptyView.setTitle(R.string.no_subscriptions_head_label); - emptyView.setMessage(R.string.no_subscriptions_label); - emptyView.attachToRecyclerView(subscriptionRecycler); - } - - @Override - public void onStart() { - super.onStart(); - EventBus.getDefault().register(this); - loadSubscriptions(); - } - - @Override - public void onStop() { - super.onStop(); - EventBus.getDefault().unregister(this); - if (disposable != null) { - disposable.dispose(); - } - if (subscriptionAdapter != null) { - subscriptionAdapter.endSelectMode(); - } - } - - private void loadSubscriptions() { - if (disposable != null) { - disposable.dispose(); - } - emptyView.hide(); - disposable = Observable.fromCallable( - () -> { - NavDrawerData data = DBReader.getNavDrawerData(UserPreferences.getSubscriptionsFilter(), - UserPreferences.getFeedOrder(), UserPreferences.getFeedCounterSetting()); - List items = data.items; - for (NavDrawerData.DrawerItem item : items) { - if (item.type == NavDrawerData.DrawerItem.Type.TAG - && item.getTitle().equals(displayedFolder)) { - return ((NavDrawerData.TagDrawerItem) item).children; - } - } - return items; - }) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - result -> { - if (listItems != null && listItems.size() > result.size()) { - // We have fewer items. This can result in items being selected that are no longer visible. - subscriptionAdapter.endSelectMode(); - } - listItems = result; - progressBar.setVisibility(View.GONE); - subscriptionAdapter.setItems(result); - emptyView.updateVisibility(); - }, error -> { - Log.e(TAG, Log.getStackTraceString(error)); - }); - - if (UserPreferences.getSubscriptionsFilter().isEnabled()) { - feedsFilteredMsg.setVisibility(View.VISIBLE); - } else { - feedsFilteredMsg.setVisibility(View.GONE); - } - } - - private int getDefaultNumOfColumns() { - return getResources().getInteger(R.integer.subscriptions_default_num_of_columns); - } - - @Override - public boolean onContextItemSelected(MenuItem item) { - NavDrawerData.DrawerItem drawerItem = subscriptionAdapter.getSelectedItem(); - if (drawerItem == null) { - return false; - } - int itemId = item.getItemId(); - if (drawerItem.type == NavDrawerData.DrawerItem.Type.TAG && itemId == R.id.rename_folder_item) { - new RenameItemDialog(getActivity(), drawerItem).show(); - return true; - } - - Feed feed = ((NavDrawerData.FeedDrawerItem) drawerItem).feed; - if (itemId == R.id.multi_select) { - return subscriptionAdapter.onContextItemSelected(item); - } - return FeedMenuHandler.onMenuItemClicked(this, item.getItemId(), feed, this::loadSubscriptions); - } - - @Subscribe(threadMode = ThreadMode.MAIN) - public void onFeedListChanged(FeedListUpdateEvent event) { - loadSubscriptions(); - } - - @Subscribe(threadMode = ThreadMode.MAIN) - public void onUnreadItemsChanged(UnreadItemsUpdateEvent event) { - loadSubscriptions(); - } - - @Override - public void onEndSelectMode() { - speedDialView.close(); - speedDialView.setVisibility(View.GONE); - subscriptionAddButton.setVisibility(View.VISIBLE); - subscriptionAdapter.setItems(listItems); - } - - @Override - public void onStartSelectMode() { - List feedsOnly = new ArrayList<>(); - for (NavDrawerData.DrawerItem item : listItems) { - if (item.type == NavDrawerData.DrawerItem.Type.FEED) { - feedsOnly.add(item); - } - } - subscriptionAdapter.setItems(feedsOnly); - speedDialView.setVisibility(View.VISIBLE); - subscriptionAddButton.setVisibility(View.GONE); - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/TransitionEffect.java b/app/src/main/java/de/danoeh/antennapod/fragment/TransitionEffect.java deleted file mode 100644 index e3ec14890..000000000 --- a/app/src/main/java/de/danoeh/antennapod/fragment/TransitionEffect.java +++ /dev/null @@ -1,5 +0,0 @@ -package de.danoeh.antennapod.fragment; - -public enum TransitionEffect { - NONE, FADE, SLIDE -} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/actions/EpisodeMultiSelectActionHandler.java b/app/src/main/java/de/danoeh/antennapod/fragment/actions/EpisodeMultiSelectActionHandler.java deleted file mode 100644 index 9325037ad..000000000 --- a/app/src/main/java/de/danoeh/antennapod/fragment/actions/EpisodeMultiSelectActionHandler.java +++ /dev/null @@ -1,133 +0,0 @@ -package de.danoeh.antennapod.fragment.actions; - -import android.util.Log; - -import androidx.annotation.PluralsRes; - -import com.google.android.material.snackbar.Snackbar; - -import java.util.List; - -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.activity.MainActivity; -import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterface; -import de.danoeh.antennapod.storage.database.DBWriter; -import de.danoeh.antennapod.storage.database.LongList; -import de.danoeh.antennapod.model.feed.FeedItem; -import de.danoeh.antennapod.view.LocalDeleteModal; - -public class EpisodeMultiSelectActionHandler { - private static final String TAG = "EpisodeSelectHandler"; - private final MainActivity activity; - private final int actionId; - private int totalNumItems = 0; - private Snackbar snackbar = null; - - public EpisodeMultiSelectActionHandler(MainActivity activity, int actionId) { - this.activity = activity; - this.actionId = actionId; - } - - public void handleAction(List items) { - if (actionId == R.id.add_to_queue_batch) { - queueChecked(items); - } else if (actionId == R.id.remove_from_queue_batch) { - removeFromQueueChecked(items); - } else if (actionId == R.id.remove_from_inbox_batch) { - removeFromInboxChecked(items); - } else if (actionId == R.id.mark_read_batch) { - markedCheckedPlayed(items); - } else if (actionId == R.id.mark_unread_batch) { - markedCheckedUnplayed(items); - } else if (actionId == R.id.download_batch) { - downloadChecked(items); - } else if (actionId == R.id.delete_batch) { - LocalDeleteModal.showLocalFeedDeleteWarningIfNecessary(activity, items, () -> deleteChecked(items)); - } else { - Log.e(TAG, "Unrecognized speed dial action item. Do nothing. id=" + actionId); - } - } - - private void queueChecked(List items) { - // Check if an episode actually contains any media files before adding it to queue - LongList toQueue = new LongList(items.size()); - for (FeedItem episode : items) { - if (episode.hasMedia()) { - toQueue.add(episode.getId()); - } - } - DBWriter.addQueueItem(activity, true, toQueue.toArray()); - showMessage(R.plurals.added_to_queue_batch_label, toQueue.size()); - } - - private void removeFromQueueChecked(List items) { - long[] checkedIds = getSelectedIds(items); - DBWriter.removeQueueItem(activity, true, checkedIds); - showMessage(R.plurals.removed_from_queue_batch_label, checkedIds.length); - } - - private void removeFromInboxChecked(List items) { - LongList markUnplayed = new LongList(); - for (FeedItem episode : items) { - if (episode.isNew()) { - markUnplayed.add(episode.getId()); - } - } - DBWriter.markItemPlayed(FeedItem.UNPLAYED, markUnplayed.toArray()); - showMessage(R.plurals.removed_from_inbox_batch_label, markUnplayed.size()); - } - - private void markedCheckedPlayed(List items) { - long[] checkedIds = getSelectedIds(items); - DBWriter.markItemPlayed(FeedItem.PLAYED, checkedIds); - showMessage(R.plurals.marked_read_batch_label, checkedIds.length); - } - - private void markedCheckedUnplayed(List items) { - long[] checkedIds = getSelectedIds(items); - DBWriter.markItemPlayed(FeedItem.UNPLAYED, checkedIds); - showMessage(R.plurals.marked_unread_batch_label, checkedIds.length); - } - - private void downloadChecked(List items) { - // download the check episodes in the same order as they are currently displayed - for (FeedItem episode : items) { - if (episode.hasMedia() && !episode.getFeed().isLocalFeed()) { - DownloadServiceInterface.get().download(activity, episode); - } - } - showMessage(R.plurals.downloading_batch_label, items.size()); - } - - private void deleteChecked(List items) { - int countHasMedia = 0; - for (FeedItem feedItem : items) { - if (feedItem.hasMedia() && feedItem.getMedia().isDownloaded()) { - countHasMedia++; - DBWriter.deleteFeedMediaOfItem(activity, feedItem.getMedia()); - } - } - showMessage(R.plurals.deleted_multi_episode_batch_label, countHasMedia); - } - - private void showMessage(@PluralsRes int msgId, int numItems) { - totalNumItems += numItems; - activity.runOnUiThread(() -> { - String text = activity.getResources().getQuantityString(msgId, totalNumItems, totalNumItems); - if (snackbar != null) { - snackbar.setText(text); - snackbar.show(); // Resets the timeout - } else { - snackbar = activity.showSnackbarAbovePlayer(text, Snackbar.LENGTH_LONG); - } - }); - } - - private long[] getSelectedIds(List items) { - long[] checkedIds = new long[items.size()]; - for (int i = 0; i < items.size(); ++i) { - checkedIds[i] = items.get(i).getId(); - } - return checkedIds; - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/actions/FeedMultiSelectActionHandler.java b/app/src/main/java/de/danoeh/antennapod/fragment/actions/FeedMultiSelectActionHandler.java deleted file mode 100644 index 5a6b4ffa9..000000000 --- a/app/src/main/java/de/danoeh/antennapod/fragment/actions/FeedMultiSelectActionHandler.java +++ /dev/null @@ -1,138 +0,0 @@ -package de.danoeh.antennapod.fragment.actions; - -import android.util.Log; - -import androidx.annotation.PluralsRes; -import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import androidx.core.util.Consumer; - -import com.google.android.material.snackbar.Snackbar; - -import java.util.ArrayList; -import java.util.List; -import java.util.Locale; - -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.activity.MainActivity; -import de.danoeh.antennapod.storage.database.DBWriter; -import de.danoeh.antennapod.databinding.PlaybackSpeedFeedSettingDialogBinding; -import de.danoeh.antennapod.dialog.RemoveFeedDialog; -import de.danoeh.antennapod.dialog.TagSettingsDialog; -import de.danoeh.antennapod.model.feed.Feed; -import de.danoeh.antennapod.model.feed.FeedPreferences; -import de.danoeh.antennapod.fragment.preferences.dialog.PreferenceListDialog; -import de.danoeh.antennapod.fragment.preferences.dialog.PreferenceSwitchDialog; - -public class FeedMultiSelectActionHandler { - private static final String TAG = "FeedSelectHandler"; - private final MainActivity activity; - private final List selectedItems; - - public FeedMultiSelectActionHandler(MainActivity activity, List selectedItems) { - this.activity = activity; - this.selectedItems = selectedItems; - } - - public void handleAction(int id) { - if (id == R.id.remove_feed) { - RemoveFeedDialog.show(activity, selectedItems); - } else if (id == R.id.notify_new_episodes) { - notifyNewEpisodesPrefHandler(); - } else if (id == R.id.keep_updated) { - keepUpdatedPrefHandler(); - } else if (id == R.id.autodownload) { - autoDownloadPrefHandler(); - } else if (id == R.id.autoDeleteDownload) { - autoDeleteEpisodesPrefHandler(); - } else if (id == R.id.playback_speed) { - playbackSpeedPrefHandler(); - } else if (id == R.id.edit_tags) { - editFeedPrefTags(); - } else { - Log.e(TAG, "Unrecognized speed dial action item. Do nothing. id=" + id); - } - } - - private void notifyNewEpisodesPrefHandler() { - PreferenceSwitchDialog preferenceSwitchDialog = new PreferenceSwitchDialog(activity, - activity.getString(R.string.episode_notification), - activity.getString(R.string.episode_notification_summary)); - preferenceSwitchDialog.setOnPreferenceChangedListener(enabled -> - saveFeedPreferences(feedPreferences -> feedPreferences.setShowEpisodeNotification(enabled))); - preferenceSwitchDialog.openDialog(); - } - - private void autoDownloadPrefHandler() { - PreferenceSwitchDialog preferenceSwitchDialog = new PreferenceSwitchDialog(activity, - activity.getString(R.string.auto_download_settings_label), - activity.getString(R.string.auto_download_label)); - preferenceSwitchDialog.setOnPreferenceChangedListener(enabled -> - saveFeedPreferences(feedPreferences -> feedPreferences.setAutoDownload(enabled))); - preferenceSwitchDialog.openDialog(); - } - - private void playbackSpeedPrefHandler() { - PlaybackSpeedFeedSettingDialogBinding viewBinding = - PlaybackSpeedFeedSettingDialogBinding.inflate(activity.getLayoutInflater()); - viewBinding.seekBar.setProgressChangedListener(speed -> - viewBinding.currentSpeedLabel.setText(String.format(Locale.getDefault(), "%.2fx", speed))); - viewBinding.useGlobalCheckbox.setOnCheckedChangeListener((buttonView, isChecked) -> { - viewBinding.seekBar.setEnabled(!isChecked); - viewBinding.seekBar.setAlpha(isChecked ? 0.4f : 1f); - viewBinding.currentSpeedLabel.setAlpha(isChecked ? 0.4f : 1f); - }); - viewBinding.seekBar.updateSpeed(1.0f); - new MaterialAlertDialogBuilder(activity) - .setTitle(R.string.playback_speed) - .setView(viewBinding.getRoot()) - .setPositiveButton(android.R.string.ok, (dialog, which) -> { - float newSpeed = viewBinding.useGlobalCheckbox.isChecked() - ? FeedPreferences.SPEED_USE_GLOBAL : viewBinding.seekBar.getCurrentSpeed(); - saveFeedPreferences(feedPreferences -> feedPreferences.setFeedPlaybackSpeed(newSpeed)); - }) - .setNegativeButton(R.string.cancel_label, null) - .show(); - } - - private void autoDeleteEpisodesPrefHandler() { - PreferenceListDialog preferenceListDialog = new PreferenceListDialog(activity, - activity.getString(R.string.auto_delete_label)); - String[] items = activity.getResources().getStringArray(R.array.spnAutoDeleteItems); - preferenceListDialog.openDialog(items); - preferenceListDialog.setOnPreferenceChangedListener(which -> { - FeedPreferences.AutoDeleteAction autoDeleteAction = FeedPreferences.AutoDeleteAction.fromCode(which); - saveFeedPreferences(feedPreferences -> feedPreferences.setAutoDeleteAction(autoDeleteAction)); - }); - } - - private void keepUpdatedPrefHandler() { - PreferenceSwitchDialog preferenceSwitchDialog = new PreferenceSwitchDialog(activity, - activity.getString(R.string.kept_updated), - activity.getString(R.string.keep_updated_summary)); - preferenceSwitchDialog.setOnPreferenceChangedListener(keepUpdated -> - saveFeedPreferences(feedPreferences -> feedPreferences.setKeepUpdated(keepUpdated))); - preferenceSwitchDialog.openDialog(); - } - - private void showMessage(@PluralsRes int msgId, int numItems) { - activity.showSnackbarAbovePlayer(activity.getResources() - .getQuantityString(msgId, numItems, numItems), Snackbar.LENGTH_LONG); - } - - private void saveFeedPreferences(Consumer preferencesConsumer) { - for (Feed feed : selectedItems) { - preferencesConsumer.accept(feed.getPreferences()); - DBWriter.setFeedPreferences(feed.getPreferences()); - } - showMessage(R.plurals.updated_feeds_batch_label, selectedItems.size()); - } - - private void editFeedPrefTags() { - ArrayList preferencesList = new ArrayList<>(); - for (Feed feed : selectedItems) { - preferencesList.add(feed.getPreferences()); - } - TagSettingsDialog.newInstance(preferencesList).show(activity.getSupportFragmentManager(), - TagSettingsDialog.TAG); - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/DownloadsPreferencesFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/DownloadsPreferencesFragment.java deleted file mode 100644 index 58e968155..000000000 --- a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/DownloadsPreferencesFragment.java +++ /dev/null @@ -1,108 +0,0 @@ -package de.danoeh.antennapod.fragment.preferences; - -import android.content.SharedPreferences; -import android.os.Bundle; - -import androidx.preference.PreferenceFragmentCompat; -import androidx.preference.PreferenceManager; -import androidx.preference.TwoStatePreference; - -import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.activity.PreferenceActivity; -import de.danoeh.antennapod.net.download.serviceinterface.FeedUpdateManager; -import de.danoeh.antennapod.ui.preferences.screen.downloads.ChooseDataFolderDialog; -import de.danoeh.antennapod.dialog.ProxyDialog; -import de.danoeh.antennapod.storage.preferences.UserPreferences; - -import java.io.File; - - -public class DownloadsPreferencesFragment extends PreferenceFragmentCompat - implements SharedPreferences.OnSharedPreferenceChangeListener { - private static final String PREF_SCREEN_AUTODL = "prefAutoDownloadSettings"; - private static final String PREF_AUTO_DELETE_LOCAL = "prefAutoDeleteLocal"; - private static final String PREF_PROXY = "prefProxy"; - private static final String PREF_CHOOSE_DATA_DIR = "prefChooseDataDir"; - - private boolean blockAutoDeleteLocal = true; - - @Override - public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { - addPreferencesFromResource(R.xml.preferences_downloads); - setupNetworkScreen(); - } - - @Override - public void onStart() { - super.onStart(); - ((PreferenceActivity) getActivity()).getSupportActionBar().setTitle(R.string.downloads_pref); - PreferenceManager.getDefaultSharedPreferences(getContext()).registerOnSharedPreferenceChangeListener(this); - } - - @Override - public void onStop() { - super.onStop(); - PreferenceManager.getDefaultSharedPreferences(getContext()).unregisterOnSharedPreferenceChangeListener(this); - } - - @Override - public void onResume() { - super.onResume(); - setDataFolderText(); - } - - private void setupNetworkScreen() { - findPreference(PREF_SCREEN_AUTODL).setOnPreferenceClickListener(preference -> { - ((PreferenceActivity) getActivity()).openScreen(R.xml.preferences_autodownload); - return true; - }); - // validate and set correct value: number of downloads between 1 and 50 (inclusive) - findPreference(PREF_PROXY).setOnPreferenceClickListener(preference -> { - ProxyDialog dialog = new ProxyDialog(getActivity()); - dialog.show(); - return true; - }); - findPreference(PREF_CHOOSE_DATA_DIR).setOnPreferenceClickListener(preference -> { - ChooseDataFolderDialog.showDialog(getContext(), path -> { - UserPreferences.setDataFolder(path); - setDataFolderText(); - }); - return true; - }); - findPreference(PREF_AUTO_DELETE_LOCAL).setOnPreferenceChangeListener((preference, newValue) -> { - if (blockAutoDeleteLocal && newValue == Boolean.TRUE) { - showAutoDeleteEnableDialog(); - return false; - } else { - return true; - } - }); - } - - private void setDataFolderText() { - File f = UserPreferences.getDataFolder(null); - if (f != null) { - findPreference(PREF_CHOOSE_DATA_DIR).setSummary(f.getAbsolutePath()); - } - } - - @Override - public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { - if (UserPreferences.PREF_UPDATE_INTERVAL.equals(key)) { - FeedUpdateManager.getInstance().restartUpdateAlarm(getContext(), true); - } - } - - private void showAutoDeleteEnableDialog() { - new MaterialAlertDialogBuilder(requireContext()) - .setMessage(R.string.pref_auto_local_delete_dialog_body) - .setPositiveButton(R.string.yes, (dialog, which) -> { - blockAutoDeleteLocal = false; - ((TwoStatePreference) findPreference(PREF_AUTO_DELETE_LOCAL)).setChecked(true); - blockAutoDeleteLocal = true; - }) - .setNegativeButton(R.string.cancel_label, null) - .show(); - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/ImportExportPreferencesFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/ImportExportPreferencesFragment.java deleted file mode 100644 index 7c607c242..000000000 --- a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/ImportExportPreferencesFragment.java +++ /dev/null @@ -1,413 +0,0 @@ -package de.danoeh.antennapod.fragment.preferences; - -import android.app.Activity; -import android.app.ProgressDialog; -import android.content.ActivityNotFoundException; -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.net.Uri; -import android.os.Bundle; -import android.util.Log; - -import androidx.activity.result.ActivityResult; -import androidx.activity.result.ActivityResultLauncher; -import androidx.activity.result.contract.ActivityResultContracts; -import androidx.activity.result.contract.ActivityResultContracts.GetContent; -import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.StringRes; -import androidx.documentfile.provider.DocumentFile; -import androidx.preference.SwitchPreferenceCompat; -import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import androidx.core.app.ShareCompat; -import androidx.core.content.FileProvider; -import androidx.preference.PreferenceFragmentCompat; -import com.google.android.material.snackbar.Snackbar; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.activity.OpmlImportActivity; -import de.danoeh.antennapod.activity.PreferenceActivity; -import de.danoeh.antennapod.storage.database.DBReader; -import de.danoeh.antennapod.model.feed.FeedItem; -import de.danoeh.antennapod.model.feed.FeedItemFilter; -import de.danoeh.antennapod.model.feed.SortOrder; -import de.danoeh.antennapod.storage.importexport.AutomaticDatabaseExportWorker; -import de.danoeh.antennapod.storage.importexport.DatabaseExporter; -import de.danoeh.antennapod.storage.importexport.FavoritesWriter; -import de.danoeh.antennapod.storage.importexport.HtmlWriter; -import de.danoeh.antennapod.storage.importexport.OpmlWriter; -import de.danoeh.antennapod.storage.preferences.UserPreferences; -import io.reactivex.Completable; -import io.reactivex.Observable; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.Disposable; -import io.reactivex.schedulers.Schedulers; - -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.io.OutputStreamWriter; -import java.nio.charset.Charset; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.List; -import java.util.Locale; - -public class ImportExportPreferencesFragment extends PreferenceFragmentCompat { - private static final String TAG = "ImportExPrefFragment"; - private static final String PREF_OPML_EXPORT = "prefOpmlExport"; - private static final String PREF_OPML_IMPORT = "prefOpmlImport"; - private static final String PREF_HTML_EXPORT = "prefHtmlExport"; - private static final String PREF_DATABASE_IMPORT = "prefDatabaseImport"; - private static final String PREF_DATABASE_EXPORT = "prefDatabaseExport"; - private static final String PREF_AUTOMATIC_DATABASE_EXPORT = "prefAutomaticDatabaseExport"; - private static final String PREF_FAVORITE_EXPORT = "prefFavoritesExport"; - private static final String DEFAULT_OPML_OUTPUT_NAME = "antennapod-feeds-%s.opml"; - private static final String CONTENT_TYPE_OPML = "text/x-opml"; - private static final String DEFAULT_HTML_OUTPUT_NAME = "antennapod-feeds-%s.html"; - private static final String CONTENT_TYPE_HTML = "text/html"; - private static final String DEFAULT_FAVORITES_OUTPUT_NAME = "antennapod-favorites-%s.html"; - private static final String DATABASE_EXPORT_FILENAME = "AntennaPodBackup-%s.db"; - - private final ActivityResultLauncher chooseOpmlExportPathLauncher = - registerForActivityResult(new StartActivityForResult(), - result -> exportToDocument(result, Export.OPML)); - private final ActivityResultLauncher chooseHtmlExportPathLauncher = - registerForActivityResult(new StartActivityForResult(), - result -> exportToDocument(result, Export.HTML)); - private final ActivityResultLauncher chooseFavoritesExportPathLauncher = - registerForActivityResult(new StartActivityForResult(), - result -> exportToDocument(result, Export.FAVORITES)); - private final ActivityResultLauncher restoreDatabaseLauncher = - registerForActivityResult(new StartActivityForResult(), this::restoreDatabaseResult); - private final ActivityResultLauncher backupDatabaseLauncher = - registerForActivityResult(new BackupDatabase(), this::backupDatabaseResult); - private final ActivityResultLauncher chooseOpmlImportPathLauncher = - registerForActivityResult(new GetContent(), uri -> { - if (uri != null) { - final Intent intent = new Intent(getContext(), OpmlImportActivity.class); - intent.setData(uri); - startActivity(intent); - } - }); - private final ActivityResultLauncher automaticBackupLauncher = - registerForActivityResult(new PickWritableFolder(), this::setupAutomaticBackup); - - private Disposable disposable; - private ProgressDialog progressDialog; - - @Override - public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { - addPreferencesFromResource(R.xml.preferences_import_export); - setupStorageScreen(); - progressDialog = new ProgressDialog(getContext()); - progressDialog.setIndeterminate(true); - progressDialog.setMessage(getContext().getString(R.string.please_wait)); - } - - @Override - public void onStart() { - super.onStart(); - ((PreferenceActivity) getActivity()).getSupportActionBar().setTitle(R.string.import_export_pref); - } - - @Override - public void onStop() { - super.onStop(); - if (disposable != null) { - disposable.dispose(); - } - } - - private void setupStorageScreen() { - findPreference(PREF_OPML_EXPORT).setOnPreferenceClickListener( - preference -> { - openExportPathPicker(Export.OPML, chooseOpmlExportPathLauncher); - return true; - } - ); - findPreference(PREF_HTML_EXPORT).setOnPreferenceClickListener( - preference -> { - openExportPathPicker(Export.HTML, chooseHtmlExportPathLauncher); - return true; - }); - findPreference(PREF_OPML_IMPORT).setOnPreferenceClickListener( - preference -> { - try { - chooseOpmlImportPathLauncher.launch("*/*"); - } catch (ActivityNotFoundException e) { - Log.e(TAG, "No activity found. Should never happen..."); - } - return true; - }); - findPreference(PREF_DATABASE_IMPORT).setOnPreferenceClickListener( - preference -> { - importDatabase(); - return true; - }); - findPreference(PREF_DATABASE_EXPORT).setOnPreferenceClickListener( - preference -> { - backupDatabaseLauncher.launch(dateStampFilename(DATABASE_EXPORT_FILENAME)); - return true; - }); - ((SwitchPreferenceCompat) findPreference(PREF_AUTOMATIC_DATABASE_EXPORT)) - .setChecked(UserPreferences.getAutomaticExportFolder() != null); - findPreference(PREF_AUTOMATIC_DATABASE_EXPORT).setOnPreferenceChangeListener( - (preference, newValue) -> { - if (Boolean.TRUE.equals(newValue)) { - try { - automaticBackupLauncher.launch(null); - } catch (ActivityNotFoundException e) { - e.printStackTrace(); - Snackbar.make(getView(), R.string.unable_to_start_system_file_manager, Snackbar.LENGTH_LONG) - .show(); - } - return false; - } else { - UserPreferences.setAutomaticExportFolder(null); - AutomaticDatabaseExportWorker.enqueueIfNeeded(getContext(), false); - } - return true; - }); - findPreference(PREF_FAVORITE_EXPORT).setOnPreferenceClickListener( - preference -> { - openExportPathPicker(Export.FAVORITES, chooseFavoritesExportPathLauncher); - return true; - }); - } - - private String dateStampFilename(String fname) { - return String.format(fname, new SimpleDateFormat("yyyy-MM-dd", Locale.US).format(new Date())); - } - - private void importDatabase() { - // setup the alert builder - MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(getActivity()); - builder.setTitle(R.string.database_import_label); - builder.setMessage(R.string.database_import_warning); - - // add a button - builder.setNegativeButton(R.string.no, null); - builder.setPositiveButton(R.string.confirm_label, (dialog, which) -> { - Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); - intent.setType("*/*"); - restoreDatabaseLauncher.launch(intent); - }); - - // create and show the alert dialog - builder.show(); - } - - private void showDatabaseImportSuccessDialog() { - MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(getContext()); - builder.setTitle(R.string.successful_import_label); - builder.setMessage(R.string.import_ok); - builder.setCancelable(false); - builder.setPositiveButton(android.R.string.ok, (dialogInterface, i) -> forceRestart()); - builder.show(); - } - - void showExportSuccessSnackbar(Uri uri, String mimeType) { - Snackbar.make(getView(), R.string.export_success_title, Snackbar.LENGTH_LONG) - .setAction(R.string.share_label, v -> - new ShareCompat.IntentBuilder(getContext()) - .setType(mimeType) - .addStream(uri) - .setChooserTitle(R.string.share_label) - .startChooser()) - .show(); - } - - private void showExportErrorDialog(final Throwable error) { - progressDialog.dismiss(); - final MaterialAlertDialogBuilder alert = new MaterialAlertDialogBuilder(getContext()); - alert.setPositiveButton(android.R.string.ok, (dialog, which) -> dialog.dismiss()); - alert.setTitle(R.string.export_error_label); - alert.setMessage(error.getMessage()); - alert.show(); - } - - private void restoreDatabaseResult(final ActivityResult result) { - if (result.getResultCode() != Activity.RESULT_OK || result.getData() == null) { - return; - } - final Uri uri = result.getData().getData(); - progressDialog.show(); - disposable = Completable.fromAction(() -> DatabaseExporter.importBackup(uri, getContext())) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(() -> { - showDatabaseImportSuccessDialog(); - progressDialog.dismiss(); - }, this::showExportErrorDialog); - } - - private void backupDatabaseResult(final Uri uri) { - if (uri == null) { - return; - } - progressDialog.show(); - disposable = Completable.fromAction(() -> DatabaseExporter.exportToDocument(uri, getContext())) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(() -> { - showExportSuccessSnackbar(uri, "application/x-sqlite3"); - progressDialog.dismiss(); - }, this::showExportErrorDialog); - } - - private void openExportPathPicker(Export exportType, ActivityResultLauncher result) { - String title = dateStampFilename(exportType.outputNameTemplate); - - Intent intentPickAction = new Intent(Intent.ACTION_CREATE_DOCUMENT) - .addCategory(Intent.CATEGORY_OPENABLE) - .setType(exportType.contentType) - .putExtra(Intent.EXTRA_TITLE, title); - - // Creates an implicit intent to launch a file manager which lets - // the user choose a specific directory to export to. - try { - result.launch(intentPickAction); - return; - } catch (ActivityNotFoundException e) { - Log.e(TAG, "No activity found. Should never happen..."); - } - - // If we are using a SDK lower than API 21 or the implicit intent failed - // fallback to the legacy export process - File output = new File(UserPreferences.getDataFolder("export/"), title); - exportToFile(exportType, output); - } - - private void exportToFile(Export exportType, File output) { - progressDialog.show(); - disposable = Observable.create( - subscriber -> { - if (output.exists()) { - boolean success = output.delete(); - Log.w(TAG, "Overwriting previously exported file: " + success); - } - try (FileOutputStream fileOutputStream = new FileOutputStream(output)) { - writeToStream(fileOutputStream, exportType); - subscriber.onNext(output); - } catch (IOException e) { - subscriber.onError(e); - } - }) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(outputFile -> { - progressDialog.dismiss(); - Uri fileUri = FileProvider.getUriForFile(getActivity().getApplicationContext(), - getString(R.string.provider_authority), output); - showExportSuccessSnackbar(fileUri, exportType.contentType); - }, this::showExportErrorDialog, progressDialog::dismiss); - } - - private void exportToDocument(final ActivityResult result, Export exportType) { - if (result.getResultCode() != Activity.RESULT_OK || result.getData() == null) { - return; - } - progressDialog.show(); - DocumentFile output = DocumentFile.fromSingleUri(getContext(), result.getData().getData()); - disposable = Observable.create( - subscriber -> { - try (OutputStream outputStream = getContext().getContentResolver() - .openOutputStream(output.getUri(), "wt")) { - writeToStream(outputStream, exportType); - subscriber.onNext(output); - } catch (IOException e) { - subscriber.onError(e); - } - }) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(ignore -> { - progressDialog.dismiss(); - showExportSuccessSnackbar(output.getUri(), exportType.contentType); - }, this::showExportErrorDialog, progressDialog::dismiss); - } - - private void writeToStream(OutputStream outputStream, Export type) throws IOException { - try (OutputStreamWriter writer = new OutputStreamWriter(outputStream, Charset.forName("UTF-8"))) { - switch (type) { - case HTML: - HtmlWriter.writeDocument(DBReader.getFeedList(), writer, getContext()); - break; - case OPML: - OpmlWriter.writeDocument(DBReader.getFeedList(), writer); - break; - case FAVORITES: - List allFavorites = DBReader.getEpisodes(0, Integer.MAX_VALUE, - new FeedItemFilter(FeedItemFilter.IS_FAVORITE), SortOrder.DATE_NEW_OLD); - FavoritesWriter.writeDocument(allFavorites, writer, getContext()); - break; - default: - showExportErrorDialog(new Exception("Invalid export type")); - break; - } - } - } - - private void setupAutomaticBackup(Uri uri) { - if (uri == null) { - return; - } - getActivity().getContentResolver().takePersistableUriPermission(uri, - Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); - UserPreferences.setAutomaticExportFolder(uri.toString()); - AutomaticDatabaseExportWorker.enqueueIfNeeded(getContext(), true); - ((SwitchPreferenceCompat) findPreference(PREF_AUTOMATIC_DATABASE_EXPORT)).setChecked(true); - } - - private void forceRestart() { - PackageManager pm = getContext().getPackageManager(); - Intent intent = pm.getLaunchIntentForPackage(getContext().getPackageName()); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - getContext().getApplicationContext().startActivity(intent); - Runtime.getRuntime().exit(0); - } - - private static class BackupDatabase extends ActivityResultContracts.CreateDocument { - - BackupDatabase() { - super("application/x-sqlite3"); - } - - @NonNull - @Override - public Intent createIntent(@NonNull final Context context, @NonNull final String input) { - return super.createIntent(context, input) - .addCategory(Intent.CATEGORY_OPENABLE) - .setType("application/x-sqlite3"); - } - } - - private static class PickWritableFolder extends ActivityResultContracts.OpenDocumentTree { - @NonNull - @Override - public Intent createIntent(@NonNull final Context context, @Nullable final Uri input) { - return super.createIntent(context, input) - .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); - } - } - - private enum Export { - OPML(CONTENT_TYPE_OPML, DEFAULT_OPML_OUTPUT_NAME, R.string.opml_export_label), - HTML(CONTENT_TYPE_HTML, DEFAULT_HTML_OUTPUT_NAME, R.string.html_export_label), - FAVORITES(CONTENT_TYPE_HTML, DEFAULT_FAVORITES_OUTPUT_NAME, R.string.favorites_export_label); - - final String contentType; - final String outputNameTemplate; - @StringRes - final int labelResId; - - Export(String contentType, String outputNameTemplate, int labelResId) { - this.contentType = contentType; - this.outputNameTemplate = outputNameTemplate; - this.labelResId = labelResId; - } - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/MainPreferencesFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/MainPreferencesFragment.java deleted file mode 100644 index 50d201f39..000000000 --- a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/MainPreferencesFragment.java +++ /dev/null @@ -1,157 +0,0 @@ -package de.danoeh.antennapod.fragment.preferences; - -import android.content.Intent; -import android.graphics.PorterDuff; -import android.graphics.PorterDuffColorFilter; -import android.os.Bundle; - -import androidx.appcompat.app.AppCompatActivity; -import androidx.preference.Preference; -import androidx.preference.PreferenceFragmentCompat; - -import com.bytehamster.lib.preferencesearch.SearchConfiguration; -import com.bytehamster.lib.preferencesearch.SearchPreference; - -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.activity.BugReportActivity; -import de.danoeh.antennapod.activity.PreferenceActivity; -import de.danoeh.antennapod.core.util.IntentUtils; -import de.danoeh.antennapod.ui.preferences.screen.about.AboutFragment; - -public class MainPreferencesFragment extends PreferenceFragmentCompat { - - private static final String PREF_SCREEN_USER_INTERFACE = "prefScreenInterface"; - private static final String PREF_SCREEN_PLAYBACK = "prefScreenPlayback"; - private static final String PREF_SCREEN_DOWNLOADS = "prefScreenDownloads"; - private static final String PREF_SCREEN_IMPORT_EXPORT = "prefScreenImportExport"; - private static final String PREF_SCREEN_SYNCHRONIZATION = "prefScreenSynchronization"; - private static final String PREF_DOCUMENTATION = "prefDocumentation"; - private static final String PREF_VIEW_FORUM = "prefViewForum"; - private static final String PREF_SEND_BUG_REPORT = "prefSendBugReport"; - private static final String PREF_CATEGORY_PROJECT = "project"; - private static final String PREF_ABOUT = "prefAbout"; - private static final String PREF_NOTIFICATION = "notifications"; - private static final String PREF_CONTRIBUTE = "prefContribute"; - - @Override - public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { - addPreferencesFromResource(R.xml.preferences); - setupMainScreen(); - setupSearch(); - - // If you are writing a spin-off, please update the details on screens like "About" and "Report bug" - // and afterwards remove the following lines. Please keep in mind that AntennaPod is licensed under the GPL. - // This means that your application needs to be open-source under the GPL, too. - // It must also include a prominent copyright notice. - int packageHash = getContext().getPackageName().hashCode(); - if (packageHash != 1790437538 && packageHash != -1190467065) { - findPreference(PREF_CATEGORY_PROJECT).setVisible(false); - Preference copyrightNotice = new Preference(getContext()); - copyrightNotice.setIcon(R.drawable.ic_info_white); - copyrightNotice.getIcon().mutate() - .setColorFilter(new PorterDuffColorFilter(0xffcc0000, PorterDuff.Mode.MULTIPLY)); - copyrightNotice.setSummary("This application is based on AntennaPod." - + " The AntennaPod team does NOT provide support for this unofficial version." - + " If you can read this message, the developers of this modification" - + " violate the GNU General Public License (GPL)."); - findPreference(PREF_CATEGORY_PROJECT).getParent().addPreference(copyrightNotice); - } else if (packageHash == -1190467065) { - Preference debugNotice = new Preference(getContext()); - debugNotice.setIcon(R.drawable.ic_info_white); - debugNotice.getIcon().mutate() - .setColorFilter(new PorterDuffColorFilter(0xffcc0000, PorterDuff.Mode.MULTIPLY)); - debugNotice.setOrder(-1); - debugNotice.setSummary("This is a development version of AntennaPod and not meant for daily use"); - findPreference(PREF_CATEGORY_PROJECT).getParent().addPreference(debugNotice); - } - } - - @Override - public void onStart() { - super.onStart(); - ((PreferenceActivity) getActivity()).getSupportActionBar().setTitle(R.string.settings_label); - } - - private void setupMainScreen() { - findPreference(PREF_SCREEN_USER_INTERFACE).setOnPreferenceClickListener(preference -> { - ((PreferenceActivity) getActivity()).openScreen(R.xml.preferences_user_interface); - return true; - }); - findPreference(PREF_SCREEN_PLAYBACK).setOnPreferenceClickListener(preference -> { - ((PreferenceActivity) getActivity()).openScreen(R.xml.preferences_playback); - return true; - }); - findPreference(PREF_SCREEN_DOWNLOADS).setOnPreferenceClickListener(preference -> { - ((PreferenceActivity) getActivity()).openScreen(R.xml.preferences_downloads); - return true; - }); - findPreference(PREF_SCREEN_SYNCHRONIZATION).setOnPreferenceClickListener(preference -> { - ((PreferenceActivity) getActivity()).openScreen(R.xml.preferences_synchronization); - return true; - }); - findPreference(PREF_SCREEN_IMPORT_EXPORT).setOnPreferenceClickListener(preference -> { - ((PreferenceActivity) getActivity()).openScreen(R.xml.preferences_import_export); - return true; - }); - findPreference(PREF_NOTIFICATION).setOnPreferenceClickListener(preference -> { - ((PreferenceActivity) getActivity()).openScreen(R.xml.preferences_notifications); - return true; - }); - findPreference(PREF_ABOUT).setOnPreferenceClickListener( - preference -> { - getParentFragmentManager().beginTransaction() - .replace(R.id.settingsContainer, new AboutFragment()) - .addToBackStack(getString(R.string.about_pref)).commit(); - return true; - } - ); - findPreference(PREF_DOCUMENTATION).setOnPreferenceClickListener(preference -> { - IntentUtils.openInBrowser(getContext(), - IntentUtils.getLocalizedWebsiteLink(getContext()) + "/documentation/"); - return true; - }); - findPreference(PREF_VIEW_FORUM).setOnPreferenceClickListener(preference -> { - IntentUtils.openInBrowser(getContext(), "https://forum.antennapod.org/"); - return true; - }); - findPreference(PREF_CONTRIBUTE).setOnPreferenceClickListener(preference -> { - IntentUtils.openInBrowser(getContext(), - IntentUtils.getLocalizedWebsiteLink(getContext()) + "/contribute/"); - return true; - }); - findPreference(PREF_SEND_BUG_REPORT).setOnPreferenceClickListener(preference -> { - startActivity(new Intent(getActivity(), BugReportActivity.class)); - return true; - }); - } - - private void setupSearch() { - SearchPreference searchPreference = findPreference("searchPreference"); - SearchConfiguration config = searchPreference.getSearchConfiguration(); - config.setActivity((AppCompatActivity) getActivity()); - config.setFragmentContainerViewId(R.id.settingsContainer); - config.setBreadcrumbsEnabled(true); - - config.index(R.xml.preferences_user_interface) - .addBreadcrumb(PreferenceActivity.getTitleOfPage(R.xml.preferences_user_interface)); - config.index(R.xml.preferences_playback) - .addBreadcrumb(PreferenceActivity.getTitleOfPage(R.xml.preferences_playback)); - config.index(R.xml.preferences_downloads) - .addBreadcrumb(PreferenceActivity.getTitleOfPage(R.xml.preferences_downloads)); - config.index(R.xml.preferences_import_export) - .addBreadcrumb(PreferenceActivity.getTitleOfPage(R.xml.preferences_import_export)); - config.index(R.xml.preferences_autodownload) - .addBreadcrumb(PreferenceActivity.getTitleOfPage(R.xml.preferences_downloads)) - .addBreadcrumb(R.string.automation) - .addBreadcrumb(PreferenceActivity.getTitleOfPage(R.xml.preferences_autodownload)); - config.index(R.xml.preferences_synchronization) - .addBreadcrumb(PreferenceActivity.getTitleOfPage(R.xml.preferences_synchronization)); - config.index(R.xml.preferences_notifications) - .addBreadcrumb(PreferenceActivity.getTitleOfPage(R.xml.preferences_notifications)); - config.index(R.xml.feed_settings) - .addBreadcrumb(PreferenceActivity.getTitleOfPage(R.xml.feed_settings)); - config.index(R.xml.preferences_swipe) - .addBreadcrumb(PreferenceActivity.getTitleOfPage(R.xml.preferences_user_interface)) - .addBreadcrumb(PreferenceActivity.getTitleOfPage(R.xml.preferences_swipe)); - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/PlaybackPreferencesFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/PlaybackPreferencesFragment.java deleted file mode 100644 index dcbd96d5b..000000000 --- a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/PlaybackPreferencesFragment.java +++ /dev/null @@ -1,128 +0,0 @@ -package de.danoeh.antennapod.fragment.preferences; - -import android.app.Activity; -import android.content.res.Resources; -import android.os.Build; -import android.os.Bundle; -import androidx.annotation.NonNull; -import androidx.collection.ArrayMap; -import androidx.preference.ListPreference; -import androidx.preference.Preference; -import androidx.preference.PreferenceFragmentCompat; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.activity.PreferenceActivity; -import de.danoeh.antennapod.event.UnreadItemsUpdateEvent; -import de.danoeh.antennapod.storage.preferences.UsageStatistics; -import de.danoeh.antennapod.storage.preferences.UserPreferences; -import de.danoeh.antennapod.dialog.SkipPreferenceDialog; -import de.danoeh.antennapod.dialog.VariableSpeedDialog; -import java.util.Map; -import org.greenrobot.eventbus.EventBus; - -public class PlaybackPreferencesFragment extends PreferenceFragmentCompat { - private static final String PREF_PLAYBACK_SPEED_LAUNCHER = "prefPlaybackSpeedLauncher"; - private static final String PREF_PLAYBACK_REWIND_DELTA_LAUNCHER = "prefPlaybackRewindDeltaLauncher"; - private static final String PREF_PLAYBACK_FAST_FORWARD_DELTA_LAUNCHER = "prefPlaybackFastForwardDeltaLauncher"; - private static final String PREF_PLAYBACK_PREFER_STREAMING = "prefStreamOverDownload"; - - @Override - public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { - addPreferencesFromResource(R.xml.preferences_playback); - - setupPlaybackScreen(); - buildSmartMarkAsPlayedPreference(); - } - - @Override - public void onStart() { - super.onStart(); - ((PreferenceActivity) getActivity()).getSupportActionBar().setTitle(R.string.playback_pref); - } - - private void setupPlaybackScreen() { - final Activity activity = getActivity(); - - findPreference(PREF_PLAYBACK_SPEED_LAUNCHER).setOnPreferenceClickListener(preference -> { - new VariableSpeedDialog().show(getChildFragmentManager(), null); - return true; - }); - findPreference(PREF_PLAYBACK_REWIND_DELTA_LAUNCHER).setOnPreferenceClickListener(preference -> { - SkipPreferenceDialog.showSkipPreference(activity, SkipPreferenceDialog.SkipDirection.SKIP_REWIND, null); - return true; - }); - findPreference(PREF_PLAYBACK_FAST_FORWARD_DELTA_LAUNCHER).setOnPreferenceClickListener(preference -> { - SkipPreferenceDialog.showSkipPreference(activity, SkipPreferenceDialog.SkipDirection.SKIP_FORWARD, null); - return true; - }); - findPreference(PREF_PLAYBACK_PREFER_STREAMING).setOnPreferenceChangeListener((preference, newValue) -> { - // Update all visible lists to reflect new streaming action button - EventBus.getDefault().post(new UnreadItemsUpdateEvent()); - // User consciously decided whether to prefer the streaming button, disable suggestion to change that - UsageStatistics.doNotAskAgain(UsageStatistics.ACTION_STREAM); - return true; - }); - if (Build.VERSION.SDK_INT >= 31) { - findPreference(UserPreferences.PREF_UNPAUSE_ON_HEADSET_RECONNECT).setVisible(false); - findPreference(UserPreferences.PREF_UNPAUSE_ON_BLUETOOTH_RECONNECT).setVisible(false); - } - - buildEnqueueLocationPreference(); - } - - private void buildEnqueueLocationPreference() { - final Resources res = requireActivity().getResources(); - final Map options = new ArrayMap<>(); - { - String[] keys = res.getStringArray(R.array.enqueue_location_values); - String[] values = res.getStringArray(R.array.enqueue_location_options); - for (int i = 0; i < keys.length; i++) { - options.put(keys[i], values[i]); - } - } - - ListPreference pref = requirePreference(UserPreferences.PREF_ENQUEUE_LOCATION); - pref.setSummary(res.getString(R.string.pref_enqueue_location_sum, options.get(pref.getValue()))); - - pref.setOnPreferenceChangeListener((preference, newValue) -> { - if (!(newValue instanceof String)) { - return false; - } - String newValStr = (String)newValue; - pref.setSummary(res.getString(R.string.pref_enqueue_location_sum, options.get(newValStr))); - return true; - }); - } - - @NonNull - private T requirePreference(@NonNull CharSequence key) { - // Possibly put it to a common method in abstract base class - T result = findPreference(key); - if (result == null) { - throw new IllegalArgumentException("Preference with key '" + key + "' is not found"); - - } - return result; - } - - private void buildSmartMarkAsPlayedPreference() { - final Resources res = getActivity().getResources(); - - ListPreference pref = findPreference(UserPreferences.PREF_SMART_MARK_AS_PLAYED_SECS); - String[] values = res.getStringArray(R.array.smart_mark_as_played_values); - String[] entries = new String[values.length]; - for (int x = 0; x < values.length; x++) { - if(x == 0) { - entries[x] = res.getString(R.string.pref_smart_mark_as_played_disabled); - } else { - int v = Integer.parseInt(values[x]); - if(v < 60) { - entries[x] = res.getQuantityString(R.plurals.time_seconds_quantified, v, v); - } else { - v /= 60; - entries[x] = res.getQuantityString(R.plurals.time_minutes_quantified, v, v); - } - } - } - pref.setEntries(entries); - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/SwipePreferencesFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/SwipePreferencesFragment.java deleted file mode 100644 index 5b81ff7a5..000000000 --- a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/SwipePreferencesFragment.java +++ /dev/null @@ -1,59 +0,0 @@ -package de.danoeh.antennapod.fragment.preferences; - -import android.os.Bundle; -import androidx.preference.PreferenceFragmentCompat; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.activity.PreferenceActivity; -import de.danoeh.antennapod.dialog.SwipeActionsDialog; -import de.danoeh.antennapod.fragment.AllEpisodesFragment; -import de.danoeh.antennapod.fragment.CompletedDownloadsFragment; -import de.danoeh.antennapod.fragment.FeedItemlistFragment; -import de.danoeh.antennapod.fragment.InboxFragment; -import de.danoeh.antennapod.fragment.PlaybackHistoryFragment; -import de.danoeh.antennapod.fragment.QueueFragment; - -public class SwipePreferencesFragment extends PreferenceFragmentCompat { - private static final String PREF_SWIPE_QUEUE = "prefSwipeQueue"; - private static final String PREF_SWIPE_INBOX = "prefSwipeInbox"; - private static final String PREF_SWIPE_EPISODES = "prefSwipeEpisodes"; - private static final String PREF_SWIPE_DOWNLOADS = "prefSwipeDownloads"; - private static final String PREF_SWIPE_FEED = "prefSwipeFeed"; - private static final String PREF_SWIPE_HISTORY = "prefSwipeHistory"; - - @Override - public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { - addPreferencesFromResource(R.xml.preferences_swipe); - - findPreference(PREF_SWIPE_QUEUE).setOnPreferenceClickListener(preference -> { - new SwipeActionsDialog(requireContext(), QueueFragment.TAG).show(() -> { }); - return true; - }); - findPreference(PREF_SWIPE_INBOX).setOnPreferenceClickListener(preference -> { - new SwipeActionsDialog(requireContext(), InboxFragment.TAG).show(() -> { }); - return true; - }); - findPreference(PREF_SWIPE_EPISODES).setOnPreferenceClickListener(preference -> { - new SwipeActionsDialog(requireContext(), AllEpisodesFragment.TAG).show(() -> { }); - return true; - }); - findPreference(PREF_SWIPE_DOWNLOADS).setOnPreferenceClickListener(preference -> { - new SwipeActionsDialog(requireContext(), CompletedDownloadsFragment.TAG).show(() -> { }); - return true; - }); - findPreference(PREF_SWIPE_FEED).setOnPreferenceClickListener(preference -> { - new SwipeActionsDialog(requireContext(), FeedItemlistFragment.TAG).show(() -> { }); - return true; - }); - findPreference(PREF_SWIPE_HISTORY).setOnPreferenceClickListener(preference -> { - new SwipeActionsDialog(requireContext(), PlaybackHistoryFragment.TAG).show(() -> { }); - return true; - }); - } - - @Override - public void onStart() { - super.onStart(); - ((PreferenceActivity) getActivity()).getSupportActionBar().setTitle(R.string.swipeactions_label); - } - -} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/UserInterfacePreferencesFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/UserInterfacePreferencesFragment.java deleted file mode 100644 index af48c705d..000000000 --- a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/UserInterfacePreferencesFragment.java +++ /dev/null @@ -1,171 +0,0 @@ -package de.danoeh.antennapod.fragment.preferences; - -import android.content.Context; -import android.content.DialogInterface; -import android.os.Build; -import android.os.Bundle; -import android.widget.Button; -import android.widget.ListView; - -import androidx.appcompat.app.AlertDialog; -import androidx.core.app.ActivityCompat; -import androidx.preference.Preference; -import androidx.preference.PreferenceFragmentCompat; - -import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import com.google.android.material.snackbar.Snackbar; - -import org.greenrobot.eventbus.EventBus; - -import java.util.List; - -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.activity.PreferenceActivity; -import de.danoeh.antennapod.dialog.DrawerPreferencesDialog; -import de.danoeh.antennapod.dialog.FeedSortDialog; -import de.danoeh.antennapod.dialog.SubscriptionsFilterDialog; -import de.danoeh.antennapod.event.PlayerStatusEvent; -import de.danoeh.antennapod.event.UnreadItemsUpdateEvent; -import de.danoeh.antennapod.storage.preferences.UserPreferences; - -public class UserInterfacePreferencesFragment extends PreferenceFragmentCompat { - private static final String PREF_SWIPE = "prefSwipe"; - - @Override - public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { - addPreferencesFromResource(R.xml.preferences_user_interface); - setupInterfaceScreen(); - } - - @Override - public void onStart() { - super.onStart(); - ((PreferenceActivity) getActivity()).getSupportActionBar().setTitle(R.string.user_interface_label); - } - - private void setupInterfaceScreen() { - Preference.OnPreferenceChangeListener restartApp = (preference, newValue) -> { - ActivityCompat.recreate(getActivity()); - return true; - }; - findPreference(UserPreferences.PREF_THEME).setOnPreferenceChangeListener(restartApp); - findPreference(UserPreferences.PREF_THEME_BLACK).setOnPreferenceChangeListener(restartApp); - findPreference(UserPreferences.PREF_TINTED_COLORS).setOnPreferenceChangeListener(restartApp); - if (Build.VERSION.SDK_INT < 31) { - findPreference(UserPreferences.PREF_TINTED_COLORS).setVisible(false); - } - - findPreference(UserPreferences.PREF_SHOW_TIME_LEFT) - .setOnPreferenceChangeListener( - (preference, newValue) -> { - UserPreferences.setShowRemainTimeSetting((Boolean) newValue); - EventBus.getDefault().post(new UnreadItemsUpdateEvent()); - EventBus.getDefault().post(new PlayerStatusEvent()); - return true; - }); - - findPreference(UserPreferences.PREF_HIDDEN_DRAWER_ITEMS) - .setOnPreferenceClickListener(preference -> { - DrawerPreferencesDialog.show(getContext(), null); - return true; - }); - - findPreference(UserPreferences.PREF_FULL_NOTIFICATION_BUTTONS) - .setOnPreferenceClickListener(preference -> { - showFullNotificationButtonsDialog(); - return true; - }); - findPreference(UserPreferences.PREF_FILTER_FEED) - .setOnPreferenceClickListener((preference -> { - new SubscriptionsFilterDialog().show(getChildFragmentManager(), "filter"); - return true; - })); - - findPreference(UserPreferences.PREF_DRAWER_FEED_ORDER) - .setOnPreferenceClickListener((preference -> { - FeedSortDialog.showDialog(requireContext()); - return true; - })); - findPreference(PREF_SWIPE) - .setOnPreferenceClickListener(preference -> { - ((PreferenceActivity) getActivity()).openScreen(R.xml.preferences_swipe); - return true; - }); - - if (Build.VERSION.SDK_INT >= 26) { - findPreference(UserPreferences.PREF_EXPANDED_NOTIFICATION).setVisible(false); - } - } - - private void showFullNotificationButtonsDialog() { - final Context context = getActivity(); - - final List preferredButtons = UserPreferences.getFullNotificationButtons(); - final String[] allButtonNames = context.getResources().getStringArray( - R.array.full_notification_buttons_options); - final int[] buttonIds = { - UserPreferences.NOTIFICATION_BUTTON_SKIP, - UserPreferences.NOTIFICATION_BUTTON_NEXT_CHAPTER, - UserPreferences.NOTIFICATION_BUTTON_PLAYBACK_SPEED, - UserPreferences.NOTIFICATION_BUTTON_SLEEP_TIMER, - }; - final DialogInterface.OnClickListener completeListener = (dialog, which) -> - UserPreferences.setFullNotificationButtons(preferredButtons); - final String title = context.getResources().getString(R.string.pref_full_notification_buttons_title); - - boolean[] checked = new boolean[allButtonNames.length]; // booleans default to false in java - - // Clear buttons that are not part of the setting anymore - for (int i = preferredButtons.size() - 1; i >= 0; i--) { - boolean isValid = false; - for (int j = 0; j < checked.length; j++) { - if (buttonIds[j] == preferredButtons.get(i)) { - isValid = true; - break; - } - } - - if (!isValid) { - preferredButtons.remove(i); - } - } - - for (int i = 0; i < checked.length; i++) { - if (preferredButtons.contains(buttonIds[i])) { - checked[i] = true; - } - } - - MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(context); - builder.setTitle(title); - builder.setMultiChoiceItems(allButtonNames, checked, (dialog, which, isChecked) -> { - checked[which] = isChecked; - if (isChecked) { - preferredButtons.add(buttonIds[which]); - } else { - preferredButtons.remove((Integer) buttonIds[which]); - } - }); - builder.setPositiveButton(R.string.confirm_label, null); - builder.setNegativeButton(R.string.cancel_label, null); - final AlertDialog dialog = builder.create(); - - dialog.show(); - - Button positiveButton = dialog.getButton(AlertDialog.BUTTON_POSITIVE); - - positiveButton.setOnClickListener(v -> { - if (preferredButtons.size() != 2) { - ListView selectionView = dialog.getListView(); - Snackbar.make( - selectionView, - context.getResources().getString(R.string.pref_compact_notification_buttons_dialog_error_exact), - Snackbar.LENGTH_SHORT).show(); - - } else { - completeListener.onClick(dialog, AlertDialog.BUTTON_POSITIVE); - dialog.cancel(); - } - }); - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/dialog/PreferenceListDialog.java b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/dialog/PreferenceListDialog.java deleted file mode 100644 index b6a11c001..000000000 --- a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/dialog/PreferenceListDialog.java +++ /dev/null @@ -1,49 +0,0 @@ -package de.danoeh.antennapod.fragment.preferences.dialog; - -import android.content.Context; - -import com.google.android.material.dialog.MaterialAlertDialogBuilder; - -import de.danoeh.antennapod.R; - -public class PreferenceListDialog { - protected Context context; - private String title; - private OnPreferenceChangedListener onPreferenceChangedListener; - private int selectedPos = 0; - - public PreferenceListDialog(Context context, String title) { - this.context = context; - this.title = title; - } - - public interface OnPreferenceChangedListener { - /** - * Notified when user confirms preference - * - * @param pos The index of the item that was selected - */ - - void preferenceChanged(int pos); - } - - public void openDialog(String[] items) { - - MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(context); - builder.setTitle(title); - builder.setSingleChoiceItems(items, selectedPos, (dialog, which) -> { - selectedPos = which; - }); - builder.setPositiveButton(R.string.confirm_label, (dialog, which) -> { - if (onPreferenceChangedListener != null && selectedPos >= 0) { - onPreferenceChangedListener.preferenceChanged(selectedPos); - } - }); - builder.setNegativeButton(R.string.cancel_label, null); - builder.create().show(); - } - - public void setOnPreferenceChangedListener(OnPreferenceChangedListener onPreferenceChangedListener) { - this.onPreferenceChangedListener = onPreferenceChangedListener; - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/dialog/PreferenceSwitchDialog.java b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/dialog/PreferenceSwitchDialog.java deleted file mode 100644 index 10fbe6137..000000000 --- a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/dialog/PreferenceSwitchDialog.java +++ /dev/null @@ -1,57 +0,0 @@ -package de.danoeh.antennapod.fragment.preferences.dialog; - -import android.content.Context; -import android.view.LayoutInflater; -import android.view.View; - -import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import com.google.android.material.materialswitch.MaterialSwitch; - -import de.danoeh.antennapod.R; - -public class PreferenceSwitchDialog { - protected Context context; - private String title; - private String text; - private OnPreferenceChangedListener onPreferenceChangedListener; - - public PreferenceSwitchDialog(Context context, String title, String text) { - this.context = context; - this.title = title; - this.text = text; - } - - public interface OnPreferenceChangedListener { - /** - * Notified when user confirms preference - * - * @param enabled The preference - */ - - void preferenceChanged(boolean enabled); - } - - public void openDialog() { - - MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(context); - builder.setTitle(title); - - LayoutInflater inflater = LayoutInflater.from(this.context); - View layout = inflater.inflate(R.layout.dialog_switch_preference, null, false); - MaterialSwitch switchButton = layout.findViewById(R.id.dialogSwitch); - switchButton.setText(text); - builder.setView(layout); - - builder.setPositiveButton(R.string.confirm_label, (dialog, which) -> { - if (onPreferenceChangedListener != null) { - onPreferenceChangedListener.preferenceChanged(switchButton.isChecked()); - } - }); - builder.setNegativeButton(R.string.cancel_label, null); - builder.create().show(); - } - - public void setOnPreferenceChangedListener(OnPreferenceChangedListener onPreferenceChangedListener) { - this.onPreferenceChangedListener = onPreferenceChangedListener; - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/AddToQueueSwipeAction.java b/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/AddToQueueSwipeAction.java deleted file mode 100644 index 06efda3ee..000000000 --- a/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/AddToQueueSwipeAction.java +++ /dev/null @@ -1,47 +0,0 @@ -package de.danoeh.antennapod.fragment.swipeactions; - -import android.content.Context; - -import androidx.fragment.app.Fragment; - -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.storage.database.DBWriter; -import de.danoeh.antennapod.model.feed.FeedItem; -import de.danoeh.antennapod.model.feed.FeedItemFilter; - -public class AddToQueueSwipeAction implements SwipeAction { - - @Override - public String getId() { - return ADD_TO_QUEUE; - } - - @Override - public int getActionIcon() { - return R.drawable.ic_playlist_play; - } - - @Override - public int getActionColor() { - return R.attr.colorAccent; - } - - @Override - public String getTitle(Context context) { - return context.getString(R.string.add_to_queue_label); - } - - @Override - public void performAction(FeedItem item, Fragment fragment, FeedItemFilter filter) { - if (!item.isTagged(FeedItem.TAG_QUEUE)) { - DBWriter.addQueueItem(fragment.requireContext(), item); - } else { - new RemoveFromQueueSwipeAction().performAction(item, fragment, filter); - } - } - - @Override - public boolean willRemove(FeedItemFilter filter, FeedItem item) { - return filter.showQueued || filter.showNew; - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/DeleteSwipeAction.java b/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/DeleteSwipeAction.java deleted file mode 100644 index 52f214eed..000000000 --- a/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/DeleteSwipeAction.java +++ /dev/null @@ -1,50 +0,0 @@ -package de.danoeh.antennapod.fragment.swipeactions; - -import android.content.Context; -import androidx.fragment.app.Fragment; - -import java.util.Collections; - -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.storage.database.DBWriter; -import de.danoeh.antennapod.model.feed.FeedItem; -import de.danoeh.antennapod.model.feed.FeedItemFilter; -import de.danoeh.antennapod.view.LocalDeleteModal; - -public class DeleteSwipeAction implements SwipeAction { - - @Override - public String getId() { - return DELETE; - } - - @Override - public int getActionIcon() { - return R.drawable.ic_delete; - } - - @Override - public int getActionColor() { - return R.attr.icon_red; - } - - @Override - public String getTitle(Context context) { - return context.getString(R.string.delete_episode_label); - } - - @Override - public void performAction(FeedItem item, Fragment fragment, FeedItemFilter filter) { - if (!item.isDownloaded() && !item.getFeed().isLocalFeed()) { - return; - } - LocalDeleteModal.showLocalFeedDeleteWarningIfNecessary( - fragment.requireContext(), Collections.singletonList(item), - () -> DBWriter.deleteFeedMediaOfItem(fragment.requireContext(), item.getMedia())); - } - - @Override - public boolean willRemove(FeedItemFilter filter, FeedItem item) { - return filter.showDownloaded && (item.isDownloaded() || item.getFeed().isLocalFeed()); - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/MarkFavoriteSwipeAction.java b/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/MarkFavoriteSwipeAction.java deleted file mode 100644 index dcea8c031..000000000 --- a/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/MarkFavoriteSwipeAction.java +++ /dev/null @@ -1,43 +0,0 @@ -package de.danoeh.antennapod.fragment.swipeactions; - -import android.content.Context; - -import androidx.fragment.app.Fragment; - -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.storage.database.DBWriter; -import de.danoeh.antennapod.model.feed.FeedItem; -import de.danoeh.antennapod.model.feed.FeedItemFilter; - -public class MarkFavoriteSwipeAction implements SwipeAction { - - @Override - public String getId() { - return MARK_FAV; - } - - @Override - public int getActionIcon() { - return R.drawable.ic_star; - } - - @Override - public int getActionColor() { - return R.attr.icon_yellow; - } - - @Override - public String getTitle(Context context) { - return context.getString(R.string.add_to_favorite_label); - } - - @Override - public void performAction(FeedItem item, Fragment fragment, FeedItemFilter filter) { - DBWriter.toggleFavoriteItem(item); - } - - @Override - public boolean willRemove(FeedItemFilter filter, FeedItem item) { - return filter.showIsFavorite || filter.showNotFavorite; - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/RemoveFromHistorySwipeAction.java b/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/RemoveFromHistorySwipeAction.java deleted file mode 100644 index 46285734e..000000000 --- a/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/RemoveFromHistorySwipeAction.java +++ /dev/null @@ -1,58 +0,0 @@ -package de.danoeh.antennapod.fragment.swipeactions; - -import android.content.Context; - -import androidx.fragment.app.Fragment; - -import com.google.android.material.snackbar.Snackbar; - -import java.util.Date; - -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.activity.MainActivity; -import de.danoeh.antennapod.storage.database.DBWriter; -import de.danoeh.antennapod.model.feed.FeedItem; -import de.danoeh.antennapod.model.feed.FeedItemFilter; - -public class RemoveFromHistorySwipeAction implements SwipeAction { - - public static final String TAG = "RemoveFromHistorySwipeAction"; - - @Override - public String getId() { - return REMOVE_FROM_HISTORY; - } - - @Override - public int getActionIcon() { - return R.drawable.ic_history_remove; - } - - @Override - public int getActionColor() { - return R.attr.icon_purple; - } - - @Override - public String getTitle(Context context) { - return context.getString(R.string.remove_history_label); - } - - @Override - public void performAction(FeedItem item, Fragment fragment, FeedItemFilter filter) { - - Date playbackCompletionDate = item.getMedia().getPlaybackCompletionDate(); - - DBWriter.deleteFromPlaybackHistory(item); - - ((MainActivity) fragment.requireActivity()) - .showSnackbarAbovePlayer(R.string.removed_history_label, Snackbar.LENGTH_LONG) - .setAction(fragment.getString(R.string.undo), - v -> DBWriter.addItemToPlaybackHistory(item.getMedia(), playbackCompletionDate)); - } - - @Override - public boolean willRemove(FeedItemFilter filter, FeedItem item) { - return true; - } -} \ No newline at end of file diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/RemoveFromInboxSwipeAction.java b/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/RemoveFromInboxSwipeAction.java deleted file mode 100644 index 41d79a711..000000000 --- a/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/RemoveFromInboxSwipeAction.java +++ /dev/null @@ -1,45 +0,0 @@ -package de.danoeh.antennapod.fragment.swipeactions; - -import android.content.Context; - -import androidx.fragment.app.Fragment; - -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.menuhandler.FeedItemMenuHandler; -import de.danoeh.antennapod.model.feed.FeedItem; -import de.danoeh.antennapod.model.feed.FeedItemFilter; - -public class RemoveFromInboxSwipeAction implements SwipeAction { - - @Override - public String getId() { - return REMOVE_FROM_INBOX; - } - - @Override - public int getActionIcon() { - return R.drawable.ic_check; - } - - @Override - public int getActionColor() { - return R.attr.icon_purple; - } - - @Override - public String getTitle(Context context) { - return context.getString(R.string.remove_inbox_label); - } - - @Override - public void performAction(FeedItem item, Fragment fragment, FeedItemFilter filter) { - if (item.isNew()) { - FeedItemMenuHandler.markReadWithUndo(fragment, item, FeedItem.UNPLAYED, willRemove(filter, item)); - } - } - - @Override - public boolean willRemove(FeedItemFilter filter, FeedItem item) { - return filter.showNew; - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/RemoveFromQueueSwipeAction.java b/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/RemoveFromQueueSwipeAction.java deleted file mode 100644 index f5cbf66c6..000000000 --- a/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/RemoveFromQueueSwipeAction.java +++ /dev/null @@ -1,57 +0,0 @@ -package de.danoeh.antennapod.fragment.swipeactions; - -import android.content.Context; - -import androidx.fragment.app.Fragment; - -import com.google.android.material.snackbar.Snackbar; - -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.activity.MainActivity; -import de.danoeh.antennapod.storage.database.DBReader; -import de.danoeh.antennapod.storage.database.DBWriter; -import de.danoeh.antennapod.model.feed.FeedItem; -import de.danoeh.antennapod.model.feed.FeedItemFilter; - -public class RemoveFromQueueSwipeAction implements SwipeAction { - - @Override - public String getId() { - return REMOVE_FROM_QUEUE; - } - - @Override - public int getActionIcon() { - return R.drawable.ic_playlist_remove; - } - - @Override - public int getActionColor() { - return R.attr.colorAccent; - } - - @Override - public String getTitle(Context context) { - return context.getString(R.string.remove_from_queue_label); - } - - @Override - public void performAction(FeedItem item, Fragment fragment, FeedItemFilter filter) { - int position = DBReader.getQueueIDList().indexOf(item.getId()); - - DBWriter.removeQueueItem(fragment.requireActivity(), true, item); - - if (willRemove(filter, item)) { - ((MainActivity) fragment.requireActivity()).showSnackbarAbovePlayer( - fragment.getResources().getQuantityString(R.plurals.removed_from_queue_batch_label, 1, 1), - Snackbar.LENGTH_LONG) - .setAction(fragment.getString(R.string.undo), v -> - DBWriter.addQueueItemAt(fragment.requireActivity(), item.getId(), position, false)); - } - } - - @Override - public boolean willRemove(FeedItemFilter filter, FeedItem item) { - return filter.showQueued || filter.showNotQueued; - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/ShowFirstSwipeDialogAction.java b/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/ShowFirstSwipeDialogAction.java deleted file mode 100644 index 3a6174e8f..000000000 --- a/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/ShowFirstSwipeDialogAction.java +++ /dev/null @@ -1,42 +0,0 @@ -package de.danoeh.antennapod.fragment.swipeactions; - -import android.content.Context; - -import androidx.fragment.app.Fragment; - -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.model.feed.FeedItem; -import de.danoeh.antennapod.model.feed.FeedItemFilter; - -public class ShowFirstSwipeDialogAction implements SwipeAction { - - @Override - public String getId() { - return "SHOW_FIRST_SWIPE_DIALOG"; - } - - @Override - public int getActionIcon() { - return R.drawable.ic_settings; - } - - @Override - public int getActionColor() { - return R.attr.icon_gray; - } - - @Override - public String getTitle(Context context) { - return ""; - } - - @Override - public void performAction(FeedItem item, Fragment fragment, FeedItemFilter filter) { - //handled in SwipeActions - } - - @Override - public boolean willRemove(FeedItemFilter filter, FeedItem item) { - return false; - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/StartDownloadSwipeAction.java b/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/StartDownloadSwipeAction.java deleted file mode 100644 index 84dc8f417..000000000 --- a/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/StartDownloadSwipeAction.java +++ /dev/null @@ -1,44 +0,0 @@ -package de.danoeh.antennapod.fragment.swipeactions; - -import android.content.Context; -import androidx.fragment.app.Fragment; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.adapter.actionbutton.DownloadActionButton; -import de.danoeh.antennapod.model.feed.FeedItem; -import de.danoeh.antennapod.model.feed.FeedItemFilter; - -public class StartDownloadSwipeAction implements SwipeAction { - - @Override - public String getId() { - return START_DOWNLOAD; - } - - @Override - public int getActionIcon() { - return R.drawable.ic_download; - } - - @Override - public int getActionColor() { - return R.attr.icon_green; - } - - @Override - public String getTitle(Context context) { - return context.getString(R.string.download_label); - } - - @Override - public void performAction(FeedItem item, Fragment fragment, FeedItemFilter filter) { - if (!item.isDownloaded() && !item.getFeed().isLocalFeed()) { - new DownloadActionButton(item) - .onClick(fragment.requireContext()); - } - } - - @Override - public boolean willRemove(FeedItemFilter filter, FeedItem item) { - return false; - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/SwipeAction.java b/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/SwipeAction.java deleted file mode 100644 index 4b1cfdc78..000000000 --- a/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/SwipeAction.java +++ /dev/null @@ -1,36 +0,0 @@ -package de.danoeh.antennapod.fragment.swipeactions; - -import android.content.Context; - -import androidx.annotation.AttrRes; -import androidx.annotation.DrawableRes; -import androidx.fragment.app.Fragment; - -import de.danoeh.antennapod.model.feed.FeedItem; -import de.danoeh.antennapod.model.feed.FeedItemFilter; - -public interface SwipeAction { - - String ADD_TO_QUEUE = "ADD_TO_QUEUE"; - String REMOVE_FROM_INBOX = "REMOVE_FROM_INBOX"; - String START_DOWNLOAD = "START_DOWNLOAD"; - String MARK_FAV = "MARK_FAV"; - String TOGGLE_PLAYED = "MARK_PLAYED"; - String REMOVE_FROM_QUEUE = "REMOVE_FROM_QUEUE"; - String DELETE = "DELETE"; - String REMOVE_FROM_HISTORY = "REMOVE_FROM_HISTORY"; - - String getId(); - - String getTitle(Context context); - - @DrawableRes - int getActionIcon(); - - @AttrRes - int getActionColor(); - - void performAction(FeedItem item, Fragment fragment, FeedItemFilter filter); - - boolean willRemove(FeedItemFilter filter, FeedItem item); -} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/SwipeActions.java b/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/SwipeActions.java deleted file mode 100644 index 28320099a..000000000 --- a/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/SwipeActions.java +++ /dev/null @@ -1,268 +0,0 @@ -package de.danoeh.antennapod.fragment.swipeactions; - -import android.content.Context; -import android.content.SharedPreferences; -import android.graphics.Canvas; - -import androidx.annotation.NonNull; -import androidx.core.graphics.ColorUtils; -import androidx.fragment.app.Fragment; -import androidx.lifecycle.Lifecycle; -import androidx.lifecycle.LifecycleObserver; -import androidx.lifecycle.OnLifecycleEvent; -import androidx.recyclerview.widget.ItemTouchHelper; -import androidx.recyclerview.widget.RecyclerView; - -import java.util.Arrays; -import java.util.Collections; -import java.util.List; - -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.dialog.SwipeActionsDialog; -import de.danoeh.antennapod.fragment.AllEpisodesFragment; -import de.danoeh.antennapod.fragment.CompletedDownloadsFragment; -import de.danoeh.antennapod.fragment.InboxFragment; -import de.danoeh.antennapod.fragment.PlaybackHistoryFragment; -import de.danoeh.antennapod.fragment.QueueFragment; -import de.danoeh.antennapod.model.feed.FeedItem; -import de.danoeh.antennapod.model.feed.FeedItemFilter; -import de.danoeh.antennapod.ui.common.ThemeUtils; -import de.danoeh.antennapod.view.viewholder.EpisodeItemViewHolder; -import it.xabaras.android.recyclerview.swipedecorator.RecyclerViewSwipeDecorator; - -public class SwipeActions extends ItemTouchHelper.SimpleCallback implements LifecycleObserver { - public static final String PREF_NAME = "SwipeActionsPrefs"; - public static final String KEY_PREFIX_SWIPEACTIONS = "PrefSwipeActions"; - public static final String KEY_PREFIX_NO_ACTION = "PrefNoSwipeAction"; - - private static final List swipeActions = Collections.unmodifiableList( - Arrays.asList(new AddToQueueSwipeAction(), new RemoveFromInboxSwipeAction(), - new StartDownloadSwipeAction(), new MarkFavoriteSwipeAction(), - new TogglePlaybackStateSwipeAction(), new RemoveFromQueueSwipeAction(), - new DeleteSwipeAction(), new RemoveFromHistorySwipeAction())); - - private final Fragment fragment; - private final String tag; - private FeedItemFilter filter = null; - - Actions actions; - boolean swipeOutEnabled = true; - int swipedOutTo = 0; - private final ItemTouchHelper itemTouchHelper = new ItemTouchHelper(this); - - public SwipeActions(int dragDirs, Fragment fragment, String tag) { - super(dragDirs, ItemTouchHelper.RIGHT | ItemTouchHelper.LEFT); - this.fragment = fragment; - this.tag = tag; - reloadPreference(); - fragment.getLifecycle().addObserver(this); - } - - public SwipeActions(Fragment fragment, String tag) { - this(0, fragment, tag); - } - - public static SwipeAction getAction(String key) { - for (SwipeAction action : swipeActions) { - if (action.getId().equals(key)) { - return action; - } - } - return null; - } - - @OnLifecycleEvent(Lifecycle.Event.ON_START) - public void reloadPreference() { - actions = getPrefs(fragment.requireContext(), tag); - } - - public void setFilter(FeedItemFilter filter) { - this.filter = filter; - } - - public SwipeActions attachTo(RecyclerView recyclerView) { - itemTouchHelper.attachToRecyclerView(recyclerView); - return this; - } - - public void detach() { - itemTouchHelper.attachToRecyclerView(null); - } - - private static Actions getPrefs(Context context, String tag, String defaultActions) { - SharedPreferences prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); - String prefsString = prefs.getString(KEY_PREFIX_SWIPEACTIONS + tag, defaultActions); - - return new Actions(prefsString); - } - - private static Actions getPrefs(Context context, String tag) { - return getPrefs(context, tag, ""); - } - - public static Actions getPrefsWithDefaults(Context context, String tag) { - String defaultActions; - switch (tag) { - case InboxFragment.TAG: - defaultActions = SwipeAction.ADD_TO_QUEUE + "," + SwipeAction.REMOVE_FROM_INBOX; - break; - case QueueFragment.TAG: - defaultActions = SwipeAction.REMOVE_FROM_QUEUE + "," + SwipeAction.REMOVE_FROM_QUEUE; - break; - case CompletedDownloadsFragment.TAG: - defaultActions = SwipeAction.DELETE + "," + SwipeAction.DELETE; - break; - case PlaybackHistoryFragment.TAG: - defaultActions = SwipeAction.REMOVE_FROM_HISTORY + "," + SwipeAction.REMOVE_FROM_HISTORY; - break; - default: - case AllEpisodesFragment.TAG: - defaultActions = SwipeAction.MARK_FAV + "," + SwipeAction.START_DOWNLOAD; - break; - } - - return getPrefs(context, tag, defaultActions); - } - - public static boolean isSwipeActionEnabled(Context context, String tag) { - SharedPreferences prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); - return prefs.getBoolean(KEY_PREFIX_NO_ACTION + tag, true); - } - - private boolean isSwipeActionEnabled() { - return isSwipeActionEnabled(fragment.requireContext(), tag); - } - - @Override - public boolean onMove(@NonNull RecyclerView recyclerView, - @NonNull RecyclerView.ViewHolder viewHolder, - @NonNull RecyclerView.ViewHolder target) { - return false; - } - - @Override - public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int swipeDir) { - if (!actions.hasActions()) { - //open settings dialog if no prefs are set - new SwipeActionsDialog(fragment.requireContext(), tag).show(this::reloadPreference); - return; - } - - FeedItem item = ((EpisodeItemViewHolder) viewHolder).getFeedItem(); - - (swipeDir == ItemTouchHelper.RIGHT ? actions.right : actions.left) - .performAction(item, fragment, filter); - } - - @Override - public void onChildDraw(@NonNull Canvas c, @NonNull RecyclerView recyclerView, - @NonNull RecyclerView.ViewHolder viewHolder, - float dx, float dy, int actionState, boolean isCurrentlyActive) { - SwipeAction right; - SwipeAction left; - if (actions.hasActions()) { - right = actions.right; - left = actions.left; - } else { - right = left = new ShowFirstSwipeDialogAction(); - } - - //check if it will be removed - FeedItem item = ((EpisodeItemViewHolder) viewHolder).getFeedItem(); - boolean rightWillRemove = right.willRemove(filter, item); - boolean leftWillRemove = left.willRemove(filter, item); - boolean wontLeave = (dx > 0 && !rightWillRemove) || (dx < 0 && !leftWillRemove); - - //Limit swipe if it's not removed - int maxMovement = recyclerView.getWidth() * 2 / 5; - float sign = dx > 0 ? 1 : -1; - float limitMovement = Math.min(maxMovement, sign * dx); - float displacementPercentage = limitMovement / maxMovement; - boolean swipeThresholdReached = displacementPercentage >= 0.85; - - if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE && wontLeave) { - swipeOutEnabled = false; - // Move slower when getting near the maxMovement - dx = sign * maxMovement * 0.7f * (float) Math.sin((Math.PI / 2) * displacementPercentage); - - if (isCurrentlyActive) { - int dir = dx > 0 ? ItemTouchHelper.RIGHT : ItemTouchHelper.LEFT; - swipedOutTo = swipeThresholdReached ? dir : 0; - } - } else { - swipeOutEnabled = true; - } - - //add color and icon - Context context = fragment.requireContext(); - int themeColor = ThemeUtils.getColorFromAttr(context, android.R.attr.colorBackground); - int actionColor = ThemeUtils.getColorFromAttr(context, - dx > 0 ? right.getActionColor() : left.getActionColor()); - RecyclerViewSwipeDecorator.Builder builder = new RecyclerViewSwipeDecorator.Builder( - c, recyclerView, viewHolder, dx, dy, actionState, isCurrentlyActive) - .addSwipeRightActionIcon(right.getActionIcon()) - .addSwipeLeftActionIcon(left.getActionIcon()) - .addSwipeRightBackgroundColor(ThemeUtils.getColorFromAttr(context, R.attr.background_elevated)) - .addSwipeLeftBackgroundColor(ThemeUtils.getColorFromAttr(context, R.attr.background_elevated)) - .setActionIconTint(ColorUtils.blendARGB(themeColor, actionColor, - (!wontLeave || swipeThresholdReached) ? 1.0f : 0.7f)); - builder.create().decorate(); - - super.onChildDraw(c, recyclerView, viewHolder, dx, dy, actionState, isCurrentlyActive); - } - - @Override - public float getSwipeEscapeVelocity(float defaultValue) { - return swipeOutEnabled ? defaultValue * 1.5f : Float.MAX_VALUE; - } - - @Override - public float getSwipeVelocityThreshold(float defaultValue) { - return swipeOutEnabled ? defaultValue * 0.6f : 0; - } - - @Override - public float getSwipeThreshold(@NonNull RecyclerView.ViewHolder viewHolder) { - return swipeOutEnabled ? 0.6f : 1.0f; - } - - @Override - public void clearView(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) { - super.clearView(recyclerView, viewHolder); - - if (swipedOutTo != 0) { - onSwiped(viewHolder, swipedOutTo); - swipedOutTo = 0; - } - } - - @Override - public int getMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) { - if (!isSwipeActionEnabled()) { - return makeMovementFlags(getDragDirs(recyclerView, viewHolder), 0); - } else { - return super.getMovementFlags(recyclerView, viewHolder); - } - } - - public void startDrag(EpisodeItemViewHolder holder) { - itemTouchHelper.startDrag(holder); - } - - public static class Actions { - public SwipeAction right = null; - public SwipeAction left = null; - - public Actions(String prefs) { - String[] actions = prefs.split(","); - if (actions.length == 2) { - right = getAction(actions[0]); - left = getAction(actions[1]); - } - } - - public boolean hasActions() { - return right != null && left != null; - } - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/TogglePlaybackStateSwipeAction.java b/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/TogglePlaybackStateSwipeAction.java deleted file mode 100644 index 8d4133058..000000000 --- a/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/TogglePlaybackStateSwipeAction.java +++ /dev/null @@ -1,48 +0,0 @@ -package de.danoeh.antennapod.fragment.swipeactions; - -import android.content.Context; - -import androidx.fragment.app.Fragment; - -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.menuhandler.FeedItemMenuHandler; -import de.danoeh.antennapod.model.feed.FeedItem; -import de.danoeh.antennapod.model.feed.FeedItemFilter; - -public class TogglePlaybackStateSwipeAction implements SwipeAction { - - @Override - public String getId() { - return TOGGLE_PLAYED; - } - - @Override - public int getActionIcon() { - return R.drawable.ic_mark_played; - } - - @Override - public int getActionColor() { - return R.attr.icon_gray; - } - - @Override - public String getTitle(Context context) { - return context.getString(R.string.toggle_played_label); - } - - @Override - public void performAction(FeedItem item, Fragment fragment, FeedItemFilter filter) { - int newState = item.getPlayState() == FeedItem.UNPLAYED ? FeedItem.PLAYED : FeedItem.UNPLAYED; - FeedItemMenuHandler.markReadWithUndo(fragment, item, newState, willRemove(filter, item)); - } - - @Override - public boolean willRemove(FeedItemFilter filter, FeedItem item) { - if (item.getPlayState() == FeedItem.NEW) { - return filter.showPlayed || filter.showNew; - } else { - return filter.showUnplayed || filter.showPlayed || filter.showNew; - } - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedItemMenuHandler.java b/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedItemMenuHandler.java deleted file mode 100644 index 422f65090..000000000 --- a/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedItemMenuHandler.java +++ /dev/null @@ -1,282 +0,0 @@ -package de.danoeh.antennapod.menuhandler; - -import android.content.Context; -import android.os.Handler; -import android.util.Log; -import android.view.KeyEvent; -import android.view.Menu; -import android.view.MenuItem; - -import androidx.annotation.NonNull; -import androidx.fragment.app.Fragment; - -import com.google.android.material.snackbar.Snackbar; - -import java.util.Arrays; - -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.activity.MainActivity; -import de.danoeh.antennapod.net.sync.serviceinterface.SynchronizationQueueSink; -import de.danoeh.antennapod.storage.preferences.PlaybackPreferences; -import de.danoeh.antennapod.core.util.FeedUtil; -import de.danoeh.antennapod.playback.service.PlaybackServiceInterface; -import de.danoeh.antennapod.storage.database.DBWriter; -import de.danoeh.antennapod.storage.preferences.SynchronizationSettings; -import de.danoeh.antennapod.core.util.FeedItemUtil; -import de.danoeh.antennapod.core.util.IntentUtils; -import de.danoeh.antennapod.playback.service.PlaybackStatus; -import de.danoeh.antennapod.core.util.ShareUtils; -import de.danoeh.antennapod.dialog.ShareDialog; -import de.danoeh.antennapod.model.feed.FeedItem; -import de.danoeh.antennapod.model.feed.FeedMedia; -import de.danoeh.antennapod.net.sync.model.EpisodeAction; -import de.danoeh.antennapod.storage.preferences.UserPreferences; -import de.danoeh.antennapod.ui.appstartintent.MediaButtonStarter; -import de.danoeh.antennapod.view.LocalDeleteModal; - -/** - * Handles interactions with the FeedItemMenu. - */ -public class FeedItemMenuHandler { - - private static final String TAG = "FeedItemMenuHandler"; - - private FeedItemMenuHandler() { - } - - /** - * 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 menu An instance of Menu - * @param selectedItem The FeedItem for which the menu is supposed to be prepared - * @return Returns true if selectedItem is not null. - */ - public static boolean onPrepareMenu(Menu menu, FeedItem selectedItem) { - if (menu == null || selectedItem == null) { - return false; - } - final boolean hasMedia = selectedItem.getMedia() != null; - final boolean isPlaying = hasMedia && PlaybackStatus.isPlaying(selectedItem.getMedia()); - final boolean isInQueue = selectedItem.isTagged(FeedItem.TAG_QUEUE); - final boolean fileDownloaded = hasMedia && selectedItem.getMedia().fileExists(); - final boolean isLocalFile = hasMedia && selectedItem.getFeed().isLocalFeed(); - final boolean isFavorite = selectedItem.isTagged(FeedItem.TAG_FAVORITE); - - setItemVisibility(menu, R.id.skip_episode_item, isPlaying); - setItemVisibility(menu, R.id.remove_from_queue_item, isInQueue); - setItemVisibility(menu, R.id.add_to_queue_item, !isInQueue && selectedItem.getMedia() != null); - setItemVisibility(menu, R.id.visit_website_item, !selectedItem.getFeed().isLocalFeed() - && ShareUtils.hasLinkToShare(selectedItem)); - setItemVisibility(menu, R.id.share_item, !selectedItem.getFeed().isLocalFeed()); - setItemVisibility(menu, R.id.remove_inbox_item, selectedItem.isNew()); - setItemVisibility(menu, R.id.mark_read_item, !selectedItem.isPlayed()); - setItemVisibility(menu, R.id.mark_unread_item, selectedItem.isPlayed()); - setItemVisibility(menu, R.id.reset_position, hasMedia && selectedItem.getMedia().getPosition() != 0); - - // Display proper strings when item has no media - if (hasMedia) { - setItemTitle(menu, R.id.mark_read_item, R.string.mark_read_label); - setItemTitle(menu, R.id.mark_unread_item, R.string.mark_unread_label); - } else { - setItemTitle(menu, R.id.mark_read_item, R.string.mark_read_no_media_label); - setItemTitle(menu, R.id.mark_unread_item, R.string.mark_unread_label_no_media); - } - - setItemVisibility(menu, R.id.add_to_favorites_item, !isFavorite); - setItemVisibility(menu, R.id.remove_from_favorites_item, isFavorite); - setItemVisibility(menu, R.id.remove_item, fileDownloaded || isLocalFile); - return true; - } - - /** - * Used to set the viability of a menu item. - * This method also does some null-checking so that neither menu nor the menu item are null - * in order to prevent nullpointer exceptions. - * @param menu The menu that should be used - * @param menuId The id of the menu item that will be used - * @param visibility The new visibility status of given menu item - * */ - private static void setItemVisibility(Menu menu, int menuId, boolean visibility) { - if (menu == null) { - return; - } - MenuItem item = menu.findItem(menuId); - if (item != null) { - item.setVisible(visibility); - } - } - - /** - * This method allows to replace to String of a menu item with a different one. - * @param menu Menu item that should be used - * @param id The id of the string that is going to be replaced. - * @param noMedia The id of the new String that is going to be used. - * */ - public static void setItemTitle(Menu menu, int id, int noMedia) { - MenuItem item = menu.findItem(id); - if (item != null) { - item.setTitle(noMedia); - } - } - - /** - * The same method as {@link #onPrepareMenu(Menu, FeedItem)}, 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(Menu menu, FeedItem selectedItem, int... excludeIds) { - if (menu == null || selectedItem == null) { - return false; - } - boolean rc = onPrepareMenu(menu, selectedItem); - if (rc && excludeIds != null) { - for (int id : excludeIds) { - setItemVisibility(menu, id, false); - } - } - return rc; - } - - /** - * Default menu handling for the given FeedItem. - * - * A Fragment instance, (rather than the more generic Context), is needed as a parameter - * to support some UI operations, e.g., creating a Snackbar. - */ - public static boolean onMenuItemClicked(@NonNull Fragment fragment, int menuItemId, - @NonNull FeedItem selectedItem) { - - @NonNull Context context = fragment.requireContext(); - if (menuItemId == R.id.skip_episode_item) { - context.sendBroadcast(MediaButtonStarter.createIntent(context, KeyEvent.KEYCODE_MEDIA_NEXT)); - } else if (menuItemId == R.id.remove_item) { - LocalDeleteModal.showLocalFeedDeleteWarningIfNecessary(context, Arrays.asList(selectedItem), - () -> DBWriter.deleteFeedMediaOfItem(context, selectedItem.getMedia())); - } else if (menuItemId == R.id.remove_inbox_item) { - removeNewFlagWithUndo(fragment, selectedItem); - } else if (menuItemId == R.id.mark_read_item) { - selectedItem.setPlayed(true); - DBWriter.markItemPlayed(selectedItem, FeedItem.PLAYED, true); - if (!selectedItem.getFeed().isLocalFeed() && SynchronizationSettings.isProviderConnected()) { - FeedMedia media = selectedItem.getMedia(); - // not all items have media, Gpodder only cares about those that do - if (media != null) { - EpisodeAction actionPlay = new EpisodeAction.Builder(selectedItem, EpisodeAction.PLAY) - .currentTimestamp() - .started(media.getDuration() / 1000) - .position(media.getDuration() / 1000) - .total(media.getDuration() / 1000) - .build(); - SynchronizationQueueSink.enqueueEpisodeActionIfSynchronizationIsActive(context, actionPlay); - } - } - } else if (menuItemId == R.id.mark_unread_item) { - selectedItem.setPlayed(false); - DBWriter.markItemPlayed(selectedItem, FeedItem.UNPLAYED, false); - if (!selectedItem.getFeed().isLocalFeed() && selectedItem.getMedia() != null) { - EpisodeAction actionNew = new EpisodeAction.Builder(selectedItem, EpisodeAction.NEW) - .currentTimestamp() - .build(); - SynchronizationQueueSink.enqueueEpisodeActionIfSynchronizationIsActive(context, actionNew); - } - } else if (menuItemId == R.id.add_to_queue_item) { - DBWriter.addQueueItem(context, selectedItem); - } else if (menuItemId == R.id.remove_from_queue_item) { - DBWriter.removeQueueItem(context, true, selectedItem); - } else if (menuItemId == R.id.add_to_favorites_item) { - DBWriter.addFavoriteItem(selectedItem); - } else if (menuItemId == R.id.remove_from_favorites_item) { - DBWriter.removeFavoriteItem(selectedItem); - } else if (menuItemId == R.id.reset_position) { - selectedItem.getMedia().setPosition(0); - if (PlaybackPreferences.getCurrentlyPlayingFeedMediaId() == selectedItem.getMedia().getId()) { - PlaybackPreferences.writeNoMediaPlaying(); - IntentUtils.sendLocalBroadcast(context, PlaybackServiceInterface.ACTION_SHUTDOWN_PLAYBACK_SERVICE); - } - DBWriter.markItemPlayed(selectedItem, FeedItem.UNPLAYED, true); - } else if (menuItemId == R.id.visit_website_item) { - IntentUtils.openInBrowser(context, FeedItemUtil.getLinkWithFallback(selectedItem)); - } else if (menuItemId == R.id.share_item) { - ShareDialog shareDialog = ShareDialog.newInstance(selectedItem); - shareDialog.show((fragment.getActivity().getSupportFragmentManager()), "ShareEpisodeDialog"); - } else { - Log.d(TAG, "Unknown menuItemId: " + menuItemId); - return false; - } - // Refresh menu state - - return true; - } - - /** - * Remove new flag with additional UI logic to allow undo with Snackbar. - * - * Undo is useful for Remove new flag, given there is no UI to undo it otherwise - * ,i.e., there is (context) menu item for add new flag - */ - public static void markReadWithUndo(@NonNull Fragment fragment, FeedItem item, - int playState, boolean showSnackbar) { - if (item == null) { - return; - } - - Log.d(TAG, "markReadWithUndo(" + item.getId() + ")"); - // we're marking it as unplayed since the user didn't actually play it - // but they don't want it considered 'NEW' anymore - DBWriter.markItemPlayed(playState, item.getId()); - - final Handler h = new Handler(fragment.requireContext().getMainLooper()); - final Runnable r = () -> { - FeedMedia media = item.getMedia(); - if (media == null) { - return; - } - boolean shouldAutoDelete = FeedUtil.shouldAutoDeleteItemsOnThatFeed(item.getFeed()); - int smartMarkAsPlayedSecs = UserPreferences.getSmartMarkAsPlayedSecs(); - boolean almostEnded = media.getDuration() > 0 - && media.getPosition() >= media.getDuration() - smartMarkAsPlayedSecs * 1000; - if (almostEnded && shouldAutoDelete) { - DBWriter.deleteFeedMediaOfItem(fragment.requireContext(), media); - } - }; - - int playStateStringRes; - switch (playState) { - default: - case FeedItem.UNPLAYED: - if (item.getPlayState() == FeedItem.NEW) { - //was new - playStateStringRes = R.string.removed_inbox_label; - } else { - //was played - playStateStringRes = R.string.marked_as_unplayed_label; - } - break; - case FeedItem.PLAYED: - playStateStringRes = R.string.marked_as_played_label; - break; - } - - int duration = Snackbar.LENGTH_LONG; - - if (showSnackbar) { - ((MainActivity) fragment.getActivity()).showSnackbarAbovePlayer( - playStateStringRes, duration) - .setAction(fragment.getString(R.string.undo), v -> { - DBWriter.markItemPlayed(item.getPlayState(), item.getId()); - // don't forget to cancel the thing that's going to remove the media - h.removeCallbacks(r); - }); - } - - h.postDelayed(r, (int) Math.ceil(duration * 1.05f)); - } - - public static void removeNewFlagWithUndo(@NonNull Fragment fragment, FeedItem item) { - markReadWithUndo(fragment, item, FeedItem.UNPLAYED, false); - } - -} diff --git a/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedMenuHandler.java b/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedMenuHandler.java deleted file mode 100644 index c0448884d..000000000 --- a/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedMenuHandler.java +++ /dev/null @@ -1,63 +0,0 @@ -package de.danoeh.antennapod.menuhandler; - -import android.annotation.SuppressLint; -import android.content.Context; -import android.content.DialogInterface; -import android.util.Log; -import androidx.annotation.NonNull; -import androidx.fragment.app.Fragment; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.core.dialog.ConfirmationDialog; -import de.danoeh.antennapod.storage.database.DBWriter; -import de.danoeh.antennapod.dialog.RemoveFeedDialog; -import de.danoeh.antennapod.dialog.RenameItemDialog; -import de.danoeh.antennapod.dialog.TagSettingsDialog; -import de.danoeh.antennapod.model.feed.Feed; -import io.reactivex.Observable; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.schedulers.Schedulers; - -import java.util.Collections; -import java.util.concurrent.Callable; -import java.util.concurrent.Future; - -/** - * Handles interactions with the FeedItemMenu. - */ -public abstract class FeedMenuHandler { - private static final String TAG = "FeedMenuHandler"; - - public static boolean onMenuItemClicked(@NonNull Fragment fragment, int menuItemId, - @NonNull Feed selectedFeed, Runnable callback) { - @NonNull Context context = fragment.requireContext(); - if (menuItemId == R.id.rename_folder_item) { - new RenameItemDialog(fragment.getActivity(), selectedFeed).show(); - } else if (menuItemId == R.id.remove_all_inbox_item) { - ConfirmationDialog dialog = new ConfirmationDialog(fragment.getActivity(), - R.string.remove_all_inbox_label, R.string.remove_all_inbox_confirmation_msg) { - @Override - @SuppressLint("CheckResult") - public void onConfirmButtonPressed(DialogInterface clickedDialog) { - clickedDialog.dismiss(); - Observable.fromCallable((Callable) () -> DBWriter.removeFeedNewFlag(selectedFeed.getId())) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(result -> callback.run(), - error -> Log.e(TAG, Log.getStackTraceString(error))); - } - }; - dialog.createNewDialog().show(); - - } else if (menuItemId == R.id.edit_tags) { - TagSettingsDialog.newInstance(Collections.singletonList(selectedFeed.getPreferences())) - .show(fragment.getChildFragmentManager(), TagSettingsDialog.TAG); - } else if (menuItemId == R.id.rename_item) { - new RenameItemDialog(fragment.getActivity(), selectedFeed).show(); - } else if (menuItemId == R.id.remove_feed) { - RemoveFeedDialog.show(context, selectedFeed, null); - } else { - return false; - } - return true; - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/preferences/PreferenceUpgrader.java b/app/src/main/java/de/danoeh/antennapod/preferences/PreferenceUpgrader.java deleted file mode 100644 index 03355510d..000000000 --- a/app/src/main/java/de/danoeh/antennapod/preferences/PreferenceUpgrader.java +++ /dev/null @@ -1,170 +0,0 @@ -package de.danoeh.antennapod.preferences; - -import android.content.Context; -import android.content.SharedPreferences; -import android.view.KeyEvent; -import androidx.core.app.NotificationManagerCompat; -import androidx.preference.PreferenceManager; - -import org.apache.commons.lang3.StringUtils; - -import java.util.concurrent.TimeUnit; - -import de.danoeh.antennapod.BuildConfig; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.storage.preferences.SleepTimerPreferences; -import de.danoeh.antennapod.error.CrashReportWriter; -import de.danoeh.antennapod.fragment.AllEpisodesFragment; -import de.danoeh.antennapod.storage.preferences.UserPreferences; -import de.danoeh.antennapod.storage.preferences.UserPreferences.EnqueueLocation; -import de.danoeh.antennapod.fragment.QueueFragment; -import de.danoeh.antennapod.fragment.swipeactions.SwipeAction; -import de.danoeh.antennapod.fragment.swipeactions.SwipeActions; - -public class PreferenceUpgrader { - private static final String PREF_CONFIGURED_VERSION = "version_code"; - private static final String PREF_NAME = "app_version"; - - private static SharedPreferences prefs; - - public static void checkUpgrades(Context context) { - prefs = PreferenceManager.getDefaultSharedPreferences(context); - SharedPreferences upgraderPrefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); - int oldVersion = upgraderPrefs.getInt(PREF_CONFIGURED_VERSION, -1); - int newVersion = BuildConfig.VERSION_CODE; - - if (oldVersion != newVersion) { - CrashReportWriter.getFile().delete(); - - upgrade(oldVersion, context); - upgraderPrefs.edit().putInt(PREF_CONFIGURED_VERSION, newVersion).apply(); - } - } - - private static void upgrade(int oldVersion, Context context) { - if (oldVersion == -1) { - //New installation - return; - } - if (oldVersion < 1070196) { - // migrate episode cleanup value (unit changed from days to hours) - int oldValueInDays = UserPreferences.getEpisodeCleanupValue(); - if (oldValueInDays > 0) { - UserPreferences.setEpisodeCleanupValue(oldValueInDays * 24); - } // else 0 or special negative values, no change needed - } - if (oldVersion < 1070197) { - if (prefs.getBoolean("prefMobileUpdate", false)) { - prefs.edit().putString("prefMobileUpdateAllowed", "everything").apply(); - } - } - if (oldVersion < 1070300) { - if (prefs.getBoolean("prefEnableAutoDownloadOnMobile", false)) { - UserPreferences.setAllowMobileAutoDownload(true); - } - switch (prefs.getString("prefMobileUpdateAllowed", "images")) { - case "everything": - UserPreferences.setAllowMobileFeedRefresh(true); - UserPreferences.setAllowMobileEpisodeDownload(true); - UserPreferences.setAllowMobileImages(true); - break; - case "images": - UserPreferences.setAllowMobileImages(true); - break; - case "nothing": - UserPreferences.setAllowMobileImages(false); - break; - } - } - if (oldVersion < 1070400) { - UserPreferences.ThemePreference theme = UserPreferences.getTheme(); - if (theme == UserPreferences.ThemePreference.LIGHT) { - prefs.edit().putString(UserPreferences.PREF_THEME, "system").apply(); - } - - UserPreferences.setQueueLocked(false); - UserPreferences.setStreamOverDownload(false); - - if (!prefs.contains(UserPreferences.PREF_ENQUEUE_LOCATION)) { - final String keyOldPrefEnqueueFront = "prefQueueAddToFront"; - boolean enqueueAtFront = prefs.getBoolean(keyOldPrefEnqueueFront, false); - EnqueueLocation enqueueLocation = enqueueAtFront ? EnqueueLocation.FRONT : EnqueueLocation.BACK; - UserPreferences.setEnqueueLocation(enqueueLocation); - } - } - if (oldVersion < 2010300) { - // Migrate hardware button preferences - if (prefs.getBoolean("prefHardwareForwardButtonSkips", false)) { - prefs.edit().putString(UserPreferences.PREF_HARDWARE_FORWARD_BUTTON, - String.valueOf(KeyEvent.KEYCODE_MEDIA_NEXT)).apply(); - } - if (prefs.getBoolean("prefHardwarePreviousButtonRestarts", false)) { - prefs.edit().putString(UserPreferences.PREF_HARDWARE_PREVIOUS_BUTTON, - String.valueOf(KeyEvent.KEYCODE_MEDIA_PREVIOUS)).apply(); - } - } - if (oldVersion < 2040000) { - SharedPreferences swipePrefs = context.getSharedPreferences(SwipeActions.PREF_NAME, Context.MODE_PRIVATE); - swipePrefs.edit().putString(SwipeActions.KEY_PREFIX_SWIPEACTIONS + QueueFragment.TAG, - SwipeAction.REMOVE_FROM_QUEUE + "," + SwipeAction.REMOVE_FROM_QUEUE).apply(); - } - if (oldVersion < 2050000) { - prefs.edit().putBoolean(UserPreferences.PREF_PAUSE_PLAYBACK_FOR_FOCUS_LOSS, true).apply(); - } - if (oldVersion < 2080000) { - // Migrate drawer feed counter setting to reflect removal of - // "unplayed and in inbox" (0), by changing it to "unplayed" (2) - String feedCounterSetting = prefs.getString(UserPreferences.PREF_DRAWER_FEED_COUNTER, "1"); - if (feedCounterSetting.equals("0")) { - prefs.edit().putString(UserPreferences.PREF_DRAWER_FEED_COUNTER, "2").apply(); - } - - SharedPreferences sleepTimerPreferences = - context.getSharedPreferences(SleepTimerPreferences.PREF_NAME, Context.MODE_PRIVATE); - TimeUnit[] timeUnits = { TimeUnit.SECONDS, TimeUnit.MINUTES, TimeUnit.HOURS }; - long value = Long.parseLong(SleepTimerPreferences.lastTimerValue()); - TimeUnit unit = timeUnits[sleepTimerPreferences.getInt("LastTimeUnit", 1)]; - SleepTimerPreferences.setLastTimer(String.valueOf(unit.toMinutes(value))); - - if (prefs.getString(UserPreferences.PREF_EPISODE_CACHE_SIZE, "20") - .equals(context.getString(R.string.pref_episode_cache_unlimited))) { - prefs.edit().putString(UserPreferences.PREF_EPISODE_CACHE_SIZE, - "" + UserPreferences.EPISODE_CACHE_SIZE_UNLIMITED).apply(); - } - } - if (oldVersion < 3000007) { - if (prefs.getString("prefBackButtonBehavior", "").equals("drawer")) { - prefs.edit().putBoolean(UserPreferences.PREF_BACK_OPENS_DRAWER, true).apply(); - } - } - if (oldVersion < 3010000) { - if (prefs.getString(UserPreferences.PREF_THEME, "system").equals("2")) { - prefs.edit() - .putString(UserPreferences.PREF_THEME, "1") - .putBoolean(UserPreferences.PREF_THEME_BLACK, true) - .apply(); - } - UserPreferences.setAllowMobileSync(true); - if (prefs.getString(UserPreferences.PREF_UPDATE_INTERVAL, ":").contains(":")) { // Unset or "time of day" - prefs.edit().putString(UserPreferences.PREF_UPDATE_INTERVAL, "12").apply(); - } - } - if (oldVersion < 3020000) { - NotificationManagerCompat.from(context).deleteNotificationChannel("auto_download"); - } - - if (oldVersion < 3030000) { - SharedPreferences allEpisodesPreferences = - context.getSharedPreferences(AllEpisodesFragment.PREF_NAME, Context.MODE_PRIVATE); - String oldEpisodeSort = allEpisodesPreferences.getString(UserPreferences.PREF_SORT_ALL_EPISODES, ""); - if (!StringUtils.isAllEmpty(oldEpisodeSort)) { - prefs.edit().putString(UserPreferences.PREF_SORT_ALL_EPISODES, oldEpisodeSort).apply(); - } - - String oldEpisodeFilter = allEpisodesPreferences.getString("filter", ""); - if (!StringUtils.isAllEmpty(oldEpisodeFilter)) { - prefs.edit().putString(UserPreferences.PREF_FILTER_ALL_EPISODES, oldEpisodeFilter).apply(); - } - } - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/preferences/VolumeAdaptationPreference.java b/app/src/main/java/de/danoeh/antennapod/preferences/VolumeAdaptationPreference.java deleted file mode 100644 index a4ed402ed..000000000 --- a/app/src/main/java/de/danoeh/antennapod/preferences/VolumeAdaptationPreference.java +++ /dev/null @@ -1,28 +0,0 @@ -package de.danoeh.antennapod.preferences; - -import android.content.Context; -import android.util.AttributeSet; - -import java.util.Arrays; - -import de.danoeh.antennapod.model.feed.VolumeAdaptionSetting; -import de.danoeh.antennapod.ui.preferences.preference.MaterialListPreference; - -public class VolumeAdaptationPreference extends MaterialListPreference { - public VolumeAdaptationPreference(Context context) { - super(context); - } - - public VolumeAdaptationPreference(Context context, AttributeSet attrs) { - super(context, attrs); - } - - @Override - public CharSequence[] getEntries() { - if (VolumeAdaptionSetting.isBoostSupported()) { - return super.getEntries(); - } else { - return Arrays.copyOfRange(super.getEntries(), 0, 3); - } - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/receiver/ConnectivityActionReceiver.java b/app/src/main/java/de/danoeh/antennapod/receiver/ConnectivityActionReceiver.java deleted file mode 100644 index 100869120..000000000 --- a/app/src/main/java/de/danoeh/antennapod/receiver/ConnectivityActionReceiver.java +++ /dev/null @@ -1,25 +0,0 @@ -package de.danoeh.antennapod.receiver; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.net.ConnectivityManager; -import android.text.TextUtils; -import android.util.Log; - -import de.danoeh.antennapod.ClientConfigurator; -import de.danoeh.antennapod.core.util.download.NetworkConnectionChangeHandler; - -public class ConnectivityActionReceiver extends BroadcastReceiver { - private static final String TAG = "ConnectivityActionRecvr"; - - @Override - public void onReceive(final Context context, Intent intent) { - if (TextUtils.equals(intent.getAction(), ConnectivityManager.CONNECTIVITY_ACTION)) { - Log.d(TAG, "Received intent"); - - ClientConfigurator.initialize(context); - NetworkConnectionChangeHandler.networkChangedDetected(); - } - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/receiver/PowerConnectionReceiver.java b/app/src/main/java/de/danoeh/antennapod/receiver/PowerConnectionReceiver.java deleted file mode 100644 index 6e179647e..000000000 --- a/app/src/main/java/de/danoeh/antennapod/receiver/PowerConnectionReceiver.java +++ /dev/null @@ -1,48 +0,0 @@ -package de.danoeh.antennapod.receiver; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.util.Log; - -import de.danoeh.antennapod.ClientConfigurator; -import de.danoeh.antennapod.net.download.serviceinterface.AutoDownloadManager; -import de.danoeh.antennapod.storage.preferences.UserPreferences; -import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterface; - -// modified from http://developer.android.com/training/monitoring-device-state/battery-monitoring.html -// and ConnectivityActionReceiver.java -// Updated based on http://stackoverflow.com/questions/20833241/android-charge-intent-has-no-extra-data -// Since the intent doesn't have the EXTRA_STATUS like the android.com article says it does -// (though it used to) -public class PowerConnectionReceiver extends BroadcastReceiver { - private static final String TAG = "PowerConnectionReceiver"; - - @Override - public void onReceive(Context context, Intent intent) { - final String action = intent.getAction(); - - Log.d(TAG, "charging intent: " + action); - - ClientConfigurator.initialize(context); - if (Intent.ACTION_POWER_CONNECTED.equals(action)) { - Log.d(TAG, "charging, starting auto-download"); - // we're plugged in, this is a great time to auto-download if everything else is - // right. So, even if the user allows auto-dl on battery, let's still start - // downloading now. They shouldn't mind. - // autodownloadUndownloadedItems will make sure we're on the right wifi networks, - // etc... so we don't have to worry about it. - AutoDownloadManager.getInstance().autodownloadUndownloadedItems(context); - } else { - // if we're not supposed to be auto-downloading when we're not charging, stop it - if (!UserPreferences.isEnableAutodownloadOnBattery()) { - Log.d(TAG, "not charging anymore, canceling auto-download"); - DownloadServiceInterface.get().cancelAll(context); - } else { - Log.d(TAG, "not charging anymore, but the user allows auto-download " + - "when on battery so we'll keep going"); - } - } - - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/receiver/SPAReceiver.java b/app/src/main/java/de/danoeh/antennapod/receiver/SPAReceiver.java deleted file mode 100644 index 1628229be..000000000 --- a/app/src/main/java/de/danoeh/antennapod/receiver/SPAReceiver.java +++ /dev/null @@ -1,54 +0,0 @@ -package de.danoeh.antennapod.receiver; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.text.TextUtils; -import android.util.Log; -import android.widget.Toast; - -import java.util.Arrays; -import java.util.Collections; - -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.ClientConfigurator; -import de.danoeh.antennapod.net.download.serviceinterface.FeedUpdateManager; -import de.danoeh.antennapod.storage.database.FeedDatabaseWriter; -import de.danoeh.antennapod.model.feed.Feed; - -/** - * Receives intents from AntennaPod Single Purpose apps - */ -public class SPAReceiver extends BroadcastReceiver{ - private static final String TAG = "SPAReceiver"; - - public static final String ACTION_SP_APPS_QUERY_FEEDS = "de.danoeh.antennapdsp.intent.SP_APPS_QUERY_FEEDS"; - private static final String ACTION_SP_APPS_QUERY_FEEDS_REPSONSE = "de.danoeh.antennapdsp.intent.SP_APPS_QUERY_FEEDS_RESPONSE"; - private static final String ACTION_SP_APPS_QUERY_FEEDS_REPSONSE_FEEDS_EXTRA = "feeds"; - - @Override - public void onReceive(Context context, Intent intent) { - if (!TextUtils.equals(intent.getAction(), ACTION_SP_APPS_QUERY_FEEDS_REPSONSE)) { - return; - } - Log.d(TAG, "Received SP_APPS_QUERY_RESPONSE"); - if (!intent.hasExtra(ACTION_SP_APPS_QUERY_FEEDS_REPSONSE_FEEDS_EXTRA)) { - Log.e(TAG, "Received invalid SP_APPS_QUERY_RESPONSE: Contains no extra"); - return; - } - String[] feedUrls = intent.getStringArrayExtra(ACTION_SP_APPS_QUERY_FEEDS_REPSONSE_FEEDS_EXTRA); - if (feedUrls == null) { - Log.e(TAG, "Received invalid SP_APPS_QUERY_REPSONSE: extra was null"); - return; - } - Log.d(TAG, "Received feeds list: " + Arrays.toString(feedUrls)); - ClientConfigurator.initialize(context); - for (String url : feedUrls) { - Feed feed = new Feed(url, null, "Unknown podcast"); - feed.setItems(Collections.emptyList()); - FeedDatabaseWriter.updateFeed(context, feed, false); - } - Toast.makeText(context, R.string.sp_apps_importing_feeds_msg, Toast.LENGTH_LONG).show(); - FeedUpdateManager.getInstance().runOnce(context); - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/spa/SPAReceiver.java b/app/src/main/java/de/danoeh/antennapod/spa/SPAReceiver.java new file mode 100644 index 000000000..e91192164 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/spa/SPAReceiver.java @@ -0,0 +1,54 @@ +package de.danoeh.antennapod.spa; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.text.TextUtils; +import android.util.Log; +import android.widget.Toast; + +import java.util.Arrays; +import java.util.Collections; + +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.ClientConfigurator; +import de.danoeh.antennapod.net.download.serviceinterface.FeedUpdateManager; +import de.danoeh.antennapod.storage.database.FeedDatabaseWriter; +import de.danoeh.antennapod.model.feed.Feed; + +/** + * Receives intents from AntennaPod Single Purpose apps + */ +public class SPAReceiver extends BroadcastReceiver{ + private static final String TAG = "SPAReceiver"; + + public static final String ACTION_SP_APPS_QUERY_FEEDS = "de.danoeh.antennapdsp.intent.SP_APPS_QUERY_FEEDS"; + private static final String ACTION_SP_APPS_QUERY_FEEDS_REPSONSE = "de.danoeh.antennapdsp.intent.SP_APPS_QUERY_FEEDS_RESPONSE"; + private static final String ACTION_SP_APPS_QUERY_FEEDS_REPSONSE_FEEDS_EXTRA = "feeds"; + + @Override + public void onReceive(Context context, Intent intent) { + if (!TextUtils.equals(intent.getAction(), ACTION_SP_APPS_QUERY_FEEDS_REPSONSE)) { + return; + } + Log.d(TAG, "Received SP_APPS_QUERY_RESPONSE"); + if (!intent.hasExtra(ACTION_SP_APPS_QUERY_FEEDS_REPSONSE_FEEDS_EXTRA)) { + Log.e(TAG, "Received invalid SP_APPS_QUERY_RESPONSE: Contains no extra"); + return; + } + String[] feedUrls = intent.getStringArrayExtra(ACTION_SP_APPS_QUERY_FEEDS_REPSONSE_FEEDS_EXTRA); + if (feedUrls == null) { + Log.e(TAG, "Received invalid SP_APPS_QUERY_REPSONSE: extra was null"); + return; + } + Log.d(TAG, "Received feeds list: " + Arrays.toString(feedUrls)); + ClientConfigurator.initialize(context); + for (String url : feedUrls) { + Feed feed = new Feed(url, null, "Unknown podcast"); + feed.setItems(Collections.emptyList()); + FeedDatabaseWriter.updateFeed(context, feed, false); + } + Toast.makeText(context, R.string.sp_apps_importing_feeds_msg, Toast.LENGTH_LONG).show(); + FeedUpdateManager.getInstance().runOnce(context); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/spa/SPAUtil.java b/app/src/main/java/de/danoeh/antennapod/spa/SPAUtil.java index f9c10041e..7630d49cd 100644 --- a/app/src/main/java/de/danoeh/antennapod/spa/SPAUtil.java +++ b/app/src/main/java/de/danoeh/antennapod/spa/SPAUtil.java @@ -7,7 +7,6 @@ import androidx.preference.PreferenceManager; import android.util.Log; import de.danoeh.antennapod.BuildConfig; -import de.danoeh.antennapod.receiver.SPAReceiver; /** * Provides methods related to AntennaPodSP (https://github.com/danieloeh/AntennaPodSP) diff --git a/app/src/main/java/de/danoeh/antennapod/ui/AllEpisodesFilterDialog.java b/app/src/main/java/de/danoeh/antennapod/ui/AllEpisodesFilterDialog.java new file mode 100644 index 000000000..d3464cd54 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/AllEpisodesFilterDialog.java @@ -0,0 +1,32 @@ +package de.danoeh.antennapod.ui; + +import android.os.Bundle; +import de.danoeh.antennapod.model.feed.FeedItemFilter; +import de.danoeh.antennapod.ui.screen.feed.ItemFilterDialog; +import org.greenrobot.eventbus.EventBus; + +import java.util.Set; + +public class AllEpisodesFilterDialog extends ItemFilterDialog { + + public static AllEpisodesFilterDialog newInstance(FeedItemFilter filter) { + AllEpisodesFilterDialog dialog = new AllEpisodesFilterDialog(); + Bundle arguments = new Bundle(); + arguments.putSerializable(ARGUMENT_FILTER, filter); + dialog.setArguments(arguments); + return dialog; + } + + @Override + public void onFilterChanged(Set newFilterValues) { + EventBus.getDefault().post(new AllEpisodesFilterChangedEvent(newFilterValues)); + } + + public static class AllEpisodesFilterChangedEvent { + public final Set filterValues; + + public AllEpisodesFilterChangedEvent(Set filterValues) { + this.filterValues = filterValues; + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/CoverLoader.java b/app/src/main/java/de/danoeh/antennapod/ui/CoverLoader.java new file mode 100644 index 000000000..370a15919 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/CoverLoader.java @@ -0,0 +1,132 @@ +package de.danoeh.antennapod.ui; + +import android.graphics.drawable.Drawable; +import android.view.View; +import android.widget.ImageView; +import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.bumptech.glide.Glide; +import com.bumptech.glide.RequestBuilder; +import com.bumptech.glide.request.RequestOptions; +import com.bumptech.glide.request.target.CustomViewTarget; +import com.bumptech.glide.request.transition.Transition; + +import java.lang.ref.WeakReference; + +public class CoverLoader { + private int resource = 0; + private String uri; + private String fallbackUri; + private ImageView imgvCover; + private boolean textAndImageCombined; + private TextView fallbackTitle; + + public CoverLoader() { + } + + public CoverLoader withUri(String uri) { + this.uri = uri; + return this; + } + + public CoverLoader withResource(int resource) { + this.resource = resource; + return this; + } + + public CoverLoader withFallbackUri(String uri) { + fallbackUri = uri; + return this; + } + + public CoverLoader withCoverView(ImageView coverView) { + imgvCover = coverView; + return this; + } + + public CoverLoader withPlaceholderView(TextView title) { + this.fallbackTitle = title; + return this; + } + + /** + * Set cover text and if it should be shown even if there is a cover image. + * @param fallbackTitle Fallback title text + * @param textAndImageCombined Show cover text even if there is a cover image? + */ + @NonNull + public CoverLoader withPlaceholderView(TextView fallbackTitle, boolean textAndImageCombined) { + this.fallbackTitle = fallbackTitle; + this.textAndImageCombined = textAndImageCombined; + return this; + } + + public void load() { + CoverTarget coverTarget = new CoverTarget(fallbackTitle, imgvCover, textAndImageCombined); + + if (resource != 0) { + Glide.with(imgvCover).clear(coverTarget); + imgvCover.setImageResource(resource); + CoverTarget.setTitleVisibility(fallbackTitle, textAndImageCombined); + return; + } + + RequestOptions options = new RequestOptions() + .fitCenter() + .dontAnimate(); + + RequestBuilder builder = Glide.with(imgvCover) + .as(Drawable.class) + .load(uri) + .apply(options); + + if (fallbackUri != null) { + builder = builder.error(Glide.with(imgvCover) + .as(Drawable.class) + .load(fallbackUri) + .apply(options)); + } + + builder.into(coverTarget); + } + + static class CoverTarget extends CustomViewTarget { + private final WeakReference fallbackTitle; + private final WeakReference cover; + private final boolean textAndImageCombined; + + public CoverTarget(TextView fallbackTitle, ImageView coverImage, boolean textAndImageCombined) { + super(coverImage); + this.fallbackTitle = new WeakReference<>(fallbackTitle); + this.cover = new WeakReference<>(coverImage); + this.textAndImageCombined = textAndImageCombined; + } + + @Override + public void onLoadFailed(Drawable errorDrawable) { + setTitleVisibility(fallbackTitle.get(), true); + } + + @Override + public void onResourceReady(@NonNull Drawable resource, + @Nullable Transition transition) { + ImageView ivCover = cover.get(); + ivCover.setImageDrawable(resource); + setTitleVisibility(fallbackTitle.get(), textAndImageCombined); + } + + @Override + protected void onResourceCleared(@Nullable Drawable placeholder) { + ImageView ivCover = cover.get(); + ivCover.setImageDrawable(placeholder); + setTitleVisibility(fallbackTitle.get(), textAndImageCombined); + } + + static void setTitleVisibility(TextView fallbackTitle, boolean textAndImageCombined) { + if (fallbackTitle != null) { + fallbackTitle.setVisibility(textAndImageCombined ? View.VISIBLE : View.GONE); + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/de/danoeh/antennapod/ui/FeedItemFilterDialog.java b/app/src/main/java/de/danoeh/antennapod/ui/FeedItemFilterDialog.java new file mode 100644 index 000000000..cce78013a --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/FeedItemFilterDialog.java @@ -0,0 +1,27 @@ +package de.danoeh.antennapod.ui; + +import android.os.Bundle; +import de.danoeh.antennapod.storage.database.DBWriter; +import de.danoeh.antennapod.model.feed.Feed; +import de.danoeh.antennapod.ui.screen.feed.ItemFilterDialog; + +import java.util.Set; + +public class FeedItemFilterDialog extends ItemFilterDialog { + private static final String ARGUMENT_FEED_ID = "feedId"; + + public static FeedItemFilterDialog newInstance(Feed feed) { + FeedItemFilterDialog dialog = new FeedItemFilterDialog(); + Bundle arguments = new Bundle(); + arguments.putSerializable(ARGUMENT_FILTER, feed.getItemFilter()); + arguments.putLong(ARGUMENT_FEED_ID, feed.getId()); + dialog.setArguments(arguments); + return dialog; + } + + @Override + public void onFilterChanged(Set newFilterValues) { + long feedId = getArguments().getLong(ARGUMENT_FEED_ID); + DBWriter.setFeedItemsFilter(feedId, newFilterValues); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/MenuItemUtils.java b/app/src/main/java/de/danoeh/antennapod/ui/MenuItemUtils.java new file mode 100644 index 000000000..f9e8f0568 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/MenuItemUtils.java @@ -0,0 +1,28 @@ +package de.danoeh.antennapod.ui; + +import android.view.Menu; +import android.view.MenuItem; + +/** + * Utilities for menu items + */ +public class MenuItemUtils { + + /** + * When pressing a context menu item, Android calls onContextItemSelected + * for ALL fragments in arbitrary order, not just for the fragment that the + * context menu was created from. This assigns the listener to every menu item, + * so that the correct fragment is always called first and can consume the click. + *

+ * Note that Android still calls the onContextItemSelected methods of all fragments + * when the passed listener returns false. + */ + public static void setOnClickListeners(Menu menu, MenuItem.OnMenuItemClickListener listener) { + for (int i = 0; i < menu.size(); i++) { + if (menu.getItem(i).getSubMenu() != null) { + setOnClickListeners(menu.getItem(i).getSubMenu(), listener); + } + menu.getItem(i).setOnMenuItemClickListener(listener); + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/SelectableAdapter.java b/app/src/main/java/de/danoeh/antennapod/ui/SelectableAdapter.java new file mode 100644 index 000000000..918562ec3 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/SelectableAdapter.java @@ -0,0 +1,200 @@ +package de.danoeh.antennapod.ui; + +import android.app.Activity; +import android.view.ActionMode; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; + +import androidx.recyclerview.widget.RecyclerView; + +import de.danoeh.antennapod.R; + +import java.util.HashSet; + +/** + * Used by Recyclerviews that need to provide ability to select items. + */ +public abstract class SelectableAdapter extends RecyclerView.Adapter { + public static final int COUNT_AUTOMATICALLY = -1; + private ActionMode actionMode; + private final HashSet selectedIds = new HashSet<>(); + private final Activity activity; + private OnSelectModeListener onSelectModeListener; + protected boolean shouldSelectLazyLoadedItems = false; + private int totalNumberOfItems = COUNT_AUTOMATICALLY; + + public SelectableAdapter(Activity activity) { + this.activity = activity; + } + + public void startSelectMode(int pos) { + if (inActionMode()) { + endSelectMode(); + } + + if (onSelectModeListener != null) { + onSelectModeListener.onStartSelectMode(); + } + + shouldSelectLazyLoadedItems = false; + selectedIds.clear(); + selectedIds.add(getItemId(pos)); + notifyDataSetChanged(); + + actionMode = activity.startActionMode(new ActionMode.Callback() { + @Override + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + MenuInflater inflater = mode.getMenuInflater(); + inflater.inflate(R.menu.multi_select_options, menu); + return true; + } + + @Override + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + updateTitle(); + toggleSelectAllIcon(menu.findItem(R.id.select_toggle), false); + return false; + } + + @Override + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + if (item.getItemId() == R.id.select_toggle) { + boolean selectAll = selectedIds.size() != getItemCount(); + shouldSelectLazyLoadedItems = selectAll; + setSelected(0, getItemCount(), selectAll); + toggleSelectAllIcon(item, selectAll); + updateTitle(); + return true; + } + return false; + } + + @Override + public void onDestroyActionMode(ActionMode mode) { + callOnEndSelectMode(); + actionMode = null; + shouldSelectLazyLoadedItems = false; + selectedIds.clear(); + notifyDataSetChanged(); + } + }); + updateTitle(); + } + + /** + * End action mode if currently in select mode, otherwise do nothing + */ + public void endSelectMode() { + if (inActionMode()) { + callOnEndSelectMode(); + actionMode.finish(); + } + } + + public boolean isSelected(int pos) { + return selectedIds.contains(getItemId(pos)); + } + + /** + * Set the selected state of item at given position + * + * @param pos the position to select + * @param selected true for selected state and false for unselected + */ + public void setSelected(int pos, boolean selected) { + if (selected) { + selectedIds.add(getItemId(pos)); + } else { + selectedIds.remove(getItemId(pos)); + } + updateTitle(); + } + + /** + * Set the selected state of item for a given range + * + * @param startPos start position of range, inclusive + * @param endPos end position of range, inclusive + * @param selected indicates the selection state + * @throws IllegalArgumentException if start and end positions are not valid + */ + public void setSelected(int startPos, int endPos, boolean selected) throws IllegalArgumentException { + for (int i = startPos; i < endPos && i < getItemCount(); i++) { + setSelected(i, selected); + } + notifyItemRangeChanged(startPos, (endPos - startPos)); + } + + protected void toggleSelection(int pos) { + setSelected(pos, !isSelected(pos)); + notifyItemChanged(pos); + + if (selectedIds.size() == 0) { + endSelectMode(); + } + } + + public boolean inActionMode() { + return actionMode != null; + } + + public int getSelectedCount() { + return selectedIds.size(); + } + + private void toggleSelectAllIcon(MenuItem selectAllItem, boolean allSelected) { + if (allSelected) { + selectAllItem.setIcon(R.drawable.ic_select_none); + selectAllItem.setTitle(R.string.deselect_all_label); + } else { + selectAllItem.setIcon(R.drawable.ic_select_all); + selectAllItem.setTitle(R.string.select_all_label); + } + } + + protected void updateTitle() { + if (actionMode == null) { + return; + } + int totalCount = getItemCount(); + int selectedCount = selectedIds.size(); + if (totalNumberOfItems != COUNT_AUTOMATICALLY) { + totalCount = totalNumberOfItems; + if (shouldSelectLazyLoadedItems) { + selectedCount += (totalNumberOfItems - getItemCount()); + } + } + actionMode.setTitle(activity.getResources() + .getQuantityString(R.plurals.num_selected_label, selectedIds.size(), + selectedCount, totalCount)); + } + + public void setOnSelectModeListener(OnSelectModeListener onSelectModeListener) { + this.onSelectModeListener = onSelectModeListener; + } + + private void callOnEndSelectMode() { + if (onSelectModeListener != null) { + onSelectModeListener.onEndSelectMode(); + } + } + + public boolean shouldSelectLazyLoadedItems() { + return shouldSelectLazyLoadedItems; + } + + /** + * Sets the total number of items that could be lazy-loaded. + * Can also be set to {@link #COUNT_AUTOMATICALLY} to simply use {@link #getItemCount} + */ + public void setTotalNumberOfItems(int totalNumberOfItems) { + this.totalNumberOfItems = totalNumberOfItems; + } + + public interface OnSelectModeListener { + void onStartSelectMode(); + + void onEndSelectMode(); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/SimpleChipAdapter.java b/app/src/main/java/de/danoeh/antennapod/ui/SimpleChipAdapter.java new file mode 100644 index 000000000..904d01883 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/SimpleChipAdapter.java @@ -0,0 +1,57 @@ +package de.danoeh.antennapod.ui; + +import android.content.Context; +import android.view.ViewGroup; +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; +import com.google.android.material.chip.Chip; +import de.danoeh.antennapod.R; + +import java.util.List; + +public abstract class SimpleChipAdapter extends RecyclerView.Adapter { + private final Context context; + + public SimpleChipAdapter(Context context) { + this.context = context; + setHasStableIds(true); + } + + protected abstract List getChips(); + + protected abstract void onRemoveClicked(int position); + + @Override + @NonNull + public SimpleChipAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + Chip chip = new Chip(context); + chip.setCloseIconVisible(true); + chip.setCloseIconResource(R.drawable.ic_delete); + return new SimpleChipAdapter.ViewHolder(chip); + } + + @Override + public void onBindViewHolder(@NonNull SimpleChipAdapter.ViewHolder holder, int position) { + holder.chip.setText(getChips().get(position)); + holder.chip.setOnCloseIconClickListener(v -> onRemoveClicked(position)); + } + + @Override + public int getItemCount() { + return getChips().size(); + } + + @Override + public long getItemId(int position) { + return getChips().get(position).hashCode(); + } + + public static class ViewHolder extends RecyclerView.ViewHolder { + Chip chip; + + ViewHolder(Chip itemView) { + super(itemView); + chip = itemView; + } + } +} \ No newline at end of file diff --git a/app/src/main/java/de/danoeh/antennapod/ui/StreamingConfirmationDialog.java b/app/src/main/java/de/danoeh/antennapod/ui/StreamingConfirmationDialog.java new file mode 100644 index 000000000..6199aa0fd --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/StreamingConfirmationDialog.java @@ -0,0 +1,38 @@ +package de.danoeh.antennapod.ui; + +import android.content.Context; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.playback.service.PlaybackServiceStarter; +import de.danoeh.antennapod.storage.preferences.UserPreferences; +import de.danoeh.antennapod.model.playback.Playable; + +public class StreamingConfirmationDialog { + private final Context context; + private final Playable playable; + + public StreamingConfirmationDialog(Context context, Playable playable) { + this.context = context; + this.playable = playable; + } + + public void show() { + new MaterialAlertDialogBuilder(context) + .setTitle(R.string.stream_label) + .setMessage(R.string.confirm_mobile_streaming_notification_message) + .setPositiveButton(R.string.confirm_mobile_streaming_button_once, (dialog, which) -> stream()) + .setNegativeButton(R.string.confirm_mobile_streaming_button_always, (dialog, which) -> { + UserPreferences.setAllowMobileStreaming(true); + stream(); + }) + .setNeutralButton(R.string.cancel_label, null) + .show(); + } + + private void stream() { + new PlaybackServiceStarter(context, playable) + .callEvenIfRunning(true) + .shouldStreamThisTime(true) + .start(); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/TransitionEffect.java b/app/src/main/java/de/danoeh/antennapod/ui/TransitionEffect.java new file mode 100644 index 000000000..092aeb3a5 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/TransitionEffect.java @@ -0,0 +1,5 @@ +package de.danoeh.antennapod.ui; + +public enum TransitionEffect { + NONE, FADE, SLIDE +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/cleaner/HtmlToPlainText.java b/app/src/main/java/de/danoeh/antennapod/ui/cleaner/HtmlToPlainText.java new file mode 100644 index 000000000..29099c1fd --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/cleaner/HtmlToPlainText.java @@ -0,0 +1,123 @@ +package de.danoeh.antennapod.ui.cleaner; + +import android.text.TextUtils; + +import org.apache.commons.lang3.StringUtils; +import org.jsoup.Jsoup; +import org.jsoup.internal.StringUtil; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.jsoup.nodes.Node; +import org.jsoup.nodes.TextNode; +import org.jsoup.select.NodeTraversor; +import org.jsoup.select.NodeVisitor; + +import java.util.regex.Pattern; + +/** + * This class is based on HtmlToPlainText from jsoup's examples package. + * + * HTML to plain-text. This example program demonstrates the use of jsoup to convert HTML input to lightly-formatted + * plain-text. That is divergent from the general goal of jsoup's .text() methods, which is to get clean data from a + * scrape. + *

+ * Note that this is a fairly simplistic formatter -- for real world use you'll want to embrace and extend. + *

+ *

+ * To invoke from the command line, assuming you've downloaded the jsoup jar to your current directory:

+ *

java -cp jsoup.jar org.jsoup.examples.HtmlToPlainText url [selector]

+ * where url is the URL to fetch, and selector is an optional CSS selector. + * + * @author Jonathan Hedley, jonathan@hedley.net + * @author AntennaPod open source community + */ +public class HtmlToPlainText { + + /** + * Use this method to strip off HTML encoding from given text. + * Replaces bullet points with *, ignores colors/bold/... + * + * @param str String with any encoding + * @return Human readable text with minimal HTML formatting + */ + public static String getPlainText(String str) { + if (!TextUtils.isEmpty(str) && isHtml(str)) { + HtmlToPlainText formatter = new HtmlToPlainText(); + Document feedDescription = Jsoup.parse(str); + str = StringUtils.trim(formatter.getPlainText(feedDescription)); + } else if (TextUtils.isEmpty(str)) { + str = ""; + } + + return str; + } + + /** + * Use this method to determine if a given text has any HTML tag + * + * @param str String to be tested for presence of HTML content + * @return True if text contains any HTML tags
False is no HTML tag is found + */ + private static boolean isHtml(String str) { + final String htmlTagPattern = "<(\"[^\"]*\"|'[^']*'|[^'\">])*>"; + return Pattern.compile(htmlTagPattern).matcher(str).find(); + } + + /** + * Format an Element to plain-text + * @param element the root element to format + * @return formatted text + */ + public String getPlainText(Element element) { + FormattingVisitor formatter = new FormattingVisitor(); + // walk the DOM, and call .head() and .tail() for each node + NodeTraversor.traverse(formatter, element); + + return formatter.toString(); + } + + // the formatting rules, implemented in a breadth-first DOM traverse + private static class FormattingVisitor implements NodeVisitor { + + private final StringBuilder accum = new StringBuilder(); // holds the accumulated text + + // hit when the node is first seen + public void head(Node node, int depth) { + String name = node.nodeName(); + if (node instanceof TextNode) { + append(((TextNode) node).text()); // TextNodes carry all user-readable text in the DOM. + } else if (name.equals("li")) { + append("\n * "); + } else if (name.equals("dt")) { + append(" "); + } else if (StringUtil.in(name, "p", "h1", "h2", "h3", "h4", "h5", "tr")) { + append("\n"); + } + } + + // hit when all of the node's children (if any) have been visited + public void tail(Node node, int depth) { + String name = node.nodeName(); + if (StringUtil.in(name, "br", "dd", "dt", "p", "h1", "h2", "h3", "h4", "h5")) { + append("\n"); + } else if (name.equals("a")) { + append(String.format(" <%s>", node.absUrl("href"))); + } + } + + // appends text to the string builder with a simple word wrap method + private void append(String text) { + if (text.equals(" ") && + (accum.length() == 0 || StringUtil.in(accum.substring(accum.length() - 1), " ", "\n"))) { + return; // don't accumulate long runs of empty spaces + } + + accum.append(text); + } + + @Override + public String toString() { + return accum.toString(); + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/cleaner/ShownotesCleaner.java b/app/src/main/java/de/danoeh/antennapod/ui/cleaner/ShownotesCleaner.java new file mode 100644 index 000000000..57f91e35c --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/cleaner/ShownotesCleaner.java @@ -0,0 +1,208 @@ +package de.danoeh.antennapod.ui.cleaner; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Color; +import androidx.annotation.ColorInt; +import androidx.annotation.NonNull; +import android.text.TextUtils; +import android.util.Log; +import android.util.TypedValue; + +import androidx.annotation.Nullable; +import org.apache.commons.io.IOUtils; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.jsoup.select.Elements; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import de.danoeh.antennapod.core.R; +import de.danoeh.antennapod.ui.common.Converter; + +/** + * Cleans up and prepares shownotes: + * - Guesses time stamps to make them clickable + * - Removes some formatting + */ +public class ShownotesCleaner { + private static final String TAG = "Timeline"; + + 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((\\d+):)?(\\d+):(\\d{2})\\b"); + private static final Pattern LINE_BREAK_REGEX = Pattern.compile("
"); + private static final String CSS_COLOR = "(?<=(\\s|;|^))color\\s*:([^;])*;"; + private static final String CSS_COMMENT = "/\\*.*?\\*/"; + + private final String rawShownotes; + private final String noShownotesLabel; + private final int playableDuration; + private final String webviewStyle; + + public ShownotesCleaner(Context context, @Nullable String rawShownotes, int playableDuration) { + this.rawShownotes = rawShownotes; + + noShownotesLabel = context.getString(R.string.no_shownotes_label); + this.playableDuration = playableDuration; + final String colorPrimary = colorToHtml(context, android.R.attr.textColorPrimary); + final String colorAccent = colorToHtml(context, R.attr.colorAccent); + final int margin = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8, + context.getResources().getDisplayMetrics()); + String styleString = ""; + try { + InputStream templateStream = context.getAssets().open("shownotes-style.css"); + styleString = IOUtils.toString(templateStream, "UTF-8"); + } catch (IOException e) { + e.printStackTrace(); + } + webviewStyle = String.format(Locale.US, styleString, colorPrimary, colorAccent, + margin, margin, margin, margin); + } + + private String colorToHtml(Context context, int colorAttr) { + TypedArray res = context.getTheme().obtainStyledAttributes(new int[]{colorAttr}); + @ColorInt int col = res.getColor(0, 0); + final String color = "rgba(" + Color.red(col) + "," + Color.green(col) + "," + + Color.blue(col) + "," + (Color.alpha(col) / 255.0) + ")"; + res.recycle(); + return color; + } + + /** + * 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. + * + * @return The processed HTML string. + */ + @NonNull + public String processShownotes() { + String shownotes = rawShownotes; + + if (TextUtils.isEmpty(shownotes)) { + Log.d(TAG, "shownotesProvider contained no shownotes. Returning 'no shownotes' message"); + shownotes = "

" + noShownotesLabel + "

"; + } + + // replace ASCII line breaks with HTML ones if shownotes don't contain HTML line breaks already + if (!LINE_BREAK_REGEX.matcher(shownotes).find() && !shownotes.contains("

")) { + shownotes = shownotes.replace("\n", "
"); + } + + Document document = Jsoup.parse(shownotes); + cleanCss(document); + document.head().appendElement("style").attr("type", "text/css").text(webviewStyle); + addTimecodes(document); + 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.parseInt(m.group(1)); + } + } catch (NumberFormatException e) { + e.printStackTrace(); + } + } + return -1; + } + + private void addTimecodes(Document document) { + Elements elementsWithTimeCodes = document.body().getElementsMatchingOwnText(TIMECODE_REGEX); + Log.d(TAG, "Recognized " + elementsWithTimeCodes.size() + " timecodes"); + + if (elementsWithTimeCodes.size() == 0) { + // No elements with timecodes + return; + } + boolean useHourFormat = true; + + if (playableDuration != Integer.MAX_VALUE) { + + // We need to decide if we are going to treat short timecodes as HH:MM or MM:SS. To do + // so we will parse all the short timecodes and see if they fit in the duration. If one + // does not we will use MM:SS, otherwise all will be parsed as HH:MM. + for (Element element : elementsWithTimeCodes) { + Matcher matcherForElement = TIMECODE_REGEX.matcher(element.html()); + while (matcherForElement.find()) { + + // We only want short timecodes right now. + if (matcherForElement.group(1) == null) { + int time = Converter.durationStringShortToMs(matcherForElement.group(0), true); + + // If the parsed timecode is greater then the duration then we know we need to + // use the minute format so we are done. + if (time > playableDuration) { + useHourFormat = false; + break; + } + } + } + + if (!useHourFormat) { + break; + } + } + } + + for (Element element : elementsWithTimeCodes) { + + Matcher matcherForElement = TIMECODE_REGEX.matcher(element.html()); + StringBuffer buffer = new StringBuffer(); + + while (matcherForElement.find()) { + String group = matcherForElement.group(0); + + int time = matcherForElement.group(1) != null + ? Converter.durationStringLongToMs(group) + : Converter.durationStringShortToMs(group, useHourFormat); + + String replacementText = group; + if (time < playableDuration) { + replacementText = String.format(Locale.US, TIMECODE_LINK, time, group); + } + + matcherForElement.appendReplacement(buffer, replacementText); + } + + matcherForElement.appendTail(buffer); + element.html(buffer.toString()); + } + } + + private void cleanCss(Document document) { + for (Element element : document.getAllElements()) { + if (element.hasAttr("style")) { + element.attr("style", element.attr("style").replaceAll(CSS_COLOR, "")); + } else if (element.tagName().equals("style")) { + element.html(cleanStyleTag(element.html())); + } + } + } + + public static String cleanStyleTag(String oldCss) { + return oldCss.replaceAll(CSS_COMMENT, "").replaceAll(CSS_COLOR, ""); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/episodeslist/EpisodeItemListAdapter.java b/app/src/main/java/de/danoeh/antennapod/ui/episodeslist/EpisodeItemListAdapter.java new file mode 100644 index 000000000..cb026240a --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/episodeslist/EpisodeItemListAdapter.java @@ -0,0 +1,233 @@ +package de.danoeh.antennapod.ui.episodeslist; + +import android.app.Activity; +import android.os.Build; +import android.view.ContextMenu; +import android.view.InputDevice; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.RecyclerView; + +import de.danoeh.antennapod.ui.SelectableAdapter; +import de.danoeh.antennapod.ui.common.ThemeUtils; +import org.apache.commons.lang3.ArrayUtils; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.List; + +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.core.util.FeedItemUtil; +import de.danoeh.antennapod.ui.screen.episode.ItemPagerFragment; + +/** + * List adapter for the list of new episodes. + */ +public class EpisodeItemListAdapter extends SelectableAdapter + implements View.OnCreateContextMenuListener { + + private final WeakReference mainActivityRef; + private List episodes = new ArrayList<>(); + private FeedItem longPressedItem; + int longPressedPosition = 0; // used to init actionMode + private int dummyViews = 0; + + public EpisodeItemListAdapter(MainActivity mainActivity) { + super(mainActivity); + this.mainActivityRef = new WeakReference<>(mainActivity); + setHasStableIds(true); + } + + public void setDummyViews(int dummyViews) { + this.dummyViews = dummyViews; + notifyDataSetChanged(); + } + + public void updateItems(List items) { + episodes = items; + notifyDataSetChanged(); + updateTitle(); + } + + @Override + public final int getItemViewType(int position) { + return R.id.view_type_episode_item; + } + + @NonNull + @Override + public final EpisodeItemViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + return new EpisodeItemViewHolder(mainActivityRef.get(), parent); + } + + @Override + public final void onBindViewHolder(EpisodeItemViewHolder holder, int pos) { + if (pos >= episodes.size()) { + beforeBindViewHolder(holder, pos); + holder.bindDummy(); + afterBindViewHolder(holder, pos); + holder.hideSeparatorIfNecessary(); + return; + } + + // Reset state of recycled views + holder.coverHolder.setVisibility(View.VISIBLE); + holder.dragHandle.setVisibility(View.GONE); + + beforeBindViewHolder(holder, pos); + + FeedItem item = episodes.get(pos); + holder.bind(item); + + holder.itemView.setOnClickListener(v -> { + MainActivity activity = mainActivityRef.get(); + if (activity != null && !inActionMode()) { + long[] ids = FeedItemUtil.getIds(episodes); + int position = ArrayUtils.indexOf(ids, item.getId()); + activity.loadChildFragment(ItemPagerFragment.newInstance(ids, position)); + } else { + toggleSelection(holder.getBindingAdapterPosition()); + } + }); + holder.itemView.setOnCreateContextMenuListener(this); + holder.itemView.setOnLongClickListener(v -> { + longPressedItem = item; + longPressedPosition = holder.getBindingAdapterPosition(); + return false; + }); + holder.itemView.setOnTouchListener((v, e) -> { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (e.isFromSource(InputDevice.SOURCE_MOUSE) + && e.getButtonState() == MotionEvent.BUTTON_SECONDARY) { + longPressedItem = item; + longPressedPosition = holder.getBindingAdapterPosition(); + return false; + } + } + return false; + }); + + if (inActionMode()) { + holder.secondaryActionButton.setOnClickListener(null); + if (isSelected(pos)) { + holder.itemView.setBackgroundColor(0x88000000 + + (0xffffff & ThemeUtils.getColorFromAttr(mainActivityRef.get(), R.attr.colorAccent))); + } else { + holder.itemView.setBackgroundResource(android.R.color.transparent); + } + } + + afterBindViewHolder(holder, pos); + holder.hideSeparatorIfNecessary(); + } + + protected void beforeBindViewHolder(EpisodeItemViewHolder holder, int pos) { + } + + protected void afterBindViewHolder(EpisodeItemViewHolder holder, int pos) { + } + + @Override + public void onViewRecycled(@NonNull EpisodeItemViewHolder holder) { + super.onViewRecycled(holder); + // Set all listeners to null. This is required to prevent leaking fragments that have set a listener. + // Activity -> recycledViewPool -> EpisodeItemViewHolder -> Listener -> Fragment (can not be garbage collected) + holder.itemView.setOnClickListener(null); + holder.itemView.setOnCreateContextMenuListener(null); + holder.itemView.setOnLongClickListener(null); + holder.itemView.setOnTouchListener(null); + holder.secondaryActionButton.setOnClickListener(null); + holder.dragHandle.setOnTouchListener(null); + holder.coverHolder.setOnTouchListener(null); + } + + /** + * {@link #notifyItemChanged(int)} is final, so we can not override. + * Calling {@link #notifyItemChanged(int)} may bind the item to a new ViewHolder and execute a transition. + * This causes flickering and breaks the download animation that stores the old progress in the View. + * Instead, we tell the adapter to use partial binding by calling {@link #notifyItemChanged(int, Object)}. + * We actually ignore the payload and always do a full bind but calling the partial bind method ensures + * that ViewHolders are always re-used. + * + * @param position Position of the item that has changed + */ + public void notifyItemChangedCompat(int position) { + notifyItemChanged(position, "foo"); + } + + @Nullable + public FeedItem getLongPressedItem() { + return longPressedItem; + } + + @Override + public long getItemId(int position) { + if (position >= episodes.size()) { + return RecyclerView.NO_ID; // Dummy views + } + FeedItem item = episodes.get(position); + return item != null ? item.getId() : RecyclerView.NO_POSITION; + } + + @Override + public int getItemCount() { + return dummyViews + episodes.size(); + } + + protected FeedItem getItem(int index) { + return episodes.get(index); + } + + protected Activity getActivity() { + return mainActivityRef.get(); + } + + @Override + public void onCreateContextMenu(final ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { + MenuInflater inflater = mainActivityRef.get().getMenuInflater(); + if (inActionMode()) { + inflater.inflate(R.menu.multi_select_context_popup, menu); + } else { + if (longPressedItem == null) { + return; + } + inflater.inflate(R.menu.feeditemlist_context, menu); + menu.setHeaderTitle(longPressedItem.getTitle()); + FeedItemMenuHandler.onPrepareMenu(menu, longPressedItem, R.id.skip_episode_item); + } + } + + public boolean onContextItemSelected(MenuItem item) { + if (item.getItemId() == R.id.multi_select) { + startSelectMode(longPressedPosition); + return true; + } else if (item.getItemId() == R.id.select_all_above) { + setSelected(0, longPressedPosition, true); + return true; + } else if (item.getItemId() == R.id.select_all_below) { + shouldSelectLazyLoadedItems = true; + setSelected(longPressedPosition + 1, getItemCount(), true); + return true; + } + return false; + } + + public List getSelectedItems() { + List items = new ArrayList<>(); + for (int i = 0; i < getItemCount(); i++) { + if (isSelected(i)) { + items.add(getItem(i)); + } + } + return items; + } + +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/episodeslist/EpisodeItemListRecyclerView.java b/app/src/main/java/de/danoeh/antennapod/ui/episodeslist/EpisodeItemListRecyclerView.java new file mode 100644 index 000000000..a4f33ec98 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/episodeslist/EpisodeItemListRecyclerView.java @@ -0,0 +1,84 @@ +package de.danoeh.antennapod.ui.episodeslist; + +import android.content.Context; +import android.content.SharedPreferences; +import android.content.res.Configuration; +import android.util.AttributeSet; +import android.view.View; +import androidx.appcompat.view.ContextThemeWrapper; +import androidx.recyclerview.widget.DividerItemDecoration; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import de.danoeh.antennapod.R; +import io.reactivex.annotations.Nullable; + +public class EpisodeItemListRecyclerView extends RecyclerView { + private static final String TAG = "EpisodeItemListRecyclerView"; + private static final String PREF_PREFIX_SCROLL_POSITION = "scroll_position_"; + private static final String PREF_PREFIX_SCROLL_OFFSET = "scroll_offset_"; + + private LinearLayoutManager layoutManager; + + public EpisodeItemListRecyclerView(Context context) { + super(new ContextThemeWrapper(context, R.style.FastScrollRecyclerView)); + setup(); + } + + public EpisodeItemListRecyclerView(Context context, @Nullable AttributeSet attrs) { + super(new ContextThemeWrapper(context, R.style.FastScrollRecyclerView), attrs); + setup(); + } + + public EpisodeItemListRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(new ContextThemeWrapper(context, R.style.FastScrollRecyclerView), attrs, defStyleAttr); + setup(); + } + + private void setup() { + layoutManager = new LinearLayoutManager(getContext()); + layoutManager.setRecycleChildrenOnDetach(true); + setLayoutManager(layoutManager); + setHasFixedSize(true); + addItemDecoration(new DividerItemDecoration(getContext(), layoutManager.getOrientation())); + setClipToPadding(false); + } + + @Override + protected void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + int horizontalSpacing = (int) getResources().getDimension(R.dimen.additional_horizontal_spacing); + setPadding(horizontalSpacing, getPaddingTop(), horizontalSpacing, getPaddingBottom()); + } + + public void saveScrollPosition(String tag) { + int firstItem = layoutManager.findFirstVisibleItemPosition(); + View firstItemView = layoutManager.findViewByPosition(firstItem); + float topOffset; + if (firstItemView == null) { + topOffset = 0; + } else { + topOffset = firstItemView.getTop(); + } + + getContext().getSharedPreferences(TAG, Context.MODE_PRIVATE).edit() + .putInt(PREF_PREFIX_SCROLL_POSITION + tag, firstItem) + .putInt(PREF_PREFIX_SCROLL_OFFSET + tag, (int) topOffset) + .apply(); + } + + public void restoreScrollPosition(String tag) { + SharedPreferences prefs = getContext().getSharedPreferences(TAG, Context.MODE_PRIVATE); + int position = prefs.getInt(PREF_PREFIX_SCROLL_POSITION + tag, 0); + int offset = prefs.getInt(PREF_PREFIX_SCROLL_OFFSET + tag, 0); + if (position > 0 || offset > 0) { + layoutManager.scrollToPositionWithOffset(position, offset); + } + } + + public boolean isScrolledToBottom() { + int visibleEpisodeCount = getChildCount(); + int totalEpisodeCount = layoutManager.getItemCount(); + int firstVisibleEpisode = layoutManager.findFirstVisibleItemPosition(); + return (totalEpisodeCount - visibleEpisodeCount) <= (firstVisibleEpisode + 3); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/episodeslist/EpisodeItemViewHolder.java b/app/src/main/java/de/danoeh/antennapod/ui/episodeslist/EpisodeItemViewHolder.java new file mode 100644 index 000000000..2d9cf7004 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/episodeslist/EpisodeItemViewHolder.java @@ -0,0 +1,279 @@ +package de.danoeh.antennapod.ui.episodeslist; + +import android.os.Build; +import android.text.Layout; +import android.text.format.Formatter; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.ProgressBar; +import android.widget.TextView; + +import androidx.cardview.widget.CardView; +import androidx.recyclerview.widget.RecyclerView; + +import com.google.android.material.elevation.SurfaceColors; + +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.ui.CoverLoader; +import de.danoeh.antennapod.actionbutton.ItemActionButton; +import de.danoeh.antennapod.playback.service.PlaybackStatus; +import de.danoeh.antennapod.event.playback.PlaybackPositionEvent; +import de.danoeh.antennapod.ui.common.DateFormatter; +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedMedia; +import de.danoeh.antennapod.model.playback.MediaType; +import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterface; +import de.danoeh.antennapod.storage.preferences.UserPreferences; +import de.danoeh.antennapod.ui.common.Converter; +import de.danoeh.antennapod.net.common.NetworkUtils; +import de.danoeh.antennapod.model.playback.Playable; +import de.danoeh.antennapod.ui.common.CircularProgressBar; +import de.danoeh.antennapod.ui.common.ThemeUtils; +import de.danoeh.antennapod.ui.episodes.ImageResourceUtils; + +/** + * Holds the view which shows FeedItems. + */ +public class EpisodeItemViewHolder extends RecyclerView.ViewHolder { + private static final String TAG = "EpisodeItemViewHolder"; + + private final View container; + public final ImageView dragHandle; + private final TextView placeholder; + private final ImageView cover; + private final TextView title; + private final TextView pubDate; + private final TextView position; + private final TextView duration; + private final TextView size; + public final ImageView isInbox; + public final ImageView isInQueue; + private final ImageView isVideo; + public final ImageView isFavorite; + private final ProgressBar progressBar; + public final View secondaryActionButton; + public final ImageView secondaryActionIcon; + private final CircularProgressBar secondaryActionProgress; + private final TextView separatorIcons; + private final View leftPadding; + public final CardView coverHolder; + + private final MainActivity activity; + private FeedItem item; + + public EpisodeItemViewHolder(MainActivity activity, ViewGroup parent) { + super(LayoutInflater.from(activity).inflate(R.layout.feeditemlist_item, parent, false)); + this.activity = activity; + container = itemView.findViewById(R.id.container); + dragHandle = itemView.findViewById(R.id.drag_handle); + placeholder = itemView.findViewById(R.id.txtvPlaceholder); + cover = itemView.findViewById(R.id.imgvCover); + title = itemView.findViewById(R.id.txtvTitle); + if (Build.VERSION.SDK_INT >= 23) { + title.setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_FULL); + } + pubDate = itemView.findViewById(R.id.txtvPubDate); + position = itemView.findViewById(R.id.txtvPosition); + duration = itemView.findViewById(R.id.txtvDuration); + progressBar = itemView.findViewById(R.id.progressBar); + isInQueue = itemView.findViewById(R.id.ivInPlaylist); + isVideo = itemView.findViewById(R.id.ivIsVideo); + isInbox = itemView.findViewById(R.id.statusInbox); + isFavorite = itemView.findViewById(R.id.isFavorite); + size = itemView.findViewById(R.id.size); + separatorIcons = itemView.findViewById(R.id.separatorIcons); + secondaryActionProgress = itemView.findViewById(R.id.secondaryActionProgress); + secondaryActionButton = itemView.findViewById(R.id.secondaryActionButton); + secondaryActionIcon = itemView.findViewById(R.id.secondaryActionIcon); + coverHolder = itemView.findViewById(R.id.coverHolder); + leftPadding = itemView.findViewById(R.id.left_padding); + itemView.setTag(this); + } + + public void bind(FeedItem item) { + this.item = item; + placeholder.setText(item.getFeed().getTitle()); + title.setText(item.getTitle()); + if (item.isPlayed()) { + leftPadding.setContentDescription(item.getTitle() + ". " + activity.getString(R.string.is_played)); + } else { + leftPadding.setContentDescription(item.getTitle()); + } + pubDate.setText(DateFormatter.formatAbbrev(activity, item.getPubDate())); + pubDate.setContentDescription(DateFormatter.formatForAccessibility(item.getPubDate())); + isInbox.setVisibility(item.isNew() ? View.VISIBLE : View.GONE); + isFavorite.setVisibility(item.isTagged(FeedItem.TAG_FAVORITE) ? View.VISIBLE : View.GONE); + isInQueue.setVisibility(item.isTagged(FeedItem.TAG_QUEUE) ? View.VISIBLE : View.GONE); + container.setAlpha(item.isPlayed() ? 0.5f : 1.0f); + + ItemActionButton actionButton = ItemActionButton.forItem(item); + actionButton.configure(secondaryActionButton, secondaryActionIcon, activity); + secondaryActionButton.setFocusable(false); + + if (item.getMedia() != null) { + bind(item.getMedia()); + } else { + secondaryActionProgress.setPercentage(0, item); + secondaryActionProgress.setIndeterminate(false); + isVideo.setVisibility(View.GONE); + progressBar.setVisibility(View.GONE); + duration.setVisibility(View.GONE); + position.setVisibility(View.GONE); + itemView.setBackgroundResource(ThemeUtils.getDrawableFromAttr(activity, R.attr.selectableItemBackground)); + } + + if (coverHolder.getVisibility() == View.VISIBLE) { + new CoverLoader() + .withUri(ImageResourceUtils.getEpisodeListImageLocation(item)) + .withFallbackUri(item.getFeed().getImageUrl()) + .withPlaceholderView(placeholder) + .withCoverView(cover) + .load(); + } + } + + private void bind(FeedMedia media) { + isVideo.setVisibility(media.getMediaType() == MediaType.VIDEO ? View.VISIBLE : View.GONE); + duration.setVisibility(media.getDuration() > 0 ? View.VISIBLE : View.GONE); + + if (PlaybackStatus.isCurrentlyPlaying(media)) { + float density = activity.getResources().getDisplayMetrics().density; + itemView.setBackgroundColor(SurfaceColors.getColorForElevation(activity, 8 * density)); + } else { + itemView.setBackgroundResource(ThemeUtils.getDrawableFromAttr(activity, R.attr.selectableItemBackground)); + } + + if (DownloadServiceInterface.get().isDownloadingEpisode(media.getDownloadUrl())) { + float percent = 0.01f * DownloadServiceInterface.get().getProgress(media.getDownloadUrl()); + secondaryActionProgress.setPercentage(Math.max(percent, 0.01f), item); + secondaryActionProgress.setIndeterminate( + DownloadServiceInterface.get().isEpisodeQueued(media.getDownloadUrl())); + } else if (media.isDownloaded()) { + secondaryActionProgress.setPercentage(1, item); // Do not animate 100% -> 0% + secondaryActionProgress.setIndeterminate(false); + } else { + secondaryActionProgress.setPercentage(0, item); // Animate X% -> 0% + secondaryActionProgress.setIndeterminate(false); + } + + duration.setText(Converter.getDurationStringLong(media.getDuration())); + duration.setContentDescription(activity.getString(R.string.chapter_duration, + Converter.getDurationStringLocalized(activity, media.getDuration()))); + if (PlaybackStatus.isPlaying(item.getMedia()) || item.isInProgress()) { + int progress = (int) (100.0 * media.getPosition() / media.getDuration()); + int remainingTime = Math.max(media.getDuration() - media.getPosition(), 0); + progressBar.setProgress(progress); + position.setText(Converter.getDurationStringLong(media.getPosition())); + position.setContentDescription(activity.getString(R.string.position, + Converter.getDurationStringLocalized(activity, media.getPosition()))); + progressBar.setVisibility(View.VISIBLE); + position.setVisibility(View.VISIBLE); + if (UserPreferences.shouldShowRemainingTime()) { + duration.setText(((remainingTime > 0) ? "-" : "") + Converter.getDurationStringLong(remainingTime)); + duration.setContentDescription(activity.getString(R.string.chapter_duration, + Converter.getDurationStringLocalized(activity, (media.getDuration() - media.getPosition())))); + } + } else { + progressBar.setVisibility(View.GONE); + position.setVisibility(View.GONE); + } + + if (media.getSize() > 0) { + size.setText(Formatter.formatShortFileSize(activity, media.getSize())); + } else if (NetworkUtils.isEpisodeHeadDownloadAllowed() && !media.checkedOnSizeButUnknown()) { + size.setText(""); + MediaSizeLoader.getFeedMediaSizeObservable(media).subscribe( + sizeValue -> { + if (sizeValue > 0) { + size.setText(Formatter.formatShortFileSize(activity, sizeValue)); + } else { + size.setText(""); + } + }, error -> { + size.setText(""); + Log.e(TAG, Log.getStackTraceString(error)); + }); + } else { + size.setText(""); + } + } + + public void bindDummy() { + item = new FeedItem(); + container.setAlpha(0.1f); + secondaryActionIcon.setImageDrawable(null); + isInbox.setVisibility(View.VISIBLE); + isVideo.setVisibility(View.GONE); + isFavorite.setVisibility(View.GONE); + isInQueue.setVisibility(View.GONE); + title.setText("███████"); + pubDate.setText("████"); + duration.setText("████"); + secondaryActionProgress.setPercentage(0, null); + secondaryActionProgress.setIndeterminate(false); + progressBar.setVisibility(View.GONE); + position.setVisibility(View.GONE); + dragHandle.setVisibility(View.GONE); + size.setText(""); + itemView.setBackgroundResource(ThemeUtils.getDrawableFromAttr(activity, R.attr.selectableItemBackground)); + placeholder.setText(""); + if (coverHolder.getVisibility() == View.VISIBLE) { + new CoverLoader() + .withResource(R.color.medium_gray) + .withPlaceholderView(placeholder) + .withCoverView(cover) + .load(); + } + } + + private void updateDuration(PlaybackPositionEvent event) { + if (getFeedItem().getMedia() != null) { + getFeedItem().getMedia().setPosition(event.getPosition()); + getFeedItem().getMedia().setDuration(event.getDuration()); + } + int currentPosition = event.getPosition(); + int timeDuration = event.getDuration(); + int remainingTime = Math.max(timeDuration - currentPosition, 0); + Log.d(TAG, "currentPosition " + Converter.getDurationStringLong(currentPosition)); + if (currentPosition == Playable.INVALID_TIME || timeDuration == Playable.INVALID_TIME) { + Log.w(TAG, "Could not react to position observer update because of invalid time"); + return; + } + if (UserPreferences.shouldShowRemainingTime()) { + duration.setText(((remainingTime > 0) ? "-" : "") + Converter.getDurationStringLong(remainingTime)); + } else { + duration.setText(Converter.getDurationStringLong(timeDuration)); + } + } + + public FeedItem getFeedItem() { + return item; + } + + public boolean isCurrentlyPlayingItem() { + return item.getMedia() != null && PlaybackStatus.isCurrentlyPlaying(item.getMedia()); + } + + public void notifyPlaybackPositionUpdated(PlaybackPositionEvent event) { + progressBar.setProgress((int) (100.0 * event.getPosition() / event.getDuration())); + position.setText(Converter.getDurationStringLong(event.getPosition())); + updateDuration(event); + duration.setVisibility(View.VISIBLE); // Even if the duration was previously unknown, it is now known + } + + /** + * Hides the separator dot between icons and text if there are no icons. + */ + public void hideSeparatorIfNecessary() { + boolean hasIcons = isInbox.getVisibility() == View.VISIBLE + || isInQueue.getVisibility() == View.VISIBLE + || isVideo.getVisibility() == View.VISIBLE + || isFavorite.getVisibility() == View.VISIBLE + || isInbox.getVisibility() == View.VISIBLE; + separatorIcons.setVisibility(hasIcons ? View.VISIBLE : View.GONE); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/episodeslist/EpisodeMultiSelectActionHandler.java b/app/src/main/java/de/danoeh/antennapod/ui/episodeslist/EpisodeMultiSelectActionHandler.java new file mode 100644 index 000000000..2a9f76939 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/episodeslist/EpisodeMultiSelectActionHandler.java @@ -0,0 +1,133 @@ +package de.danoeh.antennapod.ui.episodeslist; + +import android.util.Log; + +import androidx.annotation.PluralsRes; + +import com.google.android.material.snackbar.Snackbar; + +import java.util.List; + +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterface; +import de.danoeh.antennapod.storage.database.DBWriter; +import de.danoeh.antennapod.storage.database.LongList; +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.ui.view.LocalDeleteModal; + +public class EpisodeMultiSelectActionHandler { + private static final String TAG = "EpisodeSelectHandler"; + private final MainActivity activity; + private final int actionId; + private int totalNumItems = 0; + private Snackbar snackbar = null; + + public EpisodeMultiSelectActionHandler(MainActivity activity, int actionId) { + this.activity = activity; + this.actionId = actionId; + } + + public void handleAction(List items) { + if (actionId == R.id.add_to_queue_batch) { + queueChecked(items); + } else if (actionId == R.id.remove_from_queue_batch) { + removeFromQueueChecked(items); + } else if (actionId == R.id.remove_from_inbox_batch) { + removeFromInboxChecked(items); + } else if (actionId == R.id.mark_read_batch) { + markedCheckedPlayed(items); + } else if (actionId == R.id.mark_unread_batch) { + markedCheckedUnplayed(items); + } else if (actionId == R.id.download_batch) { + downloadChecked(items); + } else if (actionId == R.id.delete_batch) { + LocalDeleteModal.showLocalFeedDeleteWarningIfNecessary(activity, items, () -> deleteChecked(items)); + } else { + Log.e(TAG, "Unrecognized speed dial action item. Do nothing. id=" + actionId); + } + } + + private void queueChecked(List items) { + // Check if an episode actually contains any media files before adding it to queue + LongList toQueue = new LongList(items.size()); + for (FeedItem episode : items) { + if (episode.hasMedia()) { + toQueue.add(episode.getId()); + } + } + DBWriter.addQueueItem(activity, true, toQueue.toArray()); + showMessage(R.plurals.added_to_queue_batch_label, toQueue.size()); + } + + private void removeFromQueueChecked(List items) { + long[] checkedIds = getSelectedIds(items); + DBWriter.removeQueueItem(activity, true, checkedIds); + showMessage(R.plurals.removed_from_queue_batch_label, checkedIds.length); + } + + private void removeFromInboxChecked(List items) { + LongList markUnplayed = new LongList(); + for (FeedItem episode : items) { + if (episode.isNew()) { + markUnplayed.add(episode.getId()); + } + } + DBWriter.markItemPlayed(FeedItem.UNPLAYED, markUnplayed.toArray()); + showMessage(R.plurals.removed_from_inbox_batch_label, markUnplayed.size()); + } + + private void markedCheckedPlayed(List items) { + long[] checkedIds = getSelectedIds(items); + DBWriter.markItemPlayed(FeedItem.PLAYED, checkedIds); + showMessage(R.plurals.marked_read_batch_label, checkedIds.length); + } + + private void markedCheckedUnplayed(List items) { + long[] checkedIds = getSelectedIds(items); + DBWriter.markItemPlayed(FeedItem.UNPLAYED, checkedIds); + showMessage(R.plurals.marked_unread_batch_label, checkedIds.length); + } + + private void downloadChecked(List items) { + // download the check episodes in the same order as they are currently displayed + for (FeedItem episode : items) { + if (episode.hasMedia() && !episode.getFeed().isLocalFeed()) { + DownloadServiceInterface.get().download(activity, episode); + } + } + showMessage(R.plurals.downloading_batch_label, items.size()); + } + + private void deleteChecked(List items) { + int countHasMedia = 0; + for (FeedItem feedItem : items) { + if (feedItem.hasMedia() && feedItem.getMedia().isDownloaded()) { + countHasMedia++; + DBWriter.deleteFeedMediaOfItem(activity, feedItem.getMedia()); + } + } + showMessage(R.plurals.deleted_multi_episode_batch_label, countHasMedia); + } + + private void showMessage(@PluralsRes int msgId, int numItems) { + totalNumItems += numItems; + activity.runOnUiThread(() -> { + String text = activity.getResources().getQuantityString(msgId, totalNumItems, totalNumItems); + if (snackbar != null) { + snackbar.setText(text); + snackbar.show(); // Resets the timeout + } else { + snackbar = activity.showSnackbarAbovePlayer(text, Snackbar.LENGTH_LONG); + } + }); + } + + private long[] getSelectedIds(List items) { + long[] checkedIds = new long[items.size()]; + for (int i = 0; i < items.size(); ++i) { + checkedIds[i] = items.get(i).getId(); + } + return checkedIds; + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/episodeslist/EpisodesListFragment.java b/app/src/main/java/de/danoeh/antennapod/ui/episodeslist/EpisodesListFragment.java new file mode 100644 index 000000000..054183f7c --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/episodeslist/EpisodesListFragment.java @@ -0,0 +1,457 @@ +package de.danoeh.antennapod.ui.episodeslist; + +import android.content.DialogInterface; +import android.os.Bundle; +import android.util.Log; +import android.view.ContextMenu; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ProgressBar; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.appcompat.widget.Toolbar; +import androidx.core.util.Pair; +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.RecyclerView; +import androidx.recyclerview.widget.SimpleItemAnimator; +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; + +import com.google.android.material.appbar.MaterialToolbar; +import com.google.android.material.snackbar.Snackbar; +import com.leinardi.android.speeddial.SpeedDialView; + +import de.danoeh.antennapod.ui.screen.SearchFragment; +import de.danoeh.antennapod.net.download.serviceinterface.FeedUpdateManager; +import org.greenrobot.eventbus.EventBus; +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.core.util.ConfirmationDialog; +import de.danoeh.antennapod.ui.MenuItemUtils; +import de.danoeh.antennapod.core.util.FeedItemUtil; +import de.danoeh.antennapod.event.EpisodeDownloadEvent; +import de.danoeh.antennapod.event.FeedItemEvent; +import de.danoeh.antennapod.event.FeedListUpdateEvent; +import de.danoeh.antennapod.event.FeedUpdateRunningEvent; +import de.danoeh.antennapod.event.PlayerStatusEvent; +import de.danoeh.antennapod.event.UnreadItemsUpdateEvent; +import de.danoeh.antennapod.event.playback.PlaybackPositionEvent; +import de.danoeh.antennapod.ui.swipeactions.SwipeActions; +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedItemFilter; +import de.danoeh.antennapod.ui.view.EmptyViewHandler; +import de.danoeh.antennapod.ui.view.LiftOnScrollListener; +import io.reactivex.Completable; +import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; +import io.reactivex.schedulers.Schedulers; + +/** + * Shows unread or recently published episodes + */ +public abstract class EpisodesListFragment extends Fragment + implements EpisodeItemListAdapter.OnSelectModeListener, Toolbar.OnMenuItemClickListener { + public static final String TAG = "EpisodesListFragment"; + private static final String KEY_UP_ARROW = "up_arrow"; + protected static final int EPISODES_PER_PAGE = 150; + protected int page = 1; + protected boolean isLoadingMore = false; + protected boolean hasMoreItems = false; + private boolean displayUpArrow; + + protected EpisodeItemListRecyclerView recyclerView; + protected EpisodeItemListAdapter listAdapter; + protected EmptyViewHandler emptyView; + protected SpeedDialView speedDialView; + protected MaterialToolbar toolbar; + protected SwipeRefreshLayout swipeRefreshLayout; + protected SwipeActions swipeActions; + private ProgressBar progressBar; + @NonNull + protected List episodes = new ArrayList<>(); + protected Disposable disposable; + protected TextView txtvInformation; + + @Override + public void onStart() { + super.onStart(); + EventBus.getDefault().register(this); + loadItems(); + } + + @Override + public void onResume() { + super.onResume(); + registerForContextMenu(recyclerView); + } + + @Override + public void onPause() { + super.onPause(); + recyclerView.saveScrollPosition(getPrefName()); + unregisterForContextMenu(recyclerView); + } + + @Override + public void onStop() { + super.onStop(); + EventBus.getDefault().unregister(this); + if (disposable != null) { + disposable.dispose(); + } + } + + @Override + public boolean onMenuItemClick(MenuItem item) { + final int itemId = item.getItemId(); + if (itemId == R.id.refresh_item) { + FeedUpdateManager.getInstance().runOnceOrAsk(requireContext()); + return true; + } else if (itemId == R.id.action_search) { + ((MainActivity) getActivity()).loadChildFragment(SearchFragment.newInstance()); + return true; + } + return false; + } + + @Override + public boolean onContextItemSelected(@NonNull MenuItem item) { + Log.d(TAG, "onContextItemSelected() called with: " + "item = [" + item + "]"); + if (!getUserVisibleHint() || !isVisible() || !isMenuVisible()) { + // The method is called on all fragments in a ViewPager, so this needs to be ignored in invisible ones. + // Apparently, none of the visibility check method works reliably on its own, so we just use all. + return false; + } else if (listAdapter.getLongPressedItem() == null) { + Log.i(TAG, "Selected item or listAdapter was null, ignoring selection"); + return super.onContextItemSelected(item); + } else if (listAdapter.onContextItemSelected(item)) { + return true; + } + FeedItem selectedItem = listAdapter.getLongPressedItem(); + return FeedItemMenuHandler.onMenuItemClicked(this, item.getItemId(), selectedItem); + } + + @NonNull + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + super.onCreateView(inflater, container, savedInstanceState); + View root = inflater.inflate(R.layout.episodes_list_fragment, container, false); + txtvInformation = root.findViewById(R.id.txtvInformation); + toolbar = root.findViewById(R.id.toolbar); + toolbar.setOnMenuItemClickListener(this); + toolbar.setOnLongClickListener(v -> { + recyclerView.scrollToPosition(5); + recyclerView.post(() -> recyclerView.smoothScrollToPosition(0)); + return false; + }); + displayUpArrow = getParentFragmentManager().getBackStackEntryCount() != 0; + if (savedInstanceState != null) { + displayUpArrow = savedInstanceState.getBoolean(KEY_UP_ARROW); + } + ((MainActivity) getActivity()).setupToolbarToggle(toolbar, displayUpArrow); + + recyclerView = root.findViewById(R.id.recyclerView); + recyclerView.setRecycledViewPool(((MainActivity) getActivity()).getRecycledViewPool()); + setupLoadMoreScrollListener(); + recyclerView.addOnScrollListener(new LiftOnScrollListener(root.findViewById(R.id.appbar))); + + swipeActions = new SwipeActions(this, getFragmentTag()).attachTo(recyclerView); + swipeActions.setFilter(getFilter()); + + RecyclerView.ItemAnimator animator = recyclerView.getItemAnimator(); + if (animator instanceof SimpleItemAnimator) { + ((SimpleItemAnimator) animator).setSupportsChangeAnimations(false); + } + + swipeRefreshLayout = root.findViewById(R.id.swipeRefresh); + swipeRefreshLayout.setDistanceToTriggerSync(getResources().getInteger(R.integer.swipe_refresh_distance)); + swipeRefreshLayout.setOnRefreshListener(() -> FeedUpdateManager.getInstance().runOnceOrAsk(requireContext())); + + listAdapter = new EpisodeItemListAdapter((MainActivity) getActivity()) { + @Override + public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { + super.onCreateContextMenu(menu, v, menuInfo); + if (!inActionMode()) { + menu.findItem(R.id.multi_select).setVisible(true); + } + MenuItemUtils.setOnClickListeners(menu, EpisodesListFragment.this::onContextItemSelected); + } + }; + listAdapter.setOnSelectModeListener(this); + recyclerView.setAdapter(listAdapter); + progressBar = root.findViewById(R.id.progressBar); + progressBar.setVisibility(View.VISIBLE); + + emptyView = new EmptyViewHandler(getContext()); + emptyView.attachToRecyclerView(recyclerView); + emptyView.setIcon(R.drawable.ic_feed); + emptyView.setTitle(R.string.no_all_episodes_head_label); + emptyView.setMessage(R.string.no_all_episodes_label); + emptyView.updateAdapter(listAdapter); + emptyView.hide(); + + speedDialView = root.findViewById(R.id.fabSD); + speedDialView.setOverlayLayout(root.findViewById(R.id.fabSDOverlay)); + speedDialView.inflate(R.menu.episodes_apply_action_speeddial); + speedDialView.setOnChangeListener(new SpeedDialView.OnChangeListener() { + @Override + public boolean onMainActionSelected() { + return false; + } + + @Override + public void onToggleChanged(boolean open) { + if (open && listAdapter.getSelectedCount() == 0) { + ((MainActivity) getActivity()).showSnackbarAbovePlayer(R.string.no_items_selected, + Snackbar.LENGTH_SHORT); + speedDialView.close(); + } + } + }); + speedDialView.setOnActionSelectedListener(actionItem -> { + int confirmationString = 0; + if (listAdapter.getSelectedItems().size() >= 25 || listAdapter.shouldSelectLazyLoadedItems()) { + // Should ask for confirmation + if (actionItem.getId() == R.id.mark_read_batch) { + confirmationString = R.string.multi_select_mark_played_confirmation; + } else if (actionItem.getId() == R.id.mark_unread_batch) { + confirmationString = R.string.multi_select_mark_unplayed_confirmation; + } + } + if (confirmationString == 0) { + performMultiSelectAction(actionItem.getId()); + } else { + new ConfirmationDialog(getActivity(), R.string.multi_select, confirmationString) { + @Override + public void onConfirmButtonPressed(DialogInterface dialog) { + performMultiSelectAction(actionItem.getId()); + } + }.createNewDialog().show(); + } + return true; + }); + + return root; + } + + private void performMultiSelectAction(int actionItemId) { + EpisodeMultiSelectActionHandler handler = + new EpisodeMultiSelectActionHandler(((MainActivity) getActivity()), actionItemId); + Completable.fromAction( + () -> { + handler.handleAction(listAdapter.getSelectedItems()); + if (listAdapter.shouldSelectLazyLoadedItems()) { + int applyPage = page + 1; + List nextPage; + do { + nextPage = loadMoreData(applyPage); + handler.handleAction(nextPage); + applyPage++; + } while (nextPage.size() == EPISODES_PER_PAGE); + } + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(() -> listAdapter.endSelectMode(), + error -> Log.e(TAG, Log.getStackTraceString(error))); + } + + private void setupLoadMoreScrollListener() { + recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { + @Override + public void onScrolled(@NonNull RecyclerView view, int deltaX, int deltaY) { + super.onScrolled(view, deltaX, deltaY); + if (!isLoadingMore && hasMoreItems && recyclerView.isScrolledToBottom()) { + /* The end of the list has been reached. Load more data. */ + page++; + loadMoreItems(); + isLoadingMore = true; + } + } + }); + } + + private void loadMoreItems() { + if (disposable != null) { + disposable.dispose(); + } + isLoadingMore = true; + listAdapter.setDummyViews(1); + listAdapter.notifyItemInserted(listAdapter.getItemCount() - 1); + disposable = Observable.fromCallable(() -> loadMoreData(page)) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + data -> { + if (data.size() < EPISODES_PER_PAGE) { + hasMoreItems = false; + } + episodes.addAll(data); + listAdapter.setDummyViews(0); + listAdapter.updateItems(episodes); + if (listAdapter.shouldSelectLazyLoadedItems()) { + listAdapter.setSelected(episodes.size() - data.size(), episodes.size(), true); + } + }, error -> { + listAdapter.setDummyViews(0); + listAdapter.updateItems(Collections.emptyList()); + Log.e(TAG, Log.getStackTraceString(error)); + }, () -> { + // Make sure to not always load 2 pages at once + recyclerView.post(() -> isLoadingMore = false); + }); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + listAdapter.endSelectMode(); + } + + @Override + public void onStartSelectMode() { + speedDialView.setVisibility(View.VISIBLE); + } + + @Override + public void onEndSelectMode() { + speedDialView.close(); + speedDialView.setVisibility(View.GONE); + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onEventMainThread(FeedItemEvent event) { + Log.d(TAG, "onEventMainThread() called with: " + "event = [" + event + "]"); + for (FeedItem item : event.items) { + int pos = FeedItemUtil.indexOfItemWithId(episodes, item.getId()); + if (pos >= 0) { + episodes.remove(pos); + if (getFilter().matches(item)) { + episodes.add(pos, item); + listAdapter.notifyItemChangedCompat(pos); + } else { + listAdapter.notifyItemRemoved(pos); + } + } + } + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onEventMainThread(PlaybackPositionEvent event) { + for (int i = 0; i < listAdapter.getItemCount(); i++) { + EpisodeItemViewHolder holder = (EpisodeItemViewHolder) recyclerView.findViewHolderForAdapterPosition(i); + if (holder != null && holder.isCurrentlyPlayingItem()) { + holder.notifyPlaybackPositionUpdated(event); + break; + } + } + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onKeyUp(KeyEvent event) { + if (!isAdded() || !isVisible() || !isMenuVisible()) { + return; + } + switch (event.getKeyCode()) { + case KeyEvent.KEYCODE_T: + recyclerView.smoothScrollToPosition(0); + break; + case KeyEvent.KEYCODE_B: + recyclerView.smoothScrollToPosition(listAdapter.getItemCount() - 1); + break; + default: + break; + } + } + + @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) + public void onEventMainThread(EpisodeDownloadEvent event) { + for (String downloadUrl : event.getUrls()) { + int pos = FeedItemUtil.indexOfItemWithDownloadUrl(episodes, downloadUrl); + if (pos >= 0) { + listAdapter.notifyItemChangedCompat(pos); + } + } + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onPlayerStatusChanged(PlayerStatusEvent event) { + loadItems(); + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onUnreadItemsChanged(UnreadItemsUpdateEvent event) { + loadItems(); + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onFeedListChanged(FeedListUpdateEvent event) { + loadItems(); + } + + protected void loadItems() { + if (disposable != null) { + disposable.dispose(); + } + disposable = Observable.fromCallable(() -> new Pair<>(loadData(), loadTotalItemCount())) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + data -> { + final boolean restoreScrollPosition = episodes.isEmpty(); + episodes = data.first; + hasMoreItems = !(page == 1 && episodes.size() < EPISODES_PER_PAGE); + progressBar.setVisibility(View.GONE); + listAdapter.setDummyViews(0); + listAdapter.updateItems(episodes); + listAdapter.setTotalNumberOfItems(data.second); + if (restoreScrollPosition) { + recyclerView.restoreScrollPosition(getPrefName()); + } + updateToolbar(); + }, error -> { + listAdapter.setDummyViews(0); + listAdapter.updateItems(Collections.emptyList()); + Log.e(TAG, Log.getStackTraceString(error)); + }); + } + + @NonNull + protected abstract List loadData(); + + @NonNull + protected abstract List loadMoreData(int page); + + protected abstract int loadTotalItemCount(); + + protected abstract FeedItemFilter getFilter(); + + protected abstract String getFragmentTag(); + + protected abstract String getPrefName(); + + protected void updateToolbar() { + } + + @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) + public void onEventMainThread(FeedUpdateRunningEvent event) { + swipeRefreshLayout.setRefreshing(event.isFeedUpdateRunning); + } + + @Override + public void onSaveInstanceState(@NonNull Bundle outState) { + outState.putBoolean(KEY_UP_ARROW, displayUpArrow); + super.onSaveInstanceState(outState); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/episodeslist/FeedItemMenuHandler.java b/app/src/main/java/de/danoeh/antennapod/ui/episodeslist/FeedItemMenuHandler.java new file mode 100644 index 000000000..12b455d1f --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/episodeslist/FeedItemMenuHandler.java @@ -0,0 +1,282 @@ +package de.danoeh.antennapod.ui.episodeslist; + +import android.content.Context; +import android.os.Handler; +import android.util.Log; +import android.view.KeyEvent; +import android.view.Menu; +import android.view.MenuItem; + +import androidx.annotation.NonNull; +import androidx.fragment.app.Fragment; + +import com.google.android.material.snackbar.Snackbar; + +import java.util.Arrays; + +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.net.sync.serviceinterface.SynchronizationQueueSink; +import de.danoeh.antennapod.storage.preferences.PlaybackPreferences; +import de.danoeh.antennapod.core.util.FeedUtil; +import de.danoeh.antennapod.playback.service.PlaybackServiceInterface; +import de.danoeh.antennapod.storage.database.DBWriter; +import de.danoeh.antennapod.storage.preferences.SynchronizationSettings; +import de.danoeh.antennapod.core.util.FeedItemUtil; +import de.danoeh.antennapod.core.util.IntentUtils; +import de.danoeh.antennapod.playback.service.PlaybackStatus; +import de.danoeh.antennapod.ui.share.ShareUtils; +import de.danoeh.antennapod.ui.share.ShareDialog; +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedMedia; +import de.danoeh.antennapod.net.sync.model.EpisodeAction; +import de.danoeh.antennapod.storage.preferences.UserPreferences; +import de.danoeh.antennapod.ui.appstartintent.MediaButtonStarter; +import de.danoeh.antennapod.ui.view.LocalDeleteModal; + +/** + * Handles interactions with the FeedItemMenu. + */ +public class FeedItemMenuHandler { + + private static final String TAG = "FeedItemMenuHandler"; + + private FeedItemMenuHandler() { + } + + /** + * 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 menu An instance of Menu + * @param selectedItem The FeedItem for which the menu is supposed to be prepared + * @return Returns true if selectedItem is not null. + */ + public static boolean onPrepareMenu(Menu menu, FeedItem selectedItem) { + if (menu == null || selectedItem == null) { + return false; + } + final boolean hasMedia = selectedItem.getMedia() != null; + final boolean isPlaying = hasMedia && PlaybackStatus.isPlaying(selectedItem.getMedia()); + final boolean isInQueue = selectedItem.isTagged(FeedItem.TAG_QUEUE); + final boolean fileDownloaded = hasMedia && selectedItem.getMedia().fileExists(); + final boolean isLocalFile = hasMedia && selectedItem.getFeed().isLocalFeed(); + final boolean isFavorite = selectedItem.isTagged(FeedItem.TAG_FAVORITE); + + setItemVisibility(menu, R.id.skip_episode_item, isPlaying); + setItemVisibility(menu, R.id.remove_from_queue_item, isInQueue); + setItemVisibility(menu, R.id.add_to_queue_item, !isInQueue && selectedItem.getMedia() != null); + setItemVisibility(menu, R.id.visit_website_item, !selectedItem.getFeed().isLocalFeed() + && ShareUtils.hasLinkToShare(selectedItem)); + setItemVisibility(menu, R.id.share_item, !selectedItem.getFeed().isLocalFeed()); + setItemVisibility(menu, R.id.remove_inbox_item, selectedItem.isNew()); + setItemVisibility(menu, R.id.mark_read_item, !selectedItem.isPlayed()); + setItemVisibility(menu, R.id.mark_unread_item, selectedItem.isPlayed()); + setItemVisibility(menu, R.id.reset_position, hasMedia && selectedItem.getMedia().getPosition() != 0); + + // Display proper strings when item has no media + if (hasMedia) { + setItemTitle(menu, R.id.mark_read_item, R.string.mark_read_label); + setItemTitle(menu, R.id.mark_unread_item, R.string.mark_unread_label); + } else { + setItemTitle(menu, R.id.mark_read_item, R.string.mark_read_no_media_label); + setItemTitle(menu, R.id.mark_unread_item, R.string.mark_unread_label_no_media); + } + + setItemVisibility(menu, R.id.add_to_favorites_item, !isFavorite); + setItemVisibility(menu, R.id.remove_from_favorites_item, isFavorite); + setItemVisibility(menu, R.id.remove_item, fileDownloaded || isLocalFile); + return true; + } + + /** + * Used to set the viability of a menu item. + * This method also does some null-checking so that neither menu nor the menu item are null + * in order to prevent nullpointer exceptions. + * @param menu The menu that should be used + * @param menuId The id of the menu item that will be used + * @param visibility The new visibility status of given menu item + * */ + private static void setItemVisibility(Menu menu, int menuId, boolean visibility) { + if (menu == null) { + return; + } + MenuItem item = menu.findItem(menuId); + if (item != null) { + item.setVisible(visibility); + } + } + + /** + * This method allows to replace to String of a menu item with a different one. + * @param menu Menu item that should be used + * @param id The id of the string that is going to be replaced. + * @param noMedia The id of the new String that is going to be used. + * */ + public static void setItemTitle(Menu menu, int id, int noMedia) { + MenuItem item = menu.findItem(id); + if (item != null) { + item.setTitle(noMedia); + } + } + + /** + * The same method as {@link #onPrepareMenu(Menu, FeedItem)}, 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(Menu menu, FeedItem selectedItem, int... excludeIds) { + if (menu == null || selectedItem == null) { + return false; + } + boolean rc = onPrepareMenu(menu, selectedItem); + if (rc && excludeIds != null) { + for (int id : excludeIds) { + setItemVisibility(menu, id, false); + } + } + return rc; + } + + /** + * Default menu handling for the given FeedItem. + * + * A Fragment instance, (rather than the more generic Context), is needed as a parameter + * to support some UI operations, e.g., creating a Snackbar. + */ + public static boolean onMenuItemClicked(@NonNull Fragment fragment, int menuItemId, + @NonNull FeedItem selectedItem) { + + @NonNull Context context = fragment.requireContext(); + if (menuItemId == R.id.skip_episode_item) { + context.sendBroadcast(MediaButtonStarter.createIntent(context, KeyEvent.KEYCODE_MEDIA_NEXT)); + } else if (menuItemId == R.id.remove_item) { + LocalDeleteModal.showLocalFeedDeleteWarningIfNecessary(context, Arrays.asList(selectedItem), + () -> DBWriter.deleteFeedMediaOfItem(context, selectedItem.getMedia())); + } else if (menuItemId == R.id.remove_inbox_item) { + removeNewFlagWithUndo(fragment, selectedItem); + } else if (menuItemId == R.id.mark_read_item) { + selectedItem.setPlayed(true); + DBWriter.markItemPlayed(selectedItem, FeedItem.PLAYED, true); + if (!selectedItem.getFeed().isLocalFeed() && SynchronizationSettings.isProviderConnected()) { + FeedMedia media = selectedItem.getMedia(); + // not all items have media, Gpodder only cares about those that do + if (media != null) { + EpisodeAction actionPlay = new EpisodeAction.Builder(selectedItem, EpisodeAction.PLAY) + .currentTimestamp() + .started(media.getDuration() / 1000) + .position(media.getDuration() / 1000) + .total(media.getDuration() / 1000) + .build(); + SynchronizationQueueSink.enqueueEpisodeActionIfSynchronizationIsActive(context, actionPlay); + } + } + } else if (menuItemId == R.id.mark_unread_item) { + selectedItem.setPlayed(false); + DBWriter.markItemPlayed(selectedItem, FeedItem.UNPLAYED, false); + if (!selectedItem.getFeed().isLocalFeed() && selectedItem.getMedia() != null) { + EpisodeAction actionNew = new EpisodeAction.Builder(selectedItem, EpisodeAction.NEW) + .currentTimestamp() + .build(); + SynchronizationQueueSink.enqueueEpisodeActionIfSynchronizationIsActive(context, actionNew); + } + } else if (menuItemId == R.id.add_to_queue_item) { + DBWriter.addQueueItem(context, selectedItem); + } else if (menuItemId == R.id.remove_from_queue_item) { + DBWriter.removeQueueItem(context, true, selectedItem); + } else if (menuItemId == R.id.add_to_favorites_item) { + DBWriter.addFavoriteItem(selectedItem); + } else if (menuItemId == R.id.remove_from_favorites_item) { + DBWriter.removeFavoriteItem(selectedItem); + } else if (menuItemId == R.id.reset_position) { + selectedItem.getMedia().setPosition(0); + if (PlaybackPreferences.getCurrentlyPlayingFeedMediaId() == selectedItem.getMedia().getId()) { + PlaybackPreferences.writeNoMediaPlaying(); + IntentUtils.sendLocalBroadcast(context, PlaybackServiceInterface.ACTION_SHUTDOWN_PLAYBACK_SERVICE); + } + DBWriter.markItemPlayed(selectedItem, FeedItem.UNPLAYED, true); + } else if (menuItemId == R.id.visit_website_item) { + IntentUtils.openInBrowser(context, FeedItemUtil.getLinkWithFallback(selectedItem)); + } else if (menuItemId == R.id.share_item) { + ShareDialog shareDialog = ShareDialog.newInstance(selectedItem); + shareDialog.show((fragment.getActivity().getSupportFragmentManager()), "ShareEpisodeDialog"); + } else { + Log.d(TAG, "Unknown menuItemId: " + menuItemId); + return false; + } + // Refresh menu state + + return true; + } + + /** + * Remove new flag with additional UI logic to allow undo with Snackbar. + * + * Undo is useful for Remove new flag, given there is no UI to undo it otherwise + * ,i.e., there is (context) menu item for add new flag + */ + public static void markReadWithUndo(@NonNull Fragment fragment, FeedItem item, + int playState, boolean showSnackbar) { + if (item == null) { + return; + } + + Log.d(TAG, "markReadWithUndo(" + item.getId() + ")"); + // we're marking it as unplayed since the user didn't actually play it + // but they don't want it considered 'NEW' anymore + DBWriter.markItemPlayed(playState, item.getId()); + + final Handler h = new Handler(fragment.requireContext().getMainLooper()); + final Runnable r = () -> { + FeedMedia media = item.getMedia(); + if (media == null) { + return; + } + boolean shouldAutoDelete = FeedUtil.shouldAutoDeleteItemsOnThatFeed(item.getFeed()); + int smartMarkAsPlayedSecs = UserPreferences.getSmartMarkAsPlayedSecs(); + boolean almostEnded = media.getDuration() > 0 + && media.getPosition() >= media.getDuration() - smartMarkAsPlayedSecs * 1000; + if (almostEnded && shouldAutoDelete) { + DBWriter.deleteFeedMediaOfItem(fragment.requireContext(), media); + } + }; + + int playStateStringRes; + switch (playState) { + default: + case FeedItem.UNPLAYED: + if (item.getPlayState() == FeedItem.NEW) { + //was new + playStateStringRes = R.string.removed_inbox_label; + } else { + //was played + playStateStringRes = R.string.marked_as_unplayed_label; + } + break; + case FeedItem.PLAYED: + playStateStringRes = R.string.marked_as_played_label; + break; + } + + int duration = Snackbar.LENGTH_LONG; + + if (showSnackbar) { + ((MainActivity) fragment.getActivity()).showSnackbarAbovePlayer( + playStateStringRes, duration) + .setAction(fragment.getString(R.string.undo), v -> { + DBWriter.markItemPlayed(item.getPlayState(), item.getId()); + // don't forget to cancel the thing that's going to remove the media + h.removeCallbacks(r); + }); + } + + h.postDelayed(r, (int) Math.ceil(duration * 1.05f)); + } + + public static void removeNewFlagWithUndo(@NonNull Fragment fragment, FeedItem item) { + markReadWithUndo(fragment, item, FeedItem.UNPLAYED, false); + } + +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/episodeslist/HorizontalItemListAdapter.java b/app/src/main/java/de/danoeh/antennapod/ui/episodeslist/HorizontalItemListAdapter.java new file mode 100644 index 000000000..f40851ec2 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/episodeslist/HorizontalItemListAdapter.java @@ -0,0 +1,136 @@ +package de.danoeh.antennapod.ui.episodeslist; + +import android.view.ContextMenu; +import android.view.MenuInflater; +import android.view.View; +import android.view.ViewGroup; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.RecyclerView; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.core.util.FeedItemUtil; +import de.danoeh.antennapod.ui.screen.episode.ItemPagerFragment; +import de.danoeh.antennapod.model.feed.FeedItem; +import org.apache.commons.lang3.ArrayUtils; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.List; + +public class HorizontalItemListAdapter extends RecyclerView.Adapter + implements View.OnCreateContextMenuListener { + private final WeakReference mainActivityRef; + private List data = new ArrayList<>(); + private FeedItem longPressedItem; + private int dummyViews = 0; + + public HorizontalItemListAdapter(MainActivity mainActivity) { + this.mainActivityRef = new WeakReference<>(mainActivity); + setHasStableIds(true); + } + + public void setDummyViews(int dummyViews) { + this.dummyViews = dummyViews; + } + + public void updateData(List newData) { + data = newData; + notifyDataSetChanged(); + } + + @NonNull + @Override + public HorizontalItemViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + return new HorizontalItemViewHolder(mainActivityRef.get(), parent); + } + + @Override + public void onBindViewHolder(@NonNull HorizontalItemViewHolder holder, int position) { + if (position >= data.size()) { + holder.bindDummy(); + return; + } + + final FeedItem item = data.get(position); + holder.bind(item); + + holder.card.setOnCreateContextMenuListener(this); + holder.card.setOnLongClickListener(v -> { + longPressedItem = item; + return false; + }); + holder.secondaryActionIcon.setOnCreateContextMenuListener(this); + holder.secondaryActionIcon.setOnLongClickListener(v -> { + longPressedItem = item; + return false; + }); + holder.card.setOnClickListener(v -> { + MainActivity activity = mainActivityRef.get(); + if (activity != null) { + long[] ids = FeedItemUtil.getIds(data); + int clickPosition = ArrayUtils.indexOf(ids, item.getId()); + activity.loadChildFragment(ItemPagerFragment.newInstance(ids, clickPosition)); + } + }); + } + + @Override + public long getItemId(int position) { + if (position >= data.size()) { + return RecyclerView.NO_ID; // Dummy views + } + return data.get(position).getId(); + } + + @Override + public int getItemCount() { + return dummyViews + data.size(); + } + + @Override + public void onViewRecycled(@NonNull HorizontalItemViewHolder holder) { + super.onViewRecycled(holder); + // Set all listeners to null. This is required to prevent leaking fragments that have set a listener. + // Activity -> recycledViewPool -> ViewHolder -> Listener -> Fragment (can not be garbage collected) + holder.card.setOnClickListener(null); + holder.card.setOnCreateContextMenuListener(null); + holder.card.setOnLongClickListener(null); + holder.secondaryActionIcon.setOnClickListener(null); + holder.secondaryActionIcon.setOnCreateContextMenuListener(null); + holder.secondaryActionIcon.setOnLongClickListener(null); + } + + /** + * {@link #notifyItemChanged(int)} is final, so we can not override. + * Calling {@link #notifyItemChanged(int)} may bind the item to a new ViewHolder and execute a transition. + * This causes flickering and breaks the download animation that stores the old progress in the View. + * Instead, we tell the adapter to use partial binding by calling {@link #notifyItemChanged(int, Object)}. + * We actually ignore the payload and always do a full bind but calling the partial bind method ensures + * that ViewHolders are always re-used. + * + * @param position Position of the item that has changed + */ + public void notifyItemChangedCompat(int position) { + notifyItemChanged(position, "foo"); + } + + @Nullable + public FeedItem getLongPressedItem() { + return longPressedItem; + } + + @Override + public void onCreateContextMenu(final ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { + MenuInflater inflater = mainActivityRef.get().getMenuInflater(); + if (longPressedItem == null) { + return; + } + menu.clear(); + inflater.inflate(R.menu.feeditemlist_context, menu); + menu.setHeaderTitle(longPressedItem.getTitle()); + FeedItemMenuHandler.onPrepareMenu(menu, longPressedItem, R.id.skip_episode_item); + } + + +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/episodeslist/HorizontalItemViewHolder.java b/app/src/main/java/de/danoeh/antennapod/ui/episodeslist/HorizontalItemViewHolder.java new file mode 100644 index 000000000..c297725b0 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/episodeslist/HorizontalItemViewHolder.java @@ -0,0 +1,130 @@ +package de.danoeh.antennapod.ui.episodeslist; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.ProgressBar; +import android.widget.TextView; +import androidx.cardview.widget.CardView; +import androidx.recyclerview.widget.RecyclerView; +import com.google.android.material.elevation.SurfaceColors; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.ui.CoverLoader; +import de.danoeh.antennapod.actionbutton.ItemActionButton; +import de.danoeh.antennapod.ui.common.DateFormatter; +import de.danoeh.antennapod.playback.service.PlaybackStatus; +import de.danoeh.antennapod.event.playback.PlaybackPositionEvent; +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedMedia; +import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterface; +import de.danoeh.antennapod.ui.common.CircularProgressBar; +import de.danoeh.antennapod.ui.common.SquareImageView; +import de.danoeh.antennapod.ui.common.ThemeUtils; +import de.danoeh.antennapod.ui.episodes.ImageResourceUtils; + +public class HorizontalItemViewHolder extends RecyclerView.ViewHolder { + public final CardView card; + public final ImageView secondaryActionIcon; + private final SquareImageView cover; + private final TextView title; + private final TextView date; + private final ProgressBar progressBar; + private final CircularProgressBar circularProgressBar; + private final View progressBarReplacementSpacer; + + private final MainActivity activity; + private FeedItem item; + + public HorizontalItemViewHolder(MainActivity activity, ViewGroup parent) { + super(LayoutInflater.from(activity).inflate(R.layout.horizontal_itemlist_item, parent, false)); + this.activity = activity; + + card = itemView.findViewById(R.id.card); + cover = itemView.findViewById(R.id.cover); + title = itemView.findViewById(R.id.titleLabel); + date = itemView.findViewById(R.id.dateLabel); + secondaryActionIcon = itemView.findViewById(R.id.secondaryActionIcon); + circularProgressBar = itemView.findViewById(R.id.circularProgressBar); + progressBar = itemView.findViewById(R.id.progressBar); + progressBarReplacementSpacer = itemView.findViewById(R.id.progressBarReplacementSpacer); + itemView.setTag(this); + } + + public void bind(FeedItem item) { + this.item = item; + + card.setAlpha(1.0f); + float density = activity.getResources().getDisplayMetrics().density; + card.setCardBackgroundColor(SurfaceColors.getColorForElevation(activity, 1 * density)); + new CoverLoader() + .withUri(ImageResourceUtils.getEpisodeListImageLocation(item)) + .withFallbackUri(item.getFeed().getImageUrl()) + .withCoverView(cover) + .load(); + title.setText(item.getTitle()); + date.setText(DateFormatter.formatAbbrev(activity, item.getPubDate())); + date.setContentDescription(DateFormatter.formatForAccessibility(item.getPubDate())); + ItemActionButton actionButton = ItemActionButton.forItem(item); + actionButton.configure(secondaryActionIcon, secondaryActionIcon, activity); + secondaryActionIcon.setFocusable(false); + + FeedMedia media = item.getMedia(); + if (media == null) { + circularProgressBar.setPercentage(0, item); + setProgressBar(false, 0); + } else { + if (PlaybackStatus.isCurrentlyPlaying(media)) { + card.setCardBackgroundColor(ThemeUtils.getColorFromAttr(activity, R.attr.colorSurfaceVariant)); + } + + if (item.getMedia().getDuration() > 0 && item.getMedia().getPosition() > 0) { + setProgressBar(true, 100.0f * item.getMedia().getPosition() / item.getMedia().getDuration()); + } else { + setProgressBar(false, 0); + } + + if (DownloadServiceInterface.get().isDownloadingEpisode(media.getDownloadUrl())) { + float percent = 0.01f * DownloadServiceInterface.get().getProgress(media.getDownloadUrl()); + circularProgressBar.setPercentage(Math.max(percent, 0.01f), item); + circularProgressBar.setIndeterminate( + DownloadServiceInterface.get().isEpisodeQueued(media.getDownloadUrl())); + } else if (media.isDownloaded()) { + circularProgressBar.setPercentage(1, item); // Do not animate 100% -> 0% + circularProgressBar.setIndeterminate(false); + } else { + circularProgressBar.setPercentage(0, item); // Animate X% -> 0% + circularProgressBar.setIndeterminate(false); + } + } + } + + public void bindDummy() { + card.setAlpha(0.1f); + new CoverLoader() + .withResource(android.R.color.transparent) + .withCoverView(cover) + .load(); + title.setText("████ █████"); + date.setText("███"); + secondaryActionIcon.setImageDrawable(null); + circularProgressBar.setPercentage(0, null); + circularProgressBar.setIndeterminate(false); + setProgressBar(true, 50); + } + + public boolean isCurrentlyPlayingItem() { + return item != null && item.getMedia() != null && PlaybackStatus.isCurrentlyPlaying(item.getMedia()); + } + + public void notifyPlaybackPositionUpdated(PlaybackPositionEvent event) { + setProgressBar(true, 100.0f * event.getPosition() / event.getDuration()); + } + + private void setProgressBar(boolean visible, float progress) { + progressBar.setVisibility(visible ? ViewGroup.VISIBLE : ViewGroup.GONE); + progressBarReplacementSpacer.setVisibility(visible ? View.GONE : ViewGroup.VISIBLE); + progressBar.setProgress(Math.max(5, (int) progress)); // otherwise invisible below the edge radius + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/episodeslist/MediaSizeLoader.java b/app/src/main/java/de/danoeh/antennapod/ui/episodeslist/MediaSizeLoader.java new file mode 100644 index 000000000..57b29f3b3 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/episodeslist/MediaSizeLoader.java @@ -0,0 +1,78 @@ +package de.danoeh.antennapod.ui.episodeslist; + +import android.text.TextUtils; +import de.danoeh.antennapod.net.common.AntennapodHttpClient; +import de.danoeh.antennapod.storage.database.DBWriter; +import de.danoeh.antennapod.net.common.NetworkUtils; +import de.danoeh.antennapod.model.feed.FeedMedia; +import io.reactivex.Single; +import io.reactivex.SingleOnSubscribe; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.schedulers.Schedulers; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import android.util.Log; +import okhttp3.Response; + +import java.io.File; +import java.io.IOException; + +public abstract class MediaSizeLoader { + private static final String TAG = "MediaSizeLoader"; + + public static Single getFeedMediaSizeObservable(FeedMedia media) { + return Single.create((SingleOnSubscribe) emitter -> { + if (!NetworkUtils.isEpisodeHeadDownloadAllowed()) { + emitter.onSuccess(0L); + return; + } + long size = Integer.MIN_VALUE; + if (media.isDownloaded()) { + File mediaFile = new File(media.getLocalFileUrl()); + if (mediaFile.exists()) { + size = mediaFile.length(); + } + } else if (!media.checkedOnSizeButUnknown()) { + // only query the network if we haven't already checked + + String url = media.getDownloadUrl(); + if (TextUtils.isEmpty(url)) { + emitter.onSuccess(0L); + return; + } + + OkHttpClient client = AntennapodHttpClient.getHttpClient(); + Request.Builder httpReq = new Request.Builder() + .url(url) + .header("Accept-Encoding", "identity") + .head(); + try { + Response response = client.newCall(httpReq.build()).execute(); + if (response.isSuccessful()) { + String contentLength = response.header("Content-Length"); + try { + size = Integer.parseInt(contentLength); + } catch (NumberFormatException e) { + Log.e(TAG, Log.getStackTraceString(e)); + } + } + } catch (IOException e) { + emitter.onSuccess(0L); + Log.e(TAG, Log.getStackTraceString(e)); + return; // better luck next time + } + } + Log.d(TAG, "new size: " + size); + if (size <= 0) { + // they didn't tell us the size, but we don't want to keep querying on it + media.setCheckedOnSizeButUnknown(); + } else { + media.setSize(size); + } + emitter.onSuccess(size); + DBWriter.setFeedMedia(media); + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/episodeslist/MoreContentListFooterUtil.java b/app/src/main/java/de/danoeh/antennapod/ui/episodeslist/MoreContentListFooterUtil.java new file mode 100644 index 000000000..fe9ce71f0 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/episodeslist/MoreContentListFooterUtil.java @@ -0,0 +1,53 @@ +package de.danoeh.antennapod.ui.episodeslist; + +import android.view.View; +import android.widget.ImageView; +import android.widget.ProgressBar; + +import de.danoeh.antennapod.core.R; + +/** + * Utility methods for the more_content_list_footer layout. + */ +public class MoreContentListFooterUtil { + + private final View root; + + private boolean loading; + + private Listener listener; + + public MoreContentListFooterUtil(View root) { + this.root = root; + root.setOnClickListener(v -> { + if (listener != null && !loading) { + listener.onClick(); + } + }); + } + + public void setLoadingState(boolean newState) { + final ImageView imageView = root.findViewById(R.id.imgExpand); + final ProgressBar progressBar = root.findViewById(R.id.progBar); + if (newState) { + imageView.setVisibility(View.GONE); + progressBar.setVisibility(View.VISIBLE); + } else { + imageView.setVisibility(View.VISIBLE); + progressBar.setVisibility(View.GONE); + } + loading = newState; + } + + public void setClickListener(Listener l) { + listener = l; + } + + public interface Listener { + void onClick(); + } + + public View getRoot() { + return root; + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/home/HomeFragment.java b/app/src/main/java/de/danoeh/antennapod/ui/home/HomeFragment.java deleted file mode 100644 index 90d4817da..000000000 --- a/app/src/main/java/de/danoeh/antennapod/ui/home/HomeFragment.java +++ /dev/null @@ -1,207 +0,0 @@ -package de.danoeh.antennapod.ui.home; - -import android.Manifest; -import android.content.Context; -import android.content.SharedPreferences; -import android.content.pm.PackageManager; -import android.os.Build; -import android.os.Bundle; -import android.text.TextUtils; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; - -import androidx.annotation.NonNull; -import androidx.appcompat.widget.Toolbar; -import androidx.core.content.ContextCompat; -import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentContainerView; - -import de.danoeh.antennapod.net.download.serviceinterface.FeedUpdateManager; -import de.danoeh.antennapod.ui.echo.EchoConfig; -import org.greenrobot.eventbus.EventBus; -import org.greenrobot.eventbus.Subscribe; -import org.greenrobot.eventbus.ThreadMode; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Calendar; -import java.util.List; - -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.activity.MainActivity; -import de.danoeh.antennapod.storage.database.DBReader; -import de.danoeh.antennapod.databinding.HomeFragmentBinding; -import de.danoeh.antennapod.event.FeedListUpdateEvent; -import de.danoeh.antennapod.event.FeedUpdateRunningEvent; -import de.danoeh.antennapod.fragment.SearchFragment; -import de.danoeh.antennapod.storage.preferences.UserPreferences; -import de.danoeh.antennapod.ui.home.sections.AllowNotificationsSection; -import de.danoeh.antennapod.ui.home.sections.DownloadsSection; -import de.danoeh.antennapod.ui.home.sections.EchoSection; -import de.danoeh.antennapod.ui.home.sections.EpisodesSurpriseSection; -import de.danoeh.antennapod.ui.home.sections.InboxSection; -import de.danoeh.antennapod.ui.home.sections.QueueSection; -import de.danoeh.antennapod.ui.home.sections.SubscriptionsSection; -import de.danoeh.antennapod.view.LiftOnScrollListener; -import io.reactivex.Observable; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.Disposable; -import io.reactivex.schedulers.Schedulers; - -/** - * Shows unread or recently published episodes - */ -public class HomeFragment extends Fragment implements Toolbar.OnMenuItemClickListener { - - public static final String TAG = "HomeFragment"; - public static final String PREF_NAME = "PrefHomeFragment"; - public static final String PREF_HIDDEN_SECTIONS = "PrefHomeSectionsString"; - public static final String PREF_DISABLE_NOTIFICATION_PERMISSION_NAG = "DisableNotificationPermissionNag"; - public static final String PREF_HIDE_ECHO = "HideEcho"; - - private static final String KEY_UP_ARROW = "up_arrow"; - private boolean displayUpArrow; - private HomeFragmentBinding viewBinding; - private Disposable disposable; - - @NonNull - @Override - public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - super.onCreateView(inflater, container, savedInstanceState); - viewBinding = HomeFragmentBinding.inflate(inflater); - viewBinding.toolbar.inflateMenu(R.menu.home); - viewBinding.toolbar.setOnMenuItemClickListener(this); - if (savedInstanceState != null) { - displayUpArrow = savedInstanceState.getBoolean(KEY_UP_ARROW); - } - viewBinding.homeScrollView.setOnScrollChangeListener(new LiftOnScrollListener(viewBinding.appbar)); - ((MainActivity) requireActivity()).setupToolbarToggle(viewBinding.toolbar, displayUpArrow); - populateSectionList(); - updateWelcomeScreenVisibility(); - - viewBinding.swipeRefresh.setDistanceToTriggerSync(getResources().getInteger(R.integer.swipe_refresh_distance)); - viewBinding.swipeRefresh.setOnRefreshListener(() -> - FeedUpdateManager.getInstance().runOnceOrAsk(requireContext())); - - return viewBinding.getRoot(); - } - - private void populateSectionList() { - viewBinding.homeContainer.removeAllViews(); - - SharedPreferences prefs = getContext().getSharedPreferences(HomeFragment.PREF_NAME, Context.MODE_PRIVATE); - if (Build.VERSION.SDK_INT >= 33 && ContextCompat.checkSelfPermission(getContext(), - Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { - if (!prefs.getBoolean(HomeFragment.PREF_DISABLE_NOTIFICATION_PERMISSION_NAG, false)) { - addSection(new AllowNotificationsSection()); - } - } - if (Calendar.getInstance().get(Calendar.YEAR) == EchoConfig.RELEASE_YEAR - && Calendar.getInstance().get(Calendar.MONTH) == Calendar.DECEMBER - && Calendar.getInstance().get(Calendar.DAY_OF_MONTH) >= 10 - && prefs.getInt(PREF_HIDE_ECHO, 0) != EchoConfig.RELEASE_YEAR) { - addSection(new EchoSection()); - } - - List hiddenSections = getHiddenSections(getContext()); - String[] sectionTags = getResources().getStringArray(R.array.home_section_tags); - for (String sectionTag : sectionTags) { - if (hiddenSections.contains(sectionTag)) { - continue; - } - addSection(getSection(sectionTag)); - } - } - - private void addSection(Fragment section) { - FragmentContainerView containerView = new FragmentContainerView(getContext()); - containerView.setId(View.generateViewId()); - viewBinding.homeContainer.addView(containerView); - getChildFragmentManager().beginTransaction().add(containerView.getId(), section).commit(); - } - - private Fragment getSection(String tag) { - switch (tag) { - case QueueSection.TAG: - return new QueueSection(); - case InboxSection.TAG: - return new InboxSection(); - case EpisodesSurpriseSection.TAG: - return new EpisodesSurpriseSection(); - case SubscriptionsSection.TAG: - return new SubscriptionsSection(); - case DownloadsSection.TAG: - return new DownloadsSection(); - default: - return null; - } - } - - public static List getHiddenSections(Context context) { - SharedPreferences prefs = context.getSharedPreferences(HomeFragment.PREF_NAME, Context.MODE_PRIVATE); - String hiddenSectionsString = prefs.getString(HomeFragment.PREF_HIDDEN_SECTIONS, ""); - return new ArrayList<>(Arrays.asList(TextUtils.split(hiddenSectionsString, ","))); - } - - @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) - public void onEventMainThread(FeedUpdateRunningEvent event) { - viewBinding.swipeRefresh.setRefreshing(event.isFeedUpdateRunning); - } - - @Override - public boolean onMenuItemClick(MenuItem item) { - if (item.getItemId() == R.id.homesettings_items) { - HomeSectionsSettingsDialog.open(getContext(), (dialogInterface, i) -> populateSectionList()); - return true; - } else if (item.getItemId() == R.id.refresh_item) { - FeedUpdateManager.getInstance().runOnceOrAsk(requireContext()); - return true; - } else if (item.getItemId() == R.id.action_search) { - ((MainActivity) getActivity()).loadChildFragment(SearchFragment.newInstance()); - return true; - } - return false; - } - - @Override - public void onSaveInstanceState(@NonNull Bundle outState) { - outState.putBoolean(KEY_UP_ARROW, displayUpArrow); - super.onSaveInstanceState(outState); - } - - @Override - public void onStart() { - super.onStart(); - EventBus.getDefault().register(this); - } - - @Override - public void onStop() { - super.onStop(); - EventBus.getDefault().unregister(this); - } - - @Subscribe(threadMode = ThreadMode.MAIN) - public void onFeedListChanged(FeedListUpdateEvent event) { - updateWelcomeScreenVisibility(); - } - - private void updateWelcomeScreenVisibility() { - if (disposable != null) { - disposable.dispose(); - } - disposable = Observable.fromCallable(() -> - DBReader.getNavDrawerData(UserPreferences.getSubscriptionsFilter(), - UserPreferences.getFeedOrder(), UserPreferences.getFeedCounterSetting()).items.size()) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(numSubscriptions -> { - viewBinding.welcomeContainer.setVisibility(numSubscriptions == 0 ? View.VISIBLE : View.GONE); - viewBinding.homeContainer.setVisibility(numSubscriptions == 0 ? View.GONE : View.VISIBLE); - }, error -> Log.e(TAG, Log.getStackTraceString(error))); - } - -} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/home/HomeSection.java b/app/src/main/java/de/danoeh/antennapod/ui/home/HomeSection.java deleted file mode 100644 index 03036d267..000000000 --- a/app/src/main/java/de/danoeh/antennapod/ui/home/HomeSection.java +++ /dev/null @@ -1,105 +0,0 @@ -package de.danoeh.antennapod.ui.home; - -import android.os.Bundle; -import android.text.TextUtils; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.fragment.app.Fragment; -import androidx.recyclerview.widget.DefaultItemAnimator; -import de.danoeh.antennapod.adapter.EpisodeItemListAdapter; -import de.danoeh.antennapod.adapter.HorizontalFeedListAdapter; -import de.danoeh.antennapod.adapter.HorizontalItemListAdapter; -import de.danoeh.antennapod.databinding.HomeSectionBinding; -import de.danoeh.antennapod.menuhandler.FeedItemMenuHandler; -import de.danoeh.antennapod.menuhandler.FeedMenuHandler; -import de.danoeh.antennapod.model.feed.Feed; -import de.danoeh.antennapod.model.feed.FeedItem; -import org.greenrobot.eventbus.EventBus; - -import java.util.Locale; - -/** - * Section on the HomeFragment - */ -public abstract class HomeSection extends Fragment implements View.OnCreateContextMenuListener { - public static final String TAG = "HomeSection"; - protected HomeSectionBinding viewBinding; - - @Nullable - @Override - public View onCreateView(@NonNull LayoutInflater inflater, - @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - viewBinding = HomeSectionBinding.inflate(inflater); - viewBinding.titleLabel.setText(getSectionTitle()); - if (TextUtils.getLayoutDirectionFromLocale(Locale.getDefault()) == View.LAYOUT_DIRECTION_LTR) { - viewBinding.moreButton.setText(getMoreLinkTitle() + "\u00A0»"); - } else { - viewBinding.moreButton.setText("«\u00A0" + getMoreLinkTitle()); - } - viewBinding.moreButton.setOnClickListener((view) -> handleMoreClick()); - if (TextUtils.isEmpty(getMoreLinkTitle())) { - viewBinding.moreButton.setVisibility(View.INVISIBLE); - } - // Dummies are necessary to ensure height, but do not animate them - viewBinding.recyclerView.setItemAnimator(null); - viewBinding.recyclerView.postDelayed( - () -> viewBinding.recyclerView.setItemAnimator(new DefaultItemAnimator()), 500); - return viewBinding.getRoot(); - } - - @Override - public boolean onContextItemSelected(@NonNull MenuItem item) { - if (!getUserVisibleHint() || !isVisible() || !isMenuVisible()) { - // The method is called on all fragments in a ViewPager, so this needs to be ignored in invisible ones. - // Apparently, none of the visibility check method works reliably on its own, so we just use all. - return false; - } - if (viewBinding.recyclerView.getAdapter() instanceof HorizontalFeedListAdapter) { - HorizontalFeedListAdapter adapter = (HorizontalFeedListAdapter) viewBinding.recyclerView.getAdapter(); - Feed selectedFeed = adapter.getLongPressedItem(); - return selectedFeed != null - && FeedMenuHandler.onMenuItemClicked(this, item.getItemId(), selectedFeed, () -> { }); - } - FeedItem longPressedItem; - if (viewBinding.recyclerView.getAdapter() instanceof EpisodeItemListAdapter) { - EpisodeItemListAdapter adapter = (EpisodeItemListAdapter) viewBinding.recyclerView.getAdapter(); - longPressedItem = adapter.getLongPressedItem(); - } else if (viewBinding.recyclerView.getAdapter() instanceof HorizontalItemListAdapter) { - HorizontalItemListAdapter adapter = (HorizontalItemListAdapter) viewBinding.recyclerView.getAdapter(); - longPressedItem = adapter.getLongPressedItem(); - } else { - return false; - } - - if (longPressedItem == null) { - Log.i(TAG, "Selected item or listAdapter was null, ignoring selection"); - return super.onContextItemSelected(item); - } - return FeedItemMenuHandler.onMenuItemClicked(this, item.getItemId(), longPressedItem); - } - - @Override - public void onStart() { - super.onStart(); - EventBus.getDefault().register(this); - registerForContextMenu(viewBinding.recyclerView); - } - - @Override - public void onStop() { - super.onStop(); - EventBus.getDefault().unregister(this); - unregisterForContextMenu(viewBinding.recyclerView); - } - - protected abstract String getSectionTitle(); - - protected abstract String getMoreLinkTitle(); - - protected abstract void handleMoreClick(); -} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/home/HomeSectionsSettingsDialog.java b/app/src/main/java/de/danoeh/antennapod/ui/home/HomeSectionsSettingsDialog.java deleted file mode 100644 index e651aea48..000000000 --- a/app/src/main/java/de/danoeh/antennapod/ui/home/HomeSectionsSettingsDialog.java +++ /dev/null @@ -1,42 +0,0 @@ -package de.danoeh.antennapod.ui.home; - -import android.content.Context; -import android.content.DialogInterface; -import android.content.SharedPreferences; -import android.text.TextUtils; -import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import de.danoeh.antennapod.R; - -import java.util.List; - -public class HomeSectionsSettingsDialog { - public static void open(Context context, DialogInterface.OnClickListener onSettingsChanged) { - final List hiddenSections = HomeFragment.getHiddenSections(context); - String[] sectionLabels = context.getResources().getStringArray(R.array.home_section_titles); - String[] sectionTags = context.getResources().getStringArray(R.array.home_section_tags); - final boolean[] checked = new boolean[sectionLabels.length]; - for (int i = 0; i < sectionLabels.length; i++) { - String tag = sectionTags[i]; - if (!hiddenSections.contains(tag)) { - checked[i] = true; - } - } - - MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(context); - builder.setTitle(R.string.configure_home); - builder.setMultiChoiceItems(sectionLabels, checked, (dialog, which, isChecked) -> { - if (isChecked) { - hiddenSections.remove(sectionTags[which]); - } else { - hiddenSections.add(sectionTags[which]); - } - }); - builder.setPositiveButton(R.string.confirm_label, (dialog, which) -> { - SharedPreferences prefs = context.getSharedPreferences(HomeFragment.PREF_NAME, Context.MODE_PRIVATE); - prefs.edit().putString(HomeFragment.PREF_HIDDEN_SECTIONS, TextUtils.join(",", hiddenSections)).apply(); - onSettingsChanged.onClick(dialog, which); - }); - builder.setNegativeButton(R.string.cancel_label, null); - builder.create().show(); - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/home/sections/AllowNotificationsSection.java b/app/src/main/java/de/danoeh/antennapod/ui/home/sections/AllowNotificationsSection.java deleted file mode 100644 index 0a0d30485..000000000 --- a/app/src/main/java/de/danoeh/antennapod/ui/home/sections/AllowNotificationsSection.java +++ /dev/null @@ -1,68 +0,0 @@ -package de.danoeh.antennapod.ui.home.sections; - -import android.Manifest; -import android.content.Context; -import android.content.Intent; -import android.net.Uri; -import android.os.Build; -import android.os.Bundle; -import android.provider.Settings; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.Toast; -import androidx.activity.result.ActivityResultLauncher; -import androidx.activity.result.contract.ActivityResultContracts; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.fragment.app.Fragment; -import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.activity.MainActivity; -import de.danoeh.antennapod.databinding.HomeSectionNotificationBinding; -import de.danoeh.antennapod.ui.home.HomeFragment; - -public class AllowNotificationsSection extends Fragment { - HomeSectionNotificationBinding viewBinding; - - private final ActivityResultLauncher requestPermissionLauncher = - registerForActivityResult(new ActivityResultContracts.RequestPermission(), isGranted -> { - if (isGranted) { - ((MainActivity) getActivity()).loadFragment(HomeFragment.TAG, null); - } else { - viewBinding.openSettingsButton.setVisibility(View.VISIBLE); - viewBinding.allowButton.setVisibility(View.GONE); - Toast.makeText(getContext(), R.string.notification_permission_denied, Toast.LENGTH_LONG).show(); - } - }); - - @Nullable - @Override - public View onCreateView(@NonNull LayoutInflater inflater, - @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - viewBinding = HomeSectionNotificationBinding.inflate(inflater); - viewBinding.allowButton.setOnClickListener(v -> { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS); - } - }); - viewBinding.openSettingsButton.setOnClickListener(view -> { - Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); - Uri uri = Uri.fromParts("package", getContext().getPackageName(), null); - intent.setData(uri); - startActivity(intent); - }); - viewBinding.denyButton.setOnClickListener(v -> { - MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(getContext()); - builder.setMessage(R.string.notification_permission_deny_warning); - builder.setPositiveButton(R.string.deny_label, (dialog, which) -> { - getContext().getSharedPreferences(HomeFragment.PREF_NAME, Context.MODE_PRIVATE) - .edit().putBoolean(HomeFragment.PREF_DISABLE_NOTIFICATION_PERMISSION_NAG, true).apply(); - ((MainActivity) getActivity()).loadFragment(HomeFragment.TAG, null); - }); - builder.setNegativeButton(R.string.cancel_label, null); - builder.show(); - }); - return viewBinding.getRoot(); - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/home/sections/DownloadsSection.java b/app/src/main/java/de/danoeh/antennapod/ui/home/sections/DownloadsSection.java deleted file mode 100644 index 9540bc2e3..000000000 --- a/app/src/main/java/de/danoeh/antennapod/ui/home/sections/DownloadsSection.java +++ /dev/null @@ -1,140 +0,0 @@ -package de.danoeh.antennapod.ui.home.sections; - -import android.os.Bundle; -import android.util.Log; -import android.view.ContextMenu; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.activity.MainActivity; -import de.danoeh.antennapod.adapter.EpisodeItemListAdapter; -import de.danoeh.antennapod.event.DownloadLogEvent; -import de.danoeh.antennapod.core.menuhandler.MenuItemUtils; -import de.danoeh.antennapod.storage.database.DBReader; -import de.danoeh.antennapod.event.FeedItemEvent; -import de.danoeh.antennapod.event.PlayerStatusEvent; -import de.danoeh.antennapod.event.playback.PlaybackPositionEvent; -import de.danoeh.antennapod.fragment.CompletedDownloadsFragment; -import de.danoeh.antennapod.fragment.swipeactions.SwipeActions; -import de.danoeh.antennapod.model.feed.FeedItem; -import de.danoeh.antennapod.model.feed.FeedItemFilter; -import de.danoeh.antennapod.model.feed.SortOrder; -import de.danoeh.antennapod.storage.preferences.UserPreferences; -import de.danoeh.antennapod.ui.home.HomeSection; -import de.danoeh.antennapod.view.viewholder.EpisodeItemViewHolder; -import io.reactivex.Observable; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.Disposable; -import io.reactivex.schedulers.Schedulers; -import org.greenrobot.eventbus.Subscribe; -import org.greenrobot.eventbus.ThreadMode; - -import java.util.List; - -public class DownloadsSection extends HomeSection { - public static final String TAG = "DownloadsSection"; - private static final int NUM_EPISODES = 2; - private EpisodeItemListAdapter adapter; - private List items; - private Disposable disposable; - - @Nullable - @Override - public View onCreateView(@NonNull LayoutInflater inflater, - @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - final View view = super.onCreateView(inflater, container, savedInstanceState); - viewBinding.recyclerView.setPadding(0, 0, 0, 0); - viewBinding.recyclerView.setOverScrollMode(RecyclerView.OVER_SCROLL_NEVER); - viewBinding.recyclerView.setLayoutManager(new LinearLayoutManager(getContext(), RecyclerView.VERTICAL, false)); - viewBinding.recyclerView.setRecycledViewPool(((MainActivity) requireActivity()).getRecycledViewPool()); - adapter = new EpisodeItemListAdapter((MainActivity) requireActivity()) { - @Override - public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { - super.onCreateContextMenu(menu, v, menuInfo); - MenuItemUtils.setOnClickListeners(menu, DownloadsSection.this::onContextItemSelected); - } - }; - adapter.setDummyViews(NUM_EPISODES); - viewBinding.recyclerView.setAdapter(adapter); - - SwipeActions swipeActions = new SwipeActions(this, CompletedDownloadsFragment.TAG); - swipeActions.attachTo(viewBinding.recyclerView); - swipeActions.setFilter(new FeedItemFilter(FeedItemFilter.DOWNLOADED)); - return view; - } - - @Override - public void onStart() { - super.onStart(); - loadItems(); - } - - @Override - protected void handleMoreClick() { - ((MainActivity) requireActivity()).loadChildFragment(new CompletedDownloadsFragment()); - } - - @Subscribe(threadMode = ThreadMode.MAIN) - public void onEventMainThread(FeedItemEvent event) { - loadItems(); - } - - @Subscribe(threadMode = ThreadMode.MAIN) - public void onEventMainThread(PlaybackPositionEvent event) { - if (adapter == null) { - return; - } - for (int i = 0; i < adapter.getItemCount(); i++) { - EpisodeItemViewHolder holder = (EpisodeItemViewHolder) - viewBinding.recyclerView.findViewHolderForAdapterPosition(i); - if (holder != null && holder.isCurrentlyPlayingItem()) { - holder.notifyPlaybackPositionUpdated(event); - break; - } - } - } - - @Subscribe(threadMode = ThreadMode.MAIN) - public void onDownloadLogChanged(DownloadLogEvent event) { - loadItems(); - } - - @Subscribe(threadMode = ThreadMode.MAIN) - public void onPlayerStatusChanged(PlayerStatusEvent event) { - loadItems(); - } - - @Override - protected String getSectionTitle() { - return getString(R.string.home_downloads_title); - } - - @Override - protected String getMoreLinkTitle() { - return getString(R.string.downloads_label); - } - - private void loadItems() { - if (disposable != null) { - disposable.dispose(); - } - SortOrder sortOrder = UserPreferences.getDownloadsSortedOrder(); - disposable = Observable.fromCallable(() -> DBReader.getEpisodes(0, Integer.MAX_VALUE, - new FeedItemFilter(FeedItemFilter.DOWNLOADED), sortOrder)) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(downloads -> { - if (downloads.size() > NUM_EPISODES) { - downloads = downloads.subList(0, NUM_EPISODES); - } - items = downloads; - adapter.setDummyViews(0); - adapter.updateItems(items); - }, error -> Log.e(TAG, Log.getStackTraceString(error))); - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/home/sections/EchoSection.java b/app/src/main/java/de/danoeh/antennapod/ui/home/sections/EchoSection.java deleted file mode 100644 index 28ff05512..000000000 --- a/app/src/main/java/de/danoeh/antennapod/ui/home/sections/EchoSection.java +++ /dev/null @@ -1,71 +0,0 @@ -package de.danoeh.antennapod.ui.home.sections; - -import android.content.Context; -import android.content.Intent; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.fragment.app.Fragment; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.activity.MainActivity; -import de.danoeh.antennapod.databinding.HomeSectionEchoBinding; -import de.danoeh.antennapod.storage.database.DBReader; -import de.danoeh.antennapod.storage.database.StatisticsItem; -import de.danoeh.antennapod.ui.echo.EchoActivity; -import de.danoeh.antennapod.ui.echo.EchoConfig; -import de.danoeh.antennapod.ui.home.HomeFragment; -import io.reactivex.Observable; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.Disposable; -import io.reactivex.schedulers.Schedulers; - -public class EchoSection extends Fragment { - private HomeSectionEchoBinding viewBinding; - private Disposable disposable; - - @Nullable - @Override - public View onCreateView(@NonNull LayoutInflater inflater, - @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - viewBinding = HomeSectionEchoBinding.inflate(inflater); - viewBinding.titleLabel.setText(getString(R.string.antennapod_echo_year, EchoConfig.RELEASE_YEAR)); - viewBinding.echoButton.setOnClickListener(v -> startActivity(new Intent(getContext(), EchoActivity.class))); - viewBinding.closeButton.setOnClickListener(v -> hideThisYear()); - updateVisibility(); - return viewBinding.getRoot(); - } - - private void updateVisibility() { - if (disposable != null) { - disposable.dispose(); - } - disposable = Observable.fromCallable( - () -> { - DBReader.StatisticsResult statisticsResult = DBReader.getStatistics( - false, EchoConfig.jan1(), Long.MAX_VALUE); - long totalTime = 0; - for (StatisticsItem feedTime : statisticsResult.feedTime) { - totalTime += feedTime.timePlayed; - } - return totalTime; - }) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(totalTime -> { - boolean shouldShow = (totalTime >= 3600 * 10); - viewBinding.getRoot().setVisibility(shouldShow ? View.VISIBLE : View.GONE); - if (!shouldShow) { - hideThisYear(); - } - }, Throwable::printStackTrace); - } - - void hideThisYear() { - getContext().getSharedPreferences(HomeFragment.PREF_NAME, Context.MODE_PRIVATE) - .edit().putInt(HomeFragment.PREF_HIDE_ECHO, EchoConfig.RELEASE_YEAR).apply(); - ((MainActivity) getActivity()).loadFragment(HomeFragment.TAG, null); - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/home/sections/EpisodesSurpriseSection.java b/app/src/main/java/de/danoeh/antennapod/ui/home/sections/EpisodesSurpriseSection.java deleted file mode 100644 index f93e28cbd..000000000 --- a/app/src/main/java/de/danoeh/antennapod/ui/home/sections/EpisodesSurpriseSection.java +++ /dev/null @@ -1,155 +0,0 @@ -package de.danoeh.antennapod.ui.home.sections; - -import android.os.Bundle; -import android.util.Log; -import android.view.ContextMenu; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.activity.MainActivity; -import de.danoeh.antennapod.adapter.HorizontalItemListAdapter; -import de.danoeh.antennapod.core.menuhandler.MenuItemUtils; -import de.danoeh.antennapod.storage.database.DBReader; -import de.danoeh.antennapod.core.util.FeedItemUtil; -import de.danoeh.antennapod.event.EpisodeDownloadEvent; -import de.danoeh.antennapod.event.FeedItemEvent; -import de.danoeh.antennapod.event.PlayerStatusEvent; -import de.danoeh.antennapod.event.playback.PlaybackPositionEvent; -import de.danoeh.antennapod.fragment.AllEpisodesFragment; -import de.danoeh.antennapod.model.feed.FeedItem; -import de.danoeh.antennapod.ui.home.HomeSection; -import de.danoeh.antennapod.view.viewholder.HorizontalItemViewHolder; -import io.reactivex.Observable; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.Disposable; -import io.reactivex.schedulers.Schedulers; -import org.greenrobot.eventbus.Subscribe; -import org.greenrobot.eventbus.ThreadMode; - -import java.util.ArrayList; -import java.util.List; -import java.util.Random; - -public class EpisodesSurpriseSection extends HomeSection { - public static final String TAG = "EpisodesSurpriseSection"; - private static final int NUM_EPISODES = 8; - private static int seed = 0; - private HorizontalItemListAdapter listAdapter; - private Disposable disposable; - private List episodes = new ArrayList<>(); - - @Nullable - @Override - public View onCreateView(@NonNull LayoutInflater inflater, - @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - final View view = super.onCreateView(inflater, container, savedInstanceState); - viewBinding.shuffleButton.setVisibility(View.VISIBLE); - viewBinding.shuffleButton.setOnClickListener(v -> { - seed = new Random().nextInt(); - viewBinding.recyclerView.scrollToPosition(0); - loadItems(); - }); - listAdapter = new HorizontalItemListAdapter((MainActivity) getActivity()) { - @Override - public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { - super.onCreateContextMenu(menu, v, menuInfo); - MenuItemUtils.setOnClickListeners(menu, EpisodesSurpriseSection.this::onContextItemSelected); - } - }; - listAdapter.setDummyViews(NUM_EPISODES); - viewBinding.recyclerView.setLayoutManager( - new LinearLayoutManager(getContext(), RecyclerView.HORIZONTAL, false)); - viewBinding.recyclerView.setAdapter(listAdapter); - int paddingHorizontal = (int) (12 * getResources().getDisplayMetrics().density); - viewBinding.recyclerView.setPadding(paddingHorizontal, 0, paddingHorizontal, 0); - if (seed == 0) { - seed = new Random().nextInt(); - } - return view; - } - - @Override - public void onStart() { - super.onStart(); - loadItems(); - } - - @Override - protected void handleMoreClick() { - ((MainActivity) requireActivity()).loadChildFragment(new AllEpisodesFragment()); - } - - @Override - protected String getSectionTitle() { - return getString(R.string.home_surprise_title); - } - - @Override - protected String getMoreLinkTitle() { - return getString(R.string.episodes_label); - } - - - @Subscribe(threadMode = ThreadMode.MAIN) - public void onPlayerStatusChanged(PlayerStatusEvent event) { - loadItems(); - } - - @Subscribe(threadMode = ThreadMode.MAIN) - public void onEventMainThread(FeedItemEvent event) { - Log.d(TAG, "onEventMainThread() called with: " + "event = [" + event + "]"); - for (int i = 0, size = event.items.size(); i < size; i++) { - FeedItem item = event.items.get(i); - int pos = FeedItemUtil.indexOfItemWithId(episodes, item.getId()); - if (pos >= 0) { - episodes.remove(pos); - episodes.add(pos, item); - listAdapter.notifyItemChangedCompat(pos); - } - } - } - - @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) - public void onEventMainThread(EpisodeDownloadEvent event) { - for (String downloadUrl : event.getUrls()) { - int pos = FeedItemUtil.indexOfItemWithDownloadUrl(episodes, downloadUrl); - if (pos >= 0) { - listAdapter.notifyItemChangedCompat(pos); - } - } - } - - @Subscribe(threadMode = ThreadMode.MAIN) - public void onEventMainThread(PlaybackPositionEvent event) { - if (listAdapter == null) { - return; - } - for (int i = 0; i < listAdapter.getItemCount(); i++) { - HorizontalItemViewHolder holder = (HorizontalItemViewHolder) - viewBinding.recyclerView.findViewHolderForAdapterPosition(i); - if (holder != null && holder.isCurrentlyPlayingItem()) { - holder.notifyPlaybackPositionUpdated(event); - break; - } - } - } - - private void loadItems() { - if (disposable != null) { - disposable.dispose(); - } - disposable = Observable.fromCallable(() -> DBReader.getRandomEpisodes(NUM_EPISODES, seed)) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(episodes -> { - this.episodes = episodes; - listAdapter.setDummyViews(0); - listAdapter.updateData(episodes); - }, error -> Log.e(TAG, Log.getStackTraceString(error))); - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/home/sections/InboxSection.java b/app/src/main/java/de/danoeh/antennapod/ui/home/sections/InboxSection.java deleted file mode 100644 index e5a72f72c..000000000 --- a/app/src/main/java/de/danoeh/antennapod/ui/home/sections/InboxSection.java +++ /dev/null @@ -1,141 +0,0 @@ -package de.danoeh.antennapod.ui.home.sections; - -import android.os.Bundle; -import android.util.Log; -import android.view.ContextMenu; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.util.Pair; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.activity.MainActivity; -import de.danoeh.antennapod.adapter.EpisodeItemListAdapter; -import de.danoeh.antennapod.core.menuhandler.MenuItemUtils; -import de.danoeh.antennapod.storage.database.DBReader; -import de.danoeh.antennapod.core.util.FeedItemUtil; -import de.danoeh.antennapod.event.EpisodeDownloadEvent; -import de.danoeh.antennapod.event.FeedItemEvent; -import de.danoeh.antennapod.event.FeedListUpdateEvent; -import de.danoeh.antennapod.event.UnreadItemsUpdateEvent; -import de.danoeh.antennapod.fragment.InboxFragment; -import de.danoeh.antennapod.fragment.swipeactions.SwipeActions; -import de.danoeh.antennapod.model.feed.FeedItem; -import de.danoeh.antennapod.model.feed.FeedItemFilter; -import de.danoeh.antennapod.storage.preferences.UserPreferences; -import de.danoeh.antennapod.ui.home.HomeSection; -import io.reactivex.Observable; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.Disposable; -import io.reactivex.schedulers.Schedulers; -import org.greenrobot.eventbus.Subscribe; -import org.greenrobot.eventbus.ThreadMode; - -import java.util.ArrayList; -import java.util.List; -import java.util.Locale; - -public class InboxSection extends HomeSection { - public static final String TAG = "InboxSection"; - private static final int NUM_EPISODES = 2; - private EpisodeItemListAdapter adapter; - private List items = new ArrayList<>(); - private Disposable disposable; - - @Nullable - @Override - public View onCreateView(@NonNull LayoutInflater inflater, - @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - final View view = super.onCreateView(inflater, container, savedInstanceState); - viewBinding.recyclerView.setPadding(0, 0, 0, 0); - viewBinding.recyclerView.setOverScrollMode(RecyclerView.OVER_SCROLL_NEVER); - viewBinding.recyclerView.setLayoutManager(new LinearLayoutManager(getContext(), RecyclerView.VERTICAL, false)); - viewBinding.recyclerView.setRecycledViewPool(((MainActivity) requireActivity()).getRecycledViewPool()); - adapter = new EpisodeItemListAdapter((MainActivity) requireActivity()) { - @Override - public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { - super.onCreateContextMenu(menu, v, menuInfo); - MenuItemUtils.setOnClickListeners(menu, InboxSection.this::onContextItemSelected); - } - }; - adapter.setDummyViews(NUM_EPISODES); - viewBinding.recyclerView.setAdapter(adapter); - - SwipeActions swipeActions = new SwipeActions(this, InboxFragment.TAG); - swipeActions.attachTo(viewBinding.recyclerView); - swipeActions.setFilter(new FeedItemFilter(FeedItemFilter.NEW)); - return view; - } - - @Override - public void onStart() { - super.onStart(); - loadItems(); - } - - @Override - protected void handleMoreClick() { - ((MainActivity) requireActivity()).loadChildFragment(new InboxFragment()); - } - - @Subscribe(threadMode = ThreadMode.MAIN) - public void onUnreadItemsChanged(UnreadItemsUpdateEvent event) { - loadItems(); - } - - @Subscribe(threadMode = ThreadMode.MAIN) - public void onEventMainThread(FeedItemEvent event) { - loadItems(); - } - - @Subscribe(threadMode = ThreadMode.MAIN) - public void onFeedListChanged(FeedListUpdateEvent event) { - loadItems(); - } - - @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) - public void onEventMainThread(EpisodeDownloadEvent event) { - for (String downloadUrl : event.getUrls()) { - int pos = FeedItemUtil.indexOfItemWithDownloadUrl(items, downloadUrl); - if (pos >= 0) { - adapter.notifyItemChangedCompat(pos); - } - } - } - - @Override - protected String getSectionTitle() { - return getString(R.string.home_new_title); - } - - @Override - protected String getMoreLinkTitle() { - return getString(R.string.inbox_label); - } - - private void loadItems() { - if (disposable != null) { - disposable.dispose(); - } - disposable = Observable.fromCallable(() -> - new Pair<>(DBReader.getEpisodes(0, NUM_EPISODES, - new FeedItemFilter(FeedItemFilter.NEW), UserPreferences.getInboxSortedOrder()), - DBReader.getTotalEpisodeCount(new FeedItemFilter(FeedItemFilter.NEW)))) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(data -> { - items = data.first; - adapter.setDummyViews(0); - adapter.updateItems(items); - viewBinding.numNewItemsLabel.setVisibility(View.VISIBLE); - if (data.second >= 100) { - viewBinding.numNewItemsLabel.setText(String.format(Locale.getDefault(), "%d+", 99)); - } else { - viewBinding.numNewItemsLabel.setText(String.format(Locale.getDefault(), "%d", data.second)); - } - }, error -> Log.e(TAG, Log.getStackTraceString(error))); - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/home/sections/QueueSection.java b/app/src/main/java/de/danoeh/antennapod/ui/home/sections/QueueSection.java deleted file mode 100644 index 4344f967c..000000000 --- a/app/src/main/java/de/danoeh/antennapod/ui/home/sections/QueueSection.java +++ /dev/null @@ -1,163 +0,0 @@ -package de.danoeh.antennapod.ui.home.sections; - -import android.os.Bundle; -import android.util.Log; -import android.view.ContextMenu; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.activity.MainActivity; -import de.danoeh.antennapod.adapter.HorizontalItemListAdapter; -import de.danoeh.antennapod.core.menuhandler.MenuItemUtils; -import de.danoeh.antennapod.storage.database.DBReader; -import de.danoeh.antennapod.core.util.FeedItemUtil; -import de.danoeh.antennapod.event.EpisodeDownloadEvent; -import de.danoeh.antennapod.event.FeedItemEvent; -import de.danoeh.antennapod.event.PlayerStatusEvent; -import de.danoeh.antennapod.event.QueueEvent; -import de.danoeh.antennapod.event.playback.PlaybackPositionEvent; -import de.danoeh.antennapod.fragment.QueueFragment; -import de.danoeh.antennapod.model.feed.FeedItem; -import de.danoeh.antennapod.ui.home.HomeSection; -import de.danoeh.antennapod.view.viewholder.HorizontalItemViewHolder; -import io.reactivex.Observable; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.Disposable; -import io.reactivex.schedulers.Schedulers; -import org.greenrobot.eventbus.Subscribe; -import org.greenrobot.eventbus.ThreadMode; - -import java.util.ArrayList; -import java.util.List; - -public class QueueSection extends HomeSection { - public static final String TAG = "QueueSection"; - private static final int NUM_EPISODES = 8; - private HorizontalItemListAdapter listAdapter; - private Disposable disposable; - private List queue = new ArrayList<>(); - - @Nullable - @Override - public View onCreateView(@NonNull LayoutInflater inflater, - @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - final View view = super.onCreateView(inflater, container, savedInstanceState); - listAdapter = new HorizontalItemListAdapter((MainActivity) getActivity()) { - @Override - public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { - super.onCreateContextMenu(menu, v, menuInfo); - MenuItemUtils.setOnClickListeners(menu, QueueSection.this::onContextItemSelected); - } - }; - listAdapter.setDummyViews(NUM_EPISODES); - viewBinding.recyclerView.setLayoutManager( - new LinearLayoutManager(getContext(), RecyclerView.HORIZONTAL, false)); - viewBinding.recyclerView.setAdapter(listAdapter); - int paddingHorizontal = (int) (12 * getResources().getDisplayMetrics().density); - viewBinding.recyclerView.setPadding(paddingHorizontal, 0, paddingHorizontal, 0); - return view; - } - - @Override - public void onStart() { - super.onStart(); - loadItems(); - } - - @Override - protected void handleMoreClick() { - ((MainActivity) requireActivity()).loadChildFragment(new QueueFragment()); - } - - @Subscribe(threadMode = ThreadMode.MAIN) - public void onQueueChanged(QueueEvent event) { - loadItems(); - } - - @Subscribe(threadMode = ThreadMode.MAIN) - public void onPlayerStatusChanged(PlayerStatusEvent event) { - loadItems(); - } - - @Subscribe(threadMode = ThreadMode.MAIN) - public void onEventMainThread(FeedItemEvent event) { - Log.d(TAG, "onEventMainThread() called with: " + "event = [" + event + "]"); - if (queue == null) { - return; - } - for (int i = 0, size = event.items.size(); i < size; i++) { - FeedItem item = event.items.get(i); - int pos = FeedItemUtil.indexOfItemWithId(queue, item.getId()); - if (pos >= 0) { - queue.remove(pos); - queue.add(pos, item); - listAdapter.notifyItemChangedCompat(pos); - } - } - } - - @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) - public void onEventMainThread(EpisodeDownloadEvent event) { - for (String downloadUrl : event.getUrls()) { - int pos = FeedItemUtil.indexOfItemWithDownloadUrl(queue, downloadUrl); - if (pos >= 0) { - listAdapter.notifyItemChangedCompat(pos); - } - } - } - - @Subscribe(threadMode = ThreadMode.MAIN) - public void onEventMainThread(PlaybackPositionEvent event) { - if (listAdapter == null) { - return; - } - boolean foundCurrentlyPlayingItem = false; - boolean currentlyPlayingItemIsFirst = true; - for (int i = 0; i < listAdapter.getItemCount(); i++) { - HorizontalItemViewHolder holder = (HorizontalItemViewHolder) - viewBinding.recyclerView.findViewHolderForAdapterPosition(i); - if (holder == null) { - continue; - } - if (holder.isCurrentlyPlayingItem()) { - holder.notifyPlaybackPositionUpdated(event); - foundCurrentlyPlayingItem = true; - currentlyPlayingItemIsFirst = (i == 0); - break; - } - } - if (!foundCurrentlyPlayingItem || !currentlyPlayingItemIsFirst) { - loadItems(); - } - } - - @Override - protected String getSectionTitle() { - return getString(R.string.home_continue_title); - } - - @Override - protected String getMoreLinkTitle() { - return getString(R.string.queue_label); - } - - private void loadItems() { - if (disposable != null) { - disposable.dispose(); - } - disposable = Observable.fromCallable(() -> DBReader.getPausedQueue(NUM_EPISODES)) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(queue -> { - this.queue = queue; - listAdapter.setDummyViews(0); - listAdapter.updateData(queue); - }, error -> Log.e(TAG, Log.getStackTraceString(error))); - - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/home/sections/SubscriptionsSection.java b/app/src/main/java/de/danoeh/antennapod/ui/home/sections/SubscriptionsSection.java deleted file mode 100644 index b97254d49..000000000 --- a/app/src/main/java/de/danoeh/antennapod/ui/home/sections/SubscriptionsSection.java +++ /dev/null @@ -1,111 +0,0 @@ -package de.danoeh.antennapod.ui.home.sections; - -import android.content.Context; -import android.content.SharedPreferences; -import android.os.Bundle; -import android.util.Log; -import android.view.ContextMenu; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.activity.MainActivity; -import de.danoeh.antennapod.adapter.HorizontalFeedListAdapter; -import de.danoeh.antennapod.core.menuhandler.MenuItemUtils; -import de.danoeh.antennapod.storage.database.DBReader; -import de.danoeh.antennapod.event.FeedListUpdateEvent; -import de.danoeh.antennapod.fragment.SubscriptionFragment; -import de.danoeh.antennapod.model.feed.Feed; -import de.danoeh.antennapod.ui.home.HomeSection; -import de.danoeh.antennapod.ui.statistics.StatisticsFragment; -import io.reactivex.Observable; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.Disposable; -import io.reactivex.schedulers.Schedulers; -import org.greenrobot.eventbus.Subscribe; -import org.greenrobot.eventbus.ThreadMode; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -public class SubscriptionsSection extends HomeSection { - public static final String TAG = "SubscriptionsSection"; - private static final int NUM_FEEDS = 8; - private HorizontalFeedListAdapter listAdapter; - private Disposable disposable; - - @Nullable - @Override - public View onCreateView(@NonNull LayoutInflater inflater, - @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - final View view = super.onCreateView(inflater, container, savedInstanceState); - viewBinding.recyclerView.setLayoutManager( - new LinearLayoutManager(getActivity(), RecyclerView.HORIZONTAL, false)); - listAdapter = new HorizontalFeedListAdapter((MainActivity) getActivity()) { - @Override - public void onCreateContextMenu(ContextMenu contextMenu, View view, - ContextMenu.ContextMenuInfo contextMenuInfo) { - super.onCreateContextMenu(contextMenu, view, contextMenuInfo); - MenuItemUtils.setOnClickListeners(contextMenu, SubscriptionsSection.this::onContextItemSelected); - } - }; - listAdapter.setDummyViews(NUM_FEEDS); - viewBinding.recyclerView.setAdapter(listAdapter); - int paddingHorizontal = (int) (12 * getResources().getDisplayMetrics().density); - viewBinding.recyclerView.setPadding(paddingHorizontal, 0, paddingHorizontal, 0); - return view; - } - - @Override - public void onStart() { - super.onStart(); - loadItems(); - } - - @Override - protected void handleMoreClick() { - ((MainActivity) requireActivity()).loadChildFragment(new SubscriptionFragment()); - } - - @Subscribe(threadMode = ThreadMode.MAIN) - public void onFeedListChanged(FeedListUpdateEvent event) { - loadItems(); - } - - @Override - protected String getSectionTitle() { - return getString(R.string.home_classics_title); - } - - @Override - protected String getMoreLinkTitle() { - return getString(R.string.subscriptions_label); - } - - private void loadItems() { - if (disposable != null) { - disposable.dispose(); - } - SharedPreferences prefs = getContext().getSharedPreferences(StatisticsFragment.PREF_NAME, Context.MODE_PRIVATE); - boolean includeMarkedAsPlayed = prefs.getBoolean(StatisticsFragment.PREF_INCLUDE_MARKED_PLAYED, false); - disposable = Observable.fromCallable(() -> - DBReader.getStatistics(includeMarkedAsPlayed, 0, Long.MAX_VALUE).feedTime) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(statisticsData -> { - Collections.sort(statisticsData, (item1, item2) -> - Long.compare(item2.timePlayed, item1.timePlayed)); - List feeds = new ArrayList<>(); - for (int i = 0; i < statisticsData.size() && i < NUM_FEEDS; i++) { - feeds.add(statisticsData.get(i).feed); - } - listAdapter.setDummyViews(0); - listAdapter.updateData(feeds); - }, error -> Log.e(TAG, Log.getStackTraceString(error))); - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/screen/AddFeedFragment.java b/app/src/main/java/de/danoeh/antennapod/ui/screen/AddFeedFragment.java new file mode 100644 index 000000000..0b95fd601 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/screen/AddFeedFragment.java @@ -0,0 +1,217 @@ +package de.danoeh.antennapod.ui.screen; + +import android.content.ActivityNotFoundException; +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.inputmethod.InputMethodManager; + +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; +import androidx.activity.result.contract.ActivityResultContracts.GetContent; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import androidx.documentfile.provider.DocumentFile; +import androidx.fragment.app.Fragment; +import com.google.android.material.snackbar.Snackbar; + +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.activity.OpmlImportActivity; +import de.danoeh.antennapod.model.feed.Feed; +import de.danoeh.antennapod.net.download.serviceinterface.FeedUpdateManager; +import de.danoeh.antennapod.storage.database.FeedDatabaseWriter; +import de.danoeh.antennapod.model.feed.SortOrder; +import de.danoeh.antennapod.databinding.AddfeedBinding; +import de.danoeh.antennapod.databinding.EditTextDialogBinding; +import de.danoeh.antennapod.net.discovery.CombinedSearcher; +import de.danoeh.antennapod.net.discovery.FyydPodcastSearcher; +import de.danoeh.antennapod.net.discovery.ItunesPodcastSearcher; +import de.danoeh.antennapod.net.discovery.PodcastIndexPodcastSearcher; +import de.danoeh.antennapod.ui.appstartintent.OnlineFeedviewActivityStarter; +import de.danoeh.antennapod.ui.discovery.OnlineSearchFragment; +import de.danoeh.antennapod.ui.screen.feed.FeedItemlistFragment; +import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.schedulers.Schedulers; + +import java.util.Collections; + +/** + * Provides actions for adding new podcast subscriptions. + */ +public class AddFeedFragment extends Fragment { + + public static final String TAG = "AddFeedFragment"; + private static final String KEY_UP_ARROW = "up_arrow"; + + private AddfeedBinding viewBinding; + private MainActivity activity; + private boolean displayUpArrow; + + private final ActivityResultLauncher chooseOpmlImportPathLauncher = + registerForActivityResult(new GetContent(), this::chooseOpmlImportPathResult); + private final ActivityResultLauncher addLocalFolderLauncher = + registerForActivityResult(new AddLocalFolder(), this::addLocalFolderResult); + + @Override + @Nullable + public View onCreateView(@NonNull LayoutInflater inflater, + @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + super.onCreateView(inflater, container, savedInstanceState); + viewBinding = AddfeedBinding.inflate(inflater); + activity = (MainActivity) getActivity(); + + displayUpArrow = getParentFragmentManager().getBackStackEntryCount() != 0; + if (savedInstanceState != null) { + displayUpArrow = savedInstanceState.getBoolean(KEY_UP_ARROW); + } + ((MainActivity) getActivity()).setupToolbarToggle(viewBinding.toolbar, displayUpArrow); + + viewBinding.searchItunesButton.setOnClickListener(v + -> activity.loadChildFragment(OnlineSearchFragment.newInstance(ItunesPodcastSearcher.class))); + viewBinding.searchFyydButton.setOnClickListener(v + -> activity.loadChildFragment(OnlineSearchFragment.newInstance(FyydPodcastSearcher.class))); + viewBinding.searchPodcastIndexButton.setOnClickListener(v + -> activity.loadChildFragment(OnlineSearchFragment.newInstance(PodcastIndexPodcastSearcher.class))); + + viewBinding.combinedFeedSearchEditText.setOnEditorActionListener((v, actionId, event) -> { + performSearch(); + return true; + }); + + viewBinding.addViaUrlButton.setOnClickListener(v + -> showAddViaUrlDialog()); + + viewBinding.opmlImportButton.setOnClickListener(v -> { + try { + chooseOpmlImportPathLauncher.launch("*/*"); + } catch (ActivityNotFoundException e) { + e.printStackTrace(); + ((MainActivity) getActivity()) + .showSnackbarAbovePlayer(R.string.unable_to_start_system_file_manager, Snackbar.LENGTH_LONG); + } + }); + + viewBinding.addLocalFolderButton.setOnClickListener(v -> { + try { + addLocalFolderLauncher.launch(null); + } catch (ActivityNotFoundException e) { + e.printStackTrace(); + ((MainActivity) getActivity()) + .showSnackbarAbovePlayer(R.string.unable_to_start_system_file_manager, Snackbar.LENGTH_LONG); + } + }); + viewBinding.searchButton.setOnClickListener(view -> performSearch()); + + return viewBinding.getRoot(); + } + + @Override + public void onSaveInstanceState(@NonNull Bundle outState) { + outState.putBoolean(KEY_UP_ARROW, displayUpArrow); + super.onSaveInstanceState(outState); + } + + private void showAddViaUrlDialog() { + MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(getContext()); + builder.setTitle(R.string.add_podcast_by_url); + final EditTextDialogBinding dialogBinding = EditTextDialogBinding.inflate(getLayoutInflater()); + dialogBinding.urlEditText.setHint(R.string.add_podcast_by_url_hint); + + ClipboardManager clipboard = (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE); + final ClipData clipData = clipboard.getPrimaryClip(); + if (clipData != null && clipData.getItemCount() > 0 && clipData.getItemAt(0).getText() != null) { + final String clipboardContent = clipData.getItemAt(0).getText().toString(); + if (clipboardContent.trim().startsWith("http")) { + dialogBinding.urlEditText.setText(clipboardContent.trim()); + } + } + builder.setView(dialogBinding.getRoot()); + builder.setPositiveButton(R.string.confirm_label, + (dialog, which) -> addUrl(dialogBinding.urlEditText.getText().toString())); + builder.setNegativeButton(R.string.cancel_label, null); + builder.show(); + } + + private void addUrl(String url) { + startActivity(new OnlineFeedviewActivityStarter(getContext(), url).withManualUrl().getIntent()); + } + + private void performSearch() { + viewBinding.combinedFeedSearchEditText.clearFocus(); + InputMethodManager in = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE); + in.hideSoftInputFromWindow(viewBinding.combinedFeedSearchEditText.getWindowToken(), 0); + String query = viewBinding.combinedFeedSearchEditText.getText().toString(); + if (query.matches("http[s]?://.*")) { + addUrl(query); + return; + } + activity.loadChildFragment(OnlineSearchFragment.newInstance(CombinedSearcher.class, query)); + viewBinding.combinedFeedSearchEditText.post(() -> viewBinding.combinedFeedSearchEditText.setText("")); + } + + private void chooseOpmlImportPathResult(final Uri uri) { + if (uri == null) { + return; + } + final Intent intent = new Intent(getContext(), OpmlImportActivity.class); + intent.setData(uri); + startActivity(intent); + } + + private void addLocalFolderResult(final Uri uri) { + if (uri == null) { + return; + } + Observable.fromCallable(() -> addLocalFolder(uri)) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + feed -> { + Fragment fragment = FeedItemlistFragment.newInstance(feed.getId()); + ((MainActivity) getActivity()).loadChildFragment(fragment); + }, error -> { + Log.e(TAG, Log.getStackTraceString(error)); + ((MainActivity) getActivity()) + .showSnackbarAbovePlayer(error.getLocalizedMessage(), Snackbar.LENGTH_LONG); + }); + } + + private Feed addLocalFolder(Uri uri) { + getActivity().getContentResolver().takePersistableUriPermission(uri, + Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + DocumentFile documentFile = DocumentFile.fromTreeUri(getContext(), uri); + if (documentFile == null) { + throw new IllegalArgumentException("Unable to retrieve document tree"); + } + String title = documentFile.getName(); + if (title == null) { + title = getString(R.string.local_folder); + } + Feed dirFeed = new Feed(Feed.PREFIX_LOCAL_FOLDER + uri.toString(), null, title); + dirFeed.setItems(Collections.emptyList()); + dirFeed.setSortOrder(SortOrder.EPISODE_TITLE_A_Z); + Feed fromDatabase = FeedDatabaseWriter.updateFeed(getContext(), dirFeed, false); + FeedUpdateManager.getInstance().runOnce(requireContext(), fromDatabase); + return fromDatabase; + } + + private static class AddLocalFolder extends ActivityResultContracts.OpenDocumentTree { + @NonNull + @Override + public Intent createIntent(@NonNull final Context context, @Nullable final Uri input) { + return super.createIntent(context, input) + .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/screen/AllEpisodesFragment.java b/app/src/main/java/de/danoeh/antennapod/ui/screen/AllEpisodesFragment.java new file mode 100644 index 000000000..36466c990 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/screen/AllEpisodesFragment.java @@ -0,0 +1,148 @@ +package de.danoeh.antennapod.ui.screen; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.storage.database.DBReader; +import de.danoeh.antennapod.ui.AllEpisodesFilterDialog; +import de.danoeh.antennapod.ui.screen.feed.ItemSortDialog; +import de.danoeh.antennapod.event.FeedListUpdateEvent; +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedItemFilter; +import de.danoeh.antennapod.model.feed.SortOrder; +import de.danoeh.antennapod.storage.preferences.UserPreferences; +import de.danoeh.antennapod.ui.episodeslist.EpisodesListFragment; +import org.apache.commons.lang3.StringUtils; +import org.greenrobot.eventbus.EventBus; +import org.greenrobot.eventbus.Subscribe; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; + +/** + * Shows all episodes (possibly filtered by user). + */ +public class AllEpisodesFragment extends EpisodesListFragment { + public static final String TAG = "EpisodesFragment"; + public static final String PREF_NAME = "PrefAllEpisodesFragment"; + + @NonNull + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + final View root = super.onCreateView(inflater, container, savedInstanceState); + toolbar.inflateMenu(R.menu.episodes); + toolbar.setTitle(R.string.episodes_label); + updateToolbar(); + updateFilterUi(); + txtvInformation.setOnClickListener( + v -> AllEpisodesFilterDialog.newInstance(getFilter()).show(getChildFragmentManager(), null)); + return root; + } + + @NonNull + @Override + protected List loadData() { + return DBReader.getEpisodes(0, page * EPISODES_PER_PAGE, getFilter(), + UserPreferences.getAllEpisodesSortOrder()); + } + + @NonNull + @Override + protected List loadMoreData(int page) { + return DBReader.getEpisodes((page - 1) * EPISODES_PER_PAGE, EPISODES_PER_PAGE, getFilter(), + UserPreferences.getAllEpisodesSortOrder()); + } + + @Override + protected int loadTotalItemCount() { + return DBReader.getTotalEpisodeCount(getFilter()); + } + + @Override + protected FeedItemFilter getFilter() { + return new FeedItemFilter(UserPreferences.getPrefFilterAllEpisodes()); + } + + @Override + protected String getFragmentTag() { + return TAG; + } + + @Override + protected String getPrefName() { + return PREF_NAME; + } + + @Override + public boolean onMenuItemClick(MenuItem item) { + if (super.onMenuItemClick(item)) { + return true; + } + if (item.getItemId() == R.id.filter_items) { + AllEpisodesFilterDialog.newInstance(getFilter()).show(getChildFragmentManager(), null); + return true; + } else if (item.getItemId() == R.id.action_favorites) { + ArrayList filter = new ArrayList<>(getFilter().getValuesList()); + if (filter.contains(FeedItemFilter.IS_FAVORITE)) { + filter.remove(FeedItemFilter.IS_FAVORITE); + } else { + filter.add(FeedItemFilter.IS_FAVORITE); + } + onFilterChanged(new AllEpisodesFilterDialog.AllEpisodesFilterChangedEvent(new HashSet<>(filter))); + return true; + } else if (item.getItemId() == R.id.episodes_sort) { + new AllEpisodesSortDialog().show(getChildFragmentManager().beginTransaction(), "SortDialog"); + return true; + } + return false; + } + + @Subscribe + public void onFilterChanged(AllEpisodesFilterDialog.AllEpisodesFilterChangedEvent event) { + UserPreferences.setPrefFilterAllEpisodes(StringUtils.join(event.filterValues, ",")); + updateFilterUi(); + page = 1; + loadItems(); + } + + private void updateFilterUi() { + swipeActions.setFilter(getFilter()); + if (getFilter().getValues().length > 0) { + txtvInformation.setVisibility(View.VISIBLE); + emptyView.setMessage(R.string.no_all_episodes_filtered_label); + } else { + txtvInformation.setVisibility(View.GONE); + emptyView.setMessage(R.string.no_all_episodes_label); + } + toolbar.getMenu().findItem(R.id.action_favorites).setIcon( + getFilter().showIsFavorite ? R.drawable.ic_star : R.drawable.ic_star_border); + } + + public static class AllEpisodesSortDialog extends ItemSortDialog { + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + sortOrder = UserPreferences.getAllEpisodesSortOrder(); + } + + @Override + protected void onAddItem(int title, SortOrder ascending, SortOrder descending, boolean ascendingIsDefault) { + if (ascending == SortOrder.DATE_OLD_NEW || ascending == SortOrder.DURATION_SHORT_LONG) { + super.onAddItem(title, ascending, descending, ascendingIsDefault); + } + } + + @Override + protected void onSelectionChanged() { + super.onSelectionChanged(); + UserPreferences.setAllEpisodesSortOrder(sortOrder); + EventBus.getDefault().post(new FeedListUpdateEvent(0)); + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/screen/InboxFragment.java b/app/src/main/java/de/danoeh/antennapod/ui/screen/InboxFragment.java new file mode 100644 index 000000000..5ac6e5bef --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/screen/InboxFragment.java @@ -0,0 +1,154 @@ +package de.danoeh.antennapod.ui.screen; + +import android.content.Context; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CheckBox; +import android.widget.Toast; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.storage.database.DBReader; +import de.danoeh.antennapod.storage.database.DBWriter; +import de.danoeh.antennapod.ui.screen.feed.ItemSortDialog; +import de.danoeh.antennapod.event.FeedListUpdateEvent; +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedItemFilter; +import de.danoeh.antennapod.model.feed.SortOrder; +import de.danoeh.antennapod.storage.preferences.UserPreferences; +import de.danoeh.antennapod.ui.episodeslist.EpisodesListFragment; +import org.greenrobot.eventbus.EventBus; + +import java.util.List; + +/** + * Like 'EpisodesFragment' except that it only shows new episodes and + * supports swiping to mark as read. + */ +public class InboxFragment extends EpisodesListFragment { + public static final String TAG = "NewEpisodesFragment"; + private static final String PREF_NAME = "PrefNewEpisodesFragment"; + private static final String PREF_DO_NOT_PROMPT_REMOVE_ALL_FROM_INBOX = "prefDoNotPromptRemovalAllFromInbox"; + private SharedPreferences prefs; + + @NonNull + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + final View root = super.onCreateView(inflater, container, savedInstanceState); + toolbar.inflateMenu(R.menu.inbox); + toolbar.setTitle(R.string.inbox_label); + prefs = getActivity().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); + updateToolbar(); + emptyView.setIcon(R.drawable.ic_inbox); + emptyView.setTitle(R.string.no_inbox_head_label); + emptyView.setMessage(R.string.no_inbox_label); + speedDialView.removeActionItemById(R.id.mark_unread_batch); + speedDialView.removeActionItemById(R.id.remove_from_queue_batch); + speedDialView.removeActionItemById(R.id.delete_batch); + return root; + } + + @Override + protected FeedItemFilter getFilter() { + return new FeedItemFilter(FeedItemFilter.NEW); + } + + @Override + protected String getFragmentTag() { + return TAG; + } + + @Override + protected String getPrefName() { + return PREF_NAME; + } + + @Override + public boolean onMenuItemClick(MenuItem item) { + if (super.onMenuItemClick(item)) { + return true; + } + if (item.getItemId() == R.id.remove_all_inbox_item) { + if (prefs.getBoolean(PREF_DO_NOT_PROMPT_REMOVE_ALL_FROM_INBOX, false)) { + removeAllFromInbox(); + } else { + showRemoveAllDialog(); + } + return true; + } else if (item.getItemId() == R.id.inbox_sort) { + new InboxSortDialog().show(getChildFragmentManager(), "SortDialog"); + return true; + } + return false; + } + + @NonNull + @Override + protected List loadData() { + return DBReader.getEpisodes(0, page * EPISODES_PER_PAGE, + new FeedItemFilter(FeedItemFilter.NEW), UserPreferences.getInboxSortedOrder()); + } + + @NonNull + @Override + protected List loadMoreData(int page) { + return DBReader.getEpisodes((page - 1) * EPISODES_PER_PAGE, EPISODES_PER_PAGE, + new FeedItemFilter(FeedItemFilter.NEW), UserPreferences.getInboxSortedOrder()); + } + + @Override + protected int loadTotalItemCount() { + return DBReader.getTotalEpisodeCount(new FeedItemFilter(FeedItemFilter.NEW)); + } + + private void removeAllFromInbox() { + DBWriter.removeAllNewFlags(); + ((MainActivity) getActivity()).showSnackbarAbovePlayer(R.string.removed_all_inbox_msg, Toast.LENGTH_SHORT); + } + + private void showRemoveAllDialog() { + MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(getContext()); + builder.setTitle(R.string.remove_all_inbox_label); + builder.setMessage(R.string.remove_all_inbox_confirmation_msg); + + View view = View.inflate(getContext(), R.layout.checkbox_do_not_show_again, null); + CheckBox checkNeverAskAgain = view.findViewById(R.id.checkbox_do_not_show_again); + builder.setView(view); + + builder.setPositiveButton(R.string.confirm_label, (dialog, which) -> { + dialog.dismiss(); + removeAllFromInbox(); + prefs.edit().putBoolean(PREF_DO_NOT_PROMPT_REMOVE_ALL_FROM_INBOX, checkNeverAskAgain.isChecked()).apply(); + }); + builder.setNegativeButton(R.string.cancel_label, null); + builder.show(); + } + + public static class InboxSortDialog extends ItemSortDialog { + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + sortOrder = UserPreferences.getInboxSortedOrder(); + } + + @Override + protected void onAddItem(int title, SortOrder ascending, SortOrder descending, boolean ascendingIsDefault) { + if (ascending == SortOrder.DATE_OLD_NEW || ascending == SortOrder.DURATION_SHORT_LONG) { + super.onAddItem(title, ascending, descending, ascendingIsDefault); + } + } + + @Override + protected void onSelectionChanged() { + super.onSelectionChanged(); + UserPreferences.setInboxSortedOrder(sortOrder); + EventBus.getDefault().post(new FeedListUpdateEvent(0)); + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/screen/PlaybackHistoryFragment.java b/app/src/main/java/de/danoeh/antennapod/ui/screen/PlaybackHistoryFragment.java new file mode 100644 index 000000000..4efdf38a3 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/screen/PlaybackHistoryFragment.java @@ -0,0 +1,110 @@ +package de.danoeh.antennapod.ui.screen; + +import android.content.DialogInterface; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import androidx.annotation.NonNull; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.util.ConfirmationDialog; +import de.danoeh.antennapod.storage.database.DBReader; +import de.danoeh.antennapod.storage.database.DBWriter; +import de.danoeh.antennapod.event.playback.PlaybackHistoryEvent; +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedItemFilter; +import de.danoeh.antennapod.model.feed.SortOrder; +import de.danoeh.antennapod.ui.episodeslist.EpisodesListFragment; +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; + +import java.util.List; + +public class PlaybackHistoryFragment extends EpisodesListFragment { + public static final String TAG = "PlaybackHistoryFragment"; + private static final FeedItemFilter FILTER_HISTORY = new FeedItemFilter(FeedItemFilter.IS_IN_HISTORY); + + @NonNull + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + final View root = super.onCreateView(inflater, container, savedInstanceState); + toolbar.inflateMenu(R.menu.playback_history); + toolbar.setTitle(R.string.playback_history_label); + updateToolbar(); + emptyView.setIcon(R.drawable.ic_history); + emptyView.setTitle(R.string.no_history_head_label); + emptyView.setMessage(R.string.no_history_label); + return root; + } + + @Override + protected FeedItemFilter getFilter() { + return FeedItemFilter.unfiltered(); + } + + @Override + protected String getFragmentTag() { + return TAG; + } + + @Override + protected String getPrefName() { + return TAG; + } + + @Override + public boolean onMenuItemClick(MenuItem item) { + if (super.onMenuItemClick(item)) { + return true; + } + if (item.getItemId() == R.id.clear_history_item) { + + ConfirmationDialog conDialog = new ConfirmationDialog( + getActivity(), + R.string.clear_history_label, + R.string.clear_playback_history_msg) { + + @Override + public void onConfirmButtonPressed(DialogInterface dialog) { + dialog.dismiss(); + DBWriter.clearPlaybackHistory(); + } + }; + conDialog.createNewDialog().show(); + + return true; + } + return false; + } + + @Override + protected void updateToolbar() { + // Not calling super, as we do not have a refresh button that could be updated + toolbar.getMenu().findItem(R.id.clear_history_item).setVisible(!episodes.isEmpty()); + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onHistoryUpdated(PlaybackHistoryEvent event) { + loadItems(); + updateToolbar(); + } + + @NonNull + @Override + protected List loadData() { + return DBReader.getEpisodes(0, page * EPISODES_PER_PAGE, FILTER_HISTORY, SortOrder.COMPLETION_DATE_NEW_OLD); + } + + @NonNull + @Override + protected List loadMoreData(int page) { + return DBReader.getEpisodes((page - 1) * EPISODES_PER_PAGE, EPISODES_PER_PAGE, FILTER_HISTORY, + SortOrder.COMPLETION_DATE_NEW_OLD); + } + + @Override + protected int loadTotalItemCount() { + return DBReader.getTotalEpisodeCount(FILTER_HISTORY); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/screen/SearchFragment.java b/app/src/main/java/de/danoeh/antennapod/ui/screen/SearchFragment.java new file mode 100644 index 000000000..8e21117b3 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/screen/SearchFragment.java @@ -0,0 +1,460 @@ +package de.danoeh.antennapod.ui.screen; + + +import android.content.Context; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; +import android.util.Pair; +import android.view.ContextMenu; +import android.view.LayoutInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.view.inputmethod.InputMethodManager; +import android.widget.ProgressBar; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.widget.SearchView; +import com.google.android.material.appbar.MaterialToolbar; +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.google.android.material.chip.Chip; +import com.google.android.material.snackbar.Snackbar; +import com.leinardi.android.speeddial.SpeedDialView; + +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.ui.episodeslist.EpisodeItemListAdapter; +import de.danoeh.antennapod.ui.screen.subscriptions.HorizontalFeedListAdapter; +import de.danoeh.antennapod.ui.MenuItemUtils; +import de.danoeh.antennapod.databinding.MultiSelectSpeedDialBinding; +import de.danoeh.antennapod.event.EpisodeDownloadEvent; +import de.danoeh.antennapod.event.FeedItemEvent; +import de.danoeh.antennapod.event.playback.PlaybackPositionEvent; +import de.danoeh.antennapod.event.PlayerStatusEvent; +import de.danoeh.antennapod.event.UnreadItemsUpdateEvent; +import de.danoeh.antennapod.ui.episodeslist.EpisodeMultiSelectActionHandler; +import de.danoeh.antennapod.model.feed.Feed; +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.core.util.FeedItemUtil; +import de.danoeh.antennapod.ui.episodeslist.FeedItemMenuHandler; +import de.danoeh.antennapod.net.discovery.CombinedSearcher; +import de.danoeh.antennapod.storage.database.DBReader; +import de.danoeh.antennapod.ui.appstartintent.OnlineFeedviewActivityStarter; +import de.danoeh.antennapod.ui.discovery.OnlineSearchFragment; +import de.danoeh.antennapod.ui.view.EmptyViewHandler; +import de.danoeh.antennapod.ui.episodeslist.EpisodeItemListRecyclerView; +import de.danoeh.antennapod.ui.view.LiftOnScrollListener; +import de.danoeh.antennapod.ui.episodeslist.EpisodeItemViewHolder; +import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; +import io.reactivex.schedulers.Schedulers; +import org.greenrobot.eventbus.EventBus; +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; + +import java.util.Collections; +import java.util.List; +import de.danoeh.antennapod.ui.screen.subscriptions.FeedMenuHandler; +import de.danoeh.antennapod.event.FeedListUpdateEvent; + + +/** + * Performs a search operation on all feeds or one specific feed and displays the search result. + */ +public class SearchFragment extends Fragment implements EpisodeItemListAdapter.OnSelectModeListener { + private static final String TAG = "SearchFragment"; + private static final String ARG_QUERY = "query"; + private static final String ARG_FEED = "feed"; + private static final String ARG_FEED_NAME = "feedName"; + private static final int SEARCH_DEBOUNCE_INTERVAL = 1500; + + private EpisodeItemListAdapter adapter; + private HorizontalFeedListAdapter adapterFeeds; + private Disposable disposable; + private ProgressBar progressBar; + private EmptyViewHandler emptyViewHandler; + private EpisodeItemListRecyclerView recyclerView; + private List results; + private Chip chip; + private SearchView searchView; + private Handler automaticSearchDebouncer; + private long lastQueryChange = 0; + private MultiSelectSpeedDialBinding speedDialBinding; + private boolean isOtherViewInFoucus = false; + + + /** + * Create a new SearchFragment that searches all feeds. + */ + public static SearchFragment newInstance() { + SearchFragment fragment = new SearchFragment(); + Bundle args = new Bundle(); + args.putLong(ARG_FEED, 0); + fragment.setArguments(args); + return fragment; + } + + /** + * Create a new SearchFragment that searches all feeds with pre-defined query. + */ + public static SearchFragment newInstance(String query) { + SearchFragment fragment = newInstance(); + fragment.getArguments().putString(ARG_QUERY, query); + return fragment; + } + + /** + * Create a new SearchFragment that searches one specific feed. + */ + public static SearchFragment newInstance(long feed, String feedTitle) { + SearchFragment fragment = newInstance(); + fragment.getArguments().putLong(ARG_FEED, feed); + fragment.getArguments().putString(ARG_FEED_NAME, feedTitle); + return fragment; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + automaticSearchDebouncer = new Handler(Looper.getMainLooper()); + } + + @Override + public void onStop() { + super.onStop(); + if (disposable != null) { + disposable.dispose(); + } + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + View layout = inflater.inflate(R.layout.search_fragment, container, false); + setupToolbar(layout.findViewById(R.id.toolbar)); + speedDialBinding = MultiSelectSpeedDialBinding.bind(layout); + progressBar = layout.findViewById(R.id.progressBar); + recyclerView = layout.findViewById(R.id.recyclerView); + recyclerView.setRecycledViewPool(((MainActivity) getActivity()).getRecycledViewPool()); + registerForContextMenu(recyclerView); + adapter = new EpisodeItemListAdapter((MainActivity) getActivity()) { + @Override + public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { + super.onCreateContextMenu(menu, v, menuInfo); + if (!inActionMode()) { + menu.findItem(R.id.multi_select).setVisible(true); + } + MenuItemUtils.setOnClickListeners(menu, SearchFragment.this::onContextItemSelected); + } + }; + adapter.setOnSelectModeListener(this); + recyclerView.setAdapter(adapter); + recyclerView.addOnScrollListener(new LiftOnScrollListener(layout.findViewById(R.id.appbar))); + + RecyclerView recyclerViewFeeds = layout.findViewById(R.id.recyclerViewFeeds); + LinearLayoutManager layoutManagerFeeds = new LinearLayoutManager(getActivity()); + layoutManagerFeeds.setOrientation(RecyclerView.HORIZONTAL); + recyclerViewFeeds.setLayoutManager(layoutManagerFeeds); + adapterFeeds = new HorizontalFeedListAdapter((MainActivity) getActivity()) { + @Override + public void onCreateContextMenu(ContextMenu contextMenu, View view, + ContextMenu.ContextMenuInfo contextMenuInfo) { + super.onCreateContextMenu(contextMenu, view, contextMenuInfo); + MenuItemUtils.setOnClickListeners(contextMenu, SearchFragment.this::onContextItemSelected); + } + }; + recyclerViewFeeds.setAdapter(adapterFeeds); + + emptyViewHandler = new EmptyViewHandler(getContext()); + emptyViewHandler.attachToRecyclerView(recyclerView); + emptyViewHandler.setIcon(R.drawable.ic_search); + emptyViewHandler.setTitle(R.string.search_status_no_results); + emptyViewHandler.setMessage(R.string.type_to_search); + EventBus.getDefault().register(this); + + chip = layout.findViewById(R.id.feed_title_chip); + chip.setOnCloseIconClickListener(v -> { + getArguments().putLong(ARG_FEED, 0); + searchWithProgressBar(); + }); + chip.setVisibility((getArguments().getLong(ARG_FEED, 0) == 0) ? View.GONE : View.VISIBLE); + chip.setText(getArguments().getString(ARG_FEED_NAME, "")); + if (getArguments().getString(ARG_QUERY, null) != null) { + search(); + } + searchView.setOnQueryTextFocusChangeListener((view, hasFocus) -> { + if (hasFocus && !isOtherViewInFoucus) { + showInputMethod(view.findFocus()); + } + }); + recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { + @Override + public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) { + super.onScrollStateChanged(recyclerView, newState); + if (newState == RecyclerView.SCROLL_STATE_DRAGGING) { + InputMethodManager imm = (InputMethodManager) + getActivity().getSystemService(Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(recyclerView.getWindowToken(), 0); + } + } + }); + speedDialBinding.fabSD.setOverlayLayout(speedDialBinding.fabSDOverlay); + speedDialBinding.fabSD.inflate(R.menu.episodes_apply_action_speeddial); + speedDialBinding.fabSD.setOnChangeListener(new SpeedDialView.OnChangeListener() { + @Override + public boolean onMainActionSelected() { + return false; + } + + @Override + public void onToggleChanged(boolean open) { + if (open && adapter.getSelectedCount() == 0) { + ((MainActivity) getActivity()) + .showSnackbarAbovePlayer(R.string.no_items_selected, Snackbar.LENGTH_SHORT); + speedDialBinding.fabSD.close(); + } + } + }); + speedDialBinding.fabSD.setOnActionSelectedListener(actionItem -> { + new EpisodeMultiSelectActionHandler((MainActivity) getActivity(), actionItem.getId()) + .handleAction(adapter.getSelectedItems()); + adapter.endSelectMode(); + return true; + }); + + return layout; + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + EventBus.getDefault().unregister(this); + } + + private void setupToolbar(MaterialToolbar toolbar) { + toolbar.setTitle(R.string.search_label); + toolbar.setNavigationOnClickListener(v -> getParentFragmentManager().popBackStack()); + toolbar.inflateMenu(R.menu.search); + + MenuItem item = toolbar.getMenu().findItem(R.id.action_search); + item.expandActionView(); + searchView = (SearchView) item.getActionView(); + searchView.setQueryHint(getString(R.string.search_label)); + searchView.setQuery(getArguments().getString(ARG_QUERY), true); + searchView.requestFocus(); + searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { + @Override + public boolean onQueryTextSubmit(String s) { + searchView.clearFocus(); + searchWithProgressBar(); + return true; + } + + @Override + public boolean onQueryTextChange(String s) { + automaticSearchDebouncer.removeCallbacksAndMessages(null); + if (s.isEmpty() || s.endsWith(" ") || (lastQueryChange != 0 + && System.currentTimeMillis() > lastQueryChange + SEARCH_DEBOUNCE_INTERVAL)) { + search(); + } else { + automaticSearchDebouncer.postDelayed(() -> { + search(); + lastQueryChange = 0; // Don't search instantly with first symbol after some pause + }, SEARCH_DEBOUNCE_INTERVAL / 2); + } + lastQueryChange = System.currentTimeMillis(); + return false; + } + }); + item.setOnActionExpandListener(new MenuItem.OnActionExpandListener() { + @Override + public boolean onMenuItemActionExpand(MenuItem item) { + return true; + } + + @Override + public boolean onMenuItemActionCollapse(MenuItem item) { + getParentFragmentManager().popBackStack(); + return true; + } + }); + } + + @Override + public boolean onContextItemSelected(@NonNull MenuItem item) { + Feed selectedFeedItem = adapterFeeds.getLongPressedItem(); + if (selectedFeedItem != null + && FeedMenuHandler.onMenuItemClicked(this, item.getItemId(), selectedFeedItem, () -> { })) { + return true; + } + FeedItem selectedItem = adapter.getLongPressedItem(); + if (selectedItem != null) { + if (adapter.onContextItemSelected(item)) { + return true; + } + if (FeedItemMenuHandler.onMenuItemClicked(this, item.getItemId(), selectedItem)) { + return true; + } + } + return super.onContextItemSelected(item); + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onFeedListChanged(FeedListUpdateEvent event) { + search(); + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onUnreadItemsChanged(UnreadItemsUpdateEvent event) { + search(); + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onEventMainThread(FeedItemEvent event) { + Log.d(TAG, "onEventMainThread() called with: " + "event = [" + event + "]"); + if (results == null) { + return; + } else if (adapter == null) { + search(); + return; + } + for (int i = 0, size = event.items.size(); i < size; i++) { + FeedItem item = event.items.get(i); + int pos = FeedItemUtil.indexOfItemWithId(results, item.getId()); + if (pos >= 0) { + results.remove(pos); + results.add(pos, item); + adapter.notifyItemChangedCompat(pos); + } + } + } + + @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) + public void onEventMainThread(EpisodeDownloadEvent event) { + if (results == null) { + return; + } + for (String downloadUrl : event.getUrls()) { + int pos = FeedItemUtil.indexOfItemWithDownloadUrl(results, downloadUrl); + if (pos >= 0) { + adapter.notifyItemChangedCompat(pos); + } + } + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onEventMainThread(PlaybackPositionEvent event) { + if (adapter != null) { + for (int i = 0; i < adapter.getItemCount(); i++) { + EpisodeItemViewHolder holder = (EpisodeItemViewHolder) recyclerView.findViewHolderForAdapterPosition(i); + if (holder != null && holder.isCurrentlyPlayingItem()) { + holder.notifyPlaybackPositionUpdated(event); + break; + } + } + } + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onPlayerStatusChanged(PlayerStatusEvent event) { + search(); + } + + private void searchWithProgressBar() { + progressBar.setVisibility(View.VISIBLE); + emptyViewHandler.hide(); + search(); + } + + private void search() { + if (disposable != null) { + disposable.dispose(); + } + adapterFeeds.setEndButton(R.string.search_online, this::searchOnline); + chip.setVisibility((getArguments().getLong(ARG_FEED, 0) == 0) ? View.GONE : View.VISIBLE); + disposable = Observable.fromCallable(this::performSearch) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(results -> { + progressBar.setVisibility(View.GONE); + this.results = results.first; + adapter.updateItems(results.first); + if (getArguments().getLong(ARG_FEED, 0) == 0) { + adapterFeeds.updateData(results.second); + } else { + adapterFeeds.updateData(Collections.emptyList()); + } + + if (searchView.getQuery().toString().isEmpty()) { + emptyViewHandler.setMessage(R.string.type_to_search); + } else { + emptyViewHandler.setMessage(getString(R.string.no_results_for_query, searchView.getQuery())); + } + }, error -> Log.e(TAG, Log.getStackTraceString(error))); + } + + @NonNull + private Pair, List> performSearch() { + String query = searchView.getQuery().toString(); + if (query.isEmpty()) { + return new Pair<>(Collections.emptyList(), Collections.emptyList()); + } + long feed = getArguments().getLong(ARG_FEED); + List items = DBReader.searchFeedItems(feed, query); + List feeds = DBReader.searchFeeds(query); + return new Pair<>(items, feeds); + } + + private void showInputMethod(View view) { + InputMethodManager imm = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE); + if (imm != null) { + imm.showSoftInput(view, 0); + } + } + + private void searchOnline() { + searchView.clearFocus(); + InputMethodManager in = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE); + in.hideSoftInputFromWindow(searchView.getWindowToken(), 0); + String query = searchView.getQuery().toString(); + if (query.matches("http[s]?://.*")) { + startActivity(new OnlineFeedviewActivityStarter(getContext(), query).getIntent()); + return; + } + ((MainActivity) getActivity()).loadChildFragment( + OnlineSearchFragment.newInstance(CombinedSearcher.class, query)); + } + + @Override + public void onStartSelectMode() { + searchViewFocusOff(); + speedDialBinding.fabSD.removeActionItemById(R.id.remove_from_inbox_batch); + speedDialBinding.fabSD.removeActionItemById(R.id.remove_from_queue_batch); + speedDialBinding.fabSD.removeActionItemById(R.id.delete_batch); + speedDialBinding.fabSD.setVisibility(View.VISIBLE); + } + + @Override + public void onEndSelectMode() { + speedDialBinding.fabSD.close(); + speedDialBinding.fabSD.setVisibility(View.GONE); + searchViewFocusOn(); + } + + private void searchViewFocusOff() { + isOtherViewInFoucus = true; + searchView.clearFocus(); + } + + private void searchViewFocusOn() { + isOtherViewInFoucus = false; + searchView.requestFocus(); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/screen/chapter/ChaptersFragment.java b/app/src/main/java/de/danoeh/antennapod/ui/screen/chapter/ChaptersFragment.java new file mode 100644 index 000000000..f1dcbf415 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/screen/chapter/ChaptersFragment.java @@ -0,0 +1,191 @@ +package de.danoeh.antennapod.ui.screen.chapter; + +import android.app.Dialog; +import android.content.DialogInterface; +import android.os.Bundle; +import android.text.TextUtils; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.ProgressBar; +import android.widget.Toast; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AppCompatDialogFragment; +import androidx.coordinatorlayout.widget.CoordinatorLayout; +import androidx.recyclerview.widget.DividerItemDecoration; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.util.ChapterUtils; +import de.danoeh.antennapod.event.playback.PlaybackPositionEvent; +import de.danoeh.antennapod.model.feed.Chapter; +import de.danoeh.antennapod.model.feed.FeedMedia; +import de.danoeh.antennapod.model.playback.Playable; +import de.danoeh.antennapod.playback.base.PlayerStatus; +import de.danoeh.antennapod.playback.service.PlaybackController; +import io.reactivex.Maybe; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; +import io.reactivex.schedulers.Schedulers; +import org.greenrobot.eventbus.EventBus; +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; + +public class ChaptersFragment extends AppCompatDialogFragment { + public static final String TAG = "ChaptersFragment"; + private ChaptersListAdapter adapter; + private PlaybackController controller; + private Disposable disposable; + private int focusedChapter = -1; + private Playable media; + private LinearLayoutManager layoutManager; + private ProgressBar progressBar; + + @NonNull + @Override + public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { + + AlertDialog dialog = new MaterialAlertDialogBuilder(requireContext()) + .setTitle(getString(R.string.chapters_label)) + .setView(onCreateView(getLayoutInflater())) + .setPositiveButton(getString(R.string.close_label), null) //dismisses + .setNeutralButton(getString(R.string.refresh_label), null) + .create(); + dialog.show(); + dialog.getButton(DialogInterface.BUTTON_NEUTRAL).setVisibility(View.INVISIBLE); + dialog.getButton(DialogInterface.BUTTON_NEUTRAL).setOnClickListener(v -> { + progressBar.setVisibility(View.VISIBLE); + loadMediaInfo(true); + }); + + return dialog; + } + + + public View onCreateView(@NonNull LayoutInflater inflater) { + View root = inflater.inflate(R.layout.simple_list_fragment, null, false); + root.findViewById(R.id.toolbar).setVisibility(View.GONE); + RecyclerView recyclerView = root.findViewById(R.id.recyclerView); + progressBar = root.findViewById(R.id.progLoading); + layoutManager = new LinearLayoutManager(getActivity()); + recyclerView.setLayoutManager(layoutManager); + recyclerView.addItemDecoration(new DividerItemDecoration(recyclerView.getContext(), + layoutManager.getOrientation())); + + adapter = new ChaptersListAdapter(getActivity(), pos -> { + if (controller.getStatus() != PlayerStatus.PLAYING) { + controller.playPause(); + } + Chapter chapter = adapter.getItem(pos); + controller.seekTo((int) chapter.getStart()); + updateChapterSelection(pos, true); + }); + recyclerView.setAdapter(adapter); + + progressBar.setVisibility(View.VISIBLE); + + CoordinatorLayout.LayoutParams wrapHeight = new CoordinatorLayout.LayoutParams( + CoordinatorLayout.LayoutParams.MATCH_PARENT, CoordinatorLayout.LayoutParams.WRAP_CONTENT); + recyclerView.setLayoutParams(wrapHeight); + + return root; + } + + @Override + public void onStart() { + super.onStart(); + controller = new PlaybackController(getActivity()) { + @Override + public void loadMediaInfo() { + ChaptersFragment.this.loadMediaInfo(false); + } + }; + controller.init(); + EventBus.getDefault().register(this); + loadMediaInfo(false); + } + + @Override + public void onStop() { + super.onStop(); + + if (disposable != null) { + disposable.dispose(); + } + controller.release(); + controller = null; + EventBus.getDefault().unregister(this); + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onEventMainThread(PlaybackPositionEvent event) { + updateChapterSelection(getCurrentChapter(media), false); + adapter.notifyTimeChanged(event.getPosition()); + } + + private int getCurrentChapter(Playable media) { + if (controller == null) { + return -1; + } + return ChapterUtils.getCurrentChapterIndex(media, controller.getPosition()); + } + + private void loadMediaInfo(boolean forceRefresh) { + if (disposable != null) { + disposable.dispose(); + } + disposable = Maybe.create(emitter -> { + Playable media = controller.getMedia(); + if (media != null) { + ChapterUtils.loadChapters(media, getContext(), forceRefresh); + emitter.onSuccess(media); + } else { + emitter.onComplete(); + } + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(media -> onMediaChanged((Playable) media), + error -> Log.e(TAG, Log.getStackTraceString(error))); + } + + private void onMediaChanged(Playable media) { + this.media = media; + focusedChapter = -1; + if (adapter == null) { + return; + } + if (media.getChapters() != null && media.getChapters().size() == 0) { + dismiss(); + Toast.makeText(getContext(), R.string.no_chapters_label, Toast.LENGTH_LONG).show(); + } else { + progressBar.setVisibility(View.GONE); + } + adapter.setMedia(media); + ((AlertDialog) getDialog()).getButton(DialogInterface.BUTTON_NEUTRAL).setVisibility(View.INVISIBLE); + if (media instanceof FeedMedia && ((FeedMedia) media).getItem() != null + && !TextUtils.isEmpty(((FeedMedia) media).getItem().getPodcastIndexChapterUrl())) { + ((AlertDialog) getDialog()).getButton(DialogInterface.BUTTON_NEUTRAL).setVisibility(View.VISIBLE); + } + int positionOfCurrentChapter = getCurrentChapter(media); + updateChapterSelection(positionOfCurrentChapter, true); + } + + private void updateChapterSelection(int position, boolean scrollTo) { + if (adapter == null) { + return; + } + + if (position != -1 && focusedChapter != position) { + focusedChapter = position; + adapter.notifyChapterChanged(focusedChapter); + if (scrollTo && (layoutManager.findFirstCompletelyVisibleItemPosition() >= position + || layoutManager.findLastCompletelyVisibleItemPosition() <= position)) { + layoutManager.scrollToPositionWithOffset(position, 100); + } + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/screen/chapter/ChaptersListAdapter.java b/app/src/main/java/de/danoeh/antennapod/ui/screen/chapter/ChaptersListAdapter.java new file mode 100644 index 000000000..33e55a7e2 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/screen/chapter/ChaptersListAdapter.java @@ -0,0 +1,177 @@ +package de.danoeh.antennapod.ui.screen.chapter; + +import android.content.Context; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.core.content.ContextCompat; +import androidx.recyclerview.widget.RecyclerView; +import com.bumptech.glide.Glide; +import com.bumptech.glide.load.resource.bitmap.FitCenter; +import com.bumptech.glide.load.resource.bitmap.RoundedCorners; +import com.bumptech.glide.request.RequestOptions; +import com.google.android.material.elevation.SurfaceColors; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.model.feed.Chapter; +import de.danoeh.antennapod.ui.common.Converter; +import de.danoeh.antennapod.model.feed.EmbeddedChapterImage; +import de.danoeh.antennapod.core.util.IntentUtils; +import de.danoeh.antennapod.model.playback.Playable; +import de.danoeh.antennapod.ui.common.CircularProgressBar; + +public class ChaptersListAdapter extends RecyclerView.Adapter { + private Playable media; + private final Callback callback; + private final Context context; + private int currentChapterIndex = -1; + private long currentChapterPosition = -1; + private boolean hasImages = false; + + public ChaptersListAdapter(Context context, Callback callback) { + this.callback = callback; + this.context = context; + } + + public void setMedia(Playable media) { + this.media = media; + hasImages = false; + if (media.getChapters() != null) { + for (Chapter chapter : media.getChapters()) { + if (!TextUtils.isEmpty(chapter.getImageUrl())) { + hasImages = true; + } + } + } + notifyDataSetChanged(); + } + + @Override + public void onBindViewHolder(@NonNull ChapterHolder holder, int position) { + Chapter sc = getItem(position); + if (sc == null) { + holder.title.setText("Error"); + return; + } + holder.title.setText(sc.getTitle()); + holder.start.setText(Converter.getDurationStringLong((int) sc + .getStart())); + + long duration; + if (position + 1 < media.getChapters().size()) { + duration = media.getChapters().get(position + 1).getStart() - sc.getStart(); + } else { + duration = media.getDuration() - sc.getStart(); + } + holder.duration.setText(context.getString(R.string.chapter_duration, + Converter.getDurationStringLocalized(context, (int) duration))); + + if (TextUtils.isEmpty(sc.getLink())) { + holder.link.setVisibility(View.GONE); + } else { + holder.link.setVisibility(View.VISIBLE); + holder.link.setText(sc.getLink()); + holder.link.setOnClickListener(v -> IntentUtils.openInBrowser(context, sc.getLink())); + } + holder.secondaryActionIcon.setImageResource(R.drawable.ic_play_48dp); + holder.secondaryActionButton.setContentDescription(context.getString(R.string.play_chapter)); + holder.secondaryActionButton.setOnClickListener(v -> { + if (callback != null) { + callback.onPlayChapterButtonClicked(position); + } + }); + + if (position == currentChapterIndex) { + float density = context.getResources().getDisplayMetrics().density; + holder.itemView.setBackgroundColor(SurfaceColors.getColorForElevation(context, 32 * density)); + float progress = ((float) (currentChapterPosition - sc.getStart())) / duration; + progress = Math.max(progress, CircularProgressBar.MINIMUM_PERCENTAGE); + progress = Math.min(progress, CircularProgressBar.MAXIMUM_PERCENTAGE); + holder.progressBar.setPercentage(progress, position); + holder.secondaryActionIcon.setImageResource(R.drawable.ic_replay); + } else { + holder.itemView.setBackgroundColor(ContextCompat.getColor(context, android.R.color.transparent)); + holder.progressBar.setPercentage(0, null); + } + + if (hasImages) { + holder.image.setVisibility(View.VISIBLE); + if (TextUtils.isEmpty(sc.getImageUrl())) { + Glide.with(context).clear(holder.image); + } else { + Glide.with(context) + .load(EmbeddedChapterImage.getModelFor(media, position)) + .apply(new RequestOptions() + .dontAnimate() + .transform(new FitCenter(), new RoundedCorners((int) + (4 * context.getResources().getDisplayMetrics().density)))) + .into(holder.image); + } + } else { + holder.image.setVisibility(View.GONE); + } + } + + @NonNull + @Override + public ChapterHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + LayoutInflater inflater = LayoutInflater.from(context); + return new ChapterHolder(inflater.inflate(R.layout.simplechapter_item, parent, false)); + } + + @Override + public int getItemCount() { + if (media == null || media.getChapters() == null) { + return 0; + } + return media.getChapters().size(); + } + + static class ChapterHolder extends RecyclerView.ViewHolder { + final TextView title; + final TextView start; + final TextView link; + final TextView duration; + final ImageView image; + final View secondaryActionButton; + final ImageView secondaryActionIcon; + final CircularProgressBar progressBar; + + public ChapterHolder(@NonNull View itemView) { + super(itemView); + title = itemView.findViewById(R.id.txtvTitle); + start = itemView.findViewById(R.id.txtvStart); + link = itemView.findViewById(R.id.txtvLink); + image = itemView.findViewById(R.id.imgvCover); + duration = itemView.findViewById(R.id.txtvDuration); + secondaryActionButton = itemView.findViewById(R.id.secondaryActionButton); + secondaryActionIcon = itemView.findViewById(R.id.secondaryActionIcon); + progressBar = itemView.findViewById(R.id.secondaryActionProgress); + } + } + + public void notifyChapterChanged(int newChapterIndex) { + currentChapterIndex = newChapterIndex; + currentChapterPosition = getItem(newChapterIndex).getStart(); + notifyDataSetChanged(); + } + + public void notifyTimeChanged(long timeMs) { + currentChapterPosition = timeMs; + // Passing an argument prevents flickering. + // See EpisodeItemListAdapter.notifyItemChangedCompat. + notifyItemChanged(currentChapterIndex, "foo"); + } + + public Chapter getItem(int position) { + return media.getChapters().get(position); + } + + public interface Callback { + void onPlayChapterButtonClicked(int position); + } + +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/screen/download/CompletedDownloadsFragment.java b/app/src/main/java/de/danoeh/antennapod/ui/screen/download/CompletedDownloadsFragment.java new file mode 100644 index 000000000..15be1e230 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/screen/download/CompletedDownloadsFragment.java @@ -0,0 +1,393 @@ +package de.danoeh.antennapod.ui.screen.download; + +import android.os.Bundle; +import android.util.Log; +import android.view.ContextMenu; +import android.view.LayoutInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ProgressBar; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import com.google.android.material.appbar.MaterialToolbar; +import com.google.android.material.snackbar.Snackbar; +import com.leinardi.android.speeddial.SpeedDialView; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.ui.episodeslist.EpisodeItemListAdapter; +import de.danoeh.antennapod.actionbutton.DeleteActionButton; +import de.danoeh.antennapod.event.DownloadLogEvent; +import de.danoeh.antennapod.ui.MenuItemUtils; +import de.danoeh.antennapod.ui.screen.SearchFragment; +import de.danoeh.antennapod.net.download.serviceinterface.FeedUpdateManager; +import de.danoeh.antennapod.storage.database.DBReader; +import de.danoeh.antennapod.core.util.FeedItemUtil; +import de.danoeh.antennapod.ui.screen.feed.ItemSortDialog; +import de.danoeh.antennapod.event.EpisodeDownloadEvent; +import de.danoeh.antennapod.event.FeedItemEvent; +import de.danoeh.antennapod.event.PlayerStatusEvent; +import de.danoeh.antennapod.event.UnreadItemsUpdateEvent; +import de.danoeh.antennapod.event.playback.PlaybackPositionEvent; +import de.danoeh.antennapod.ui.episodeslist.EpisodeMultiSelectActionHandler; +import de.danoeh.antennapod.ui.swipeactions.SwipeActions; +import de.danoeh.antennapod.ui.episodeslist.FeedItemMenuHandler; +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedItemFilter; +import de.danoeh.antennapod.model.feed.SortOrder; +import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterface; +import de.danoeh.antennapod.storage.preferences.UserPreferences; +import de.danoeh.antennapod.ui.view.EmptyViewHandler; +import de.danoeh.antennapod.ui.episodeslist.EpisodeItemListRecyclerView; +import de.danoeh.antennapod.ui.view.LiftOnScrollListener; +import de.danoeh.antennapod.ui.episodeslist.EpisodeItemViewHolder; +import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; +import io.reactivex.schedulers.Schedulers; +import org.greenrobot.eventbus.EventBus; +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Displays all completed downloads and provides a button to delete them. + */ +public class CompletedDownloadsFragment extends Fragment + implements EpisodeItemListAdapter.OnSelectModeListener, MaterialToolbar.OnMenuItemClickListener { + public static final String TAG = "DownloadsFragment"; + public static final String ARG_SHOW_LOGS = "show_logs"; + private static final String KEY_UP_ARROW = "up_arrow"; + + private Set runningDownloads = new HashSet<>(); + private List items = new ArrayList<>(); + private CompletedDownloadsListAdapter adapter; + private EpisodeItemListRecyclerView recyclerView; + private Disposable disposable; + private EmptyViewHandler emptyView; + private boolean displayUpArrow; + private SpeedDialView speedDialView; + private SwipeActions swipeActions; + private ProgressBar progressBar; + private MaterialToolbar toolbar; + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + View root = inflater.inflate(R.layout.simple_list_fragment, container, false); + toolbar = root.findViewById(R.id.toolbar); + toolbar.setTitle(R.string.downloads_label); + toolbar.inflateMenu(R.menu.downloads_completed); + toolbar.setOnMenuItemClickListener(this); + toolbar.setOnLongClickListener(v -> { + recyclerView.scrollToPosition(5); + recyclerView.post(() -> recyclerView.smoothScrollToPosition(0)); + return false; + }); + displayUpArrow = getParentFragmentManager().getBackStackEntryCount() != 0; + if (savedInstanceState != null) { + displayUpArrow = savedInstanceState.getBoolean(KEY_UP_ARROW); + } + ((MainActivity) getActivity()).setupToolbarToggle(toolbar, displayUpArrow); + + recyclerView = root.findViewById(R.id.recyclerView); + recyclerView.setRecycledViewPool(((MainActivity) getActivity()).getRecycledViewPool()); + adapter = new CompletedDownloadsListAdapter((MainActivity) getActivity()); + adapter.setOnSelectModeListener(this); + recyclerView.setAdapter(adapter); + recyclerView.addOnScrollListener(new LiftOnScrollListener(root.findViewById(R.id.appbar))); + swipeActions = new SwipeActions(this, TAG).attachTo(recyclerView); + swipeActions.setFilter(new FeedItemFilter(FeedItemFilter.DOWNLOADED)); + + progressBar = root.findViewById(R.id.progLoading); + progressBar.setVisibility(View.VISIBLE); + + speedDialView = root.findViewById(R.id.fabSD); + speedDialView.setOverlayLayout(root.findViewById(R.id.fabSDOverlay)); + speedDialView.inflate(R.menu.episodes_apply_action_speeddial); + speedDialView.removeActionItemById(R.id.download_batch); + speedDialView.removeActionItemById(R.id.mark_read_batch); + speedDialView.removeActionItemById(R.id.mark_unread_batch); + speedDialView.removeActionItemById(R.id.remove_from_queue_batch); + speedDialView.removeActionItemById(R.id.remove_all_inbox_item); + speedDialView.setOnChangeListener(new SpeedDialView.OnChangeListener() { + @Override + public boolean onMainActionSelected() { + return false; + } + + @Override + public void onToggleChanged(boolean open) { + if (open && adapter.getSelectedCount() == 0) { + ((MainActivity) getActivity()).showSnackbarAbovePlayer(R.string.no_items_selected, + Snackbar.LENGTH_SHORT); + speedDialView.close(); + } + } + }); + speedDialView.setOnActionSelectedListener(actionItem -> { + new EpisodeMultiSelectActionHandler(((MainActivity) getActivity()), actionItem.getId()) + .handleAction(adapter.getSelectedItems()); + adapter.endSelectMode(); + return true; + }); + if (getArguments() != null && getArguments().getBoolean(ARG_SHOW_LOGS, false)) { + new DownloadLogFragment().show(getChildFragmentManager(), null); + } + + addEmptyView(); + EventBus.getDefault().register(this); + return root; + } + + @Override + public void onSaveInstanceState(@NonNull Bundle outState) { + outState.putBoolean(KEY_UP_ARROW, displayUpArrow); + super.onSaveInstanceState(outState); + } + + @Override + public void onDestroyView() { + EventBus.getDefault().unregister(this); + adapter.endSelectMode(); + if (toolbar != null) { + toolbar.setOnMenuItemClickListener(null); + toolbar.setOnLongClickListener(null); + } + super.onDestroyView(); + } + + @Override + public void onStart() { + super.onStart(); + loadItems(); + } + + @Override + public void onStop() { + super.onStop(); + if (disposable != null) { + disposable.dispose(); + } + } + + @Override + public boolean onMenuItemClick(MenuItem item) { + if (item.getItemId() == R.id.refresh_item) { + FeedUpdateManager.getInstance().runOnceOrAsk(requireContext()); + return true; + } else if (item.getItemId() == R.id.action_download_logs) { + new DownloadLogFragment().show(getChildFragmentManager(), null); + return true; + } else if (item.getItemId() == R.id.action_search) { + ((MainActivity) getActivity()).loadChildFragment(SearchFragment.newInstance()); + return true; + } else if (item.getItemId() == R.id.downloads_sort) { + new DownloadsSortDialog().show(getChildFragmentManager(), "SortDialog"); + return true; + } + return false; + } + + @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) + public void onEventMainThread(EpisodeDownloadEvent event) { + Set newRunningDownloads = new HashSet<>(); + for (String url : event.getUrls()) { + if (DownloadServiceInterface.get().isDownloadingEpisode(url)) { + newRunningDownloads.add(url); + } + } + if (!newRunningDownloads.equals(runningDownloads)) { + runningDownloads = newRunningDownloads; + loadItems(); + return; // Refreshed anyway + } + for (String downloadUrl : event.getUrls()) { + int pos = FeedItemUtil.indexOfItemWithDownloadUrl(items, downloadUrl); + if (pos >= 0) { + adapter.notifyItemChangedCompat(pos); + } + } + } + + @Override + public boolean onContextItemSelected(@NonNull MenuItem item) { + FeedItem selectedItem = adapter.getLongPressedItem(); + if (selectedItem == null) { + Log.i(TAG, "Selected item at current position was null, ignoring selection"); + return super.onContextItemSelected(item); + } + if (adapter.onContextItemSelected(item)) { + return true; + } + + return FeedItemMenuHandler.onMenuItemClicked(this, item.getItemId(), selectedItem); + } + + private void addEmptyView() { + emptyView = new EmptyViewHandler(getActivity()); + emptyView.setIcon(R.drawable.ic_download); + emptyView.setTitle(R.string.no_comp_downloads_head_label); + emptyView.setMessage(R.string.no_comp_downloads_label); + emptyView.attachToRecyclerView(recyclerView); + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onEventMainThread(FeedItemEvent event) { + Log.d(TAG, "onEventMainThread() called with: " + "event = [" + event + "]"); + if (items == null) { + return; + } else if (adapter == null) { + loadItems(); + return; + } + for (int i = 0, size = event.items.size(); i < size; i++) { + FeedItem item = event.items.get(i); + int pos = FeedItemUtil.indexOfItemWithId(items, item.getId()); + if (pos >= 0) { + items.remove(pos); + if (item.getMedia().isDownloaded()) { + items.add(pos, item); + adapter.notifyItemChangedCompat(pos); + } else { + adapter.notifyItemRemoved(pos); + } + } + } + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onEventMainThread(PlaybackPositionEvent event) { + if (adapter != null) { + for (int i = 0; i < adapter.getItemCount(); i++) { + EpisodeItemViewHolder holder = (EpisodeItemViewHolder) recyclerView.findViewHolderForAdapterPosition(i); + if (holder != null && holder.isCurrentlyPlayingItem()) { + holder.notifyPlaybackPositionUpdated(event); + break; + } + } + } + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onPlayerStatusChanged(PlayerStatusEvent event) { + loadItems(); + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onDownloadLogChanged(DownloadLogEvent event) { + loadItems(); + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onUnreadItemsChanged(UnreadItemsUpdateEvent event) { + loadItems(); + } + + private void loadItems() { + if (disposable != null) { + disposable.dispose(); + } + emptyView.hide(); + disposable = Observable.fromCallable(() -> { + SortOrder sortOrder = UserPreferences.getDownloadsSortedOrder(); + List downloadedItems = DBReader.getEpisodes(0, Integer.MAX_VALUE, + new FeedItemFilter(FeedItemFilter.DOWNLOADED), sortOrder); + + List mediaUrls = new ArrayList<>(); + if (runningDownloads == null) { + return downloadedItems; + } + for (String url : runningDownloads) { + if (FeedItemUtil.indexOfItemWithDownloadUrl(downloadedItems, url) != -1) { + continue; // Already in list + } + mediaUrls.add(url); + } + List currentDownloads = DBReader.getFeedItemsWithUrl(mediaUrls); + currentDownloads.addAll(downloadedItems); + return currentDownloads; + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + result -> { + items = result; + adapter.setDummyViews(0); + progressBar.setVisibility(View.GONE); + adapter.updateItems(result); + }, error -> { + adapter.setDummyViews(0); + adapter.updateItems(Collections.emptyList()); + Log.e(TAG, Log.getStackTraceString(error)); + }); + } + + @Override + public void onStartSelectMode() { + swipeActions.detach(); + speedDialView.setVisibility(View.VISIBLE); + } + + @Override + public void onEndSelectMode() { + speedDialView.close(); + speedDialView.setVisibility(View.GONE); + swipeActions.attachTo(recyclerView); + } + + private class CompletedDownloadsListAdapter extends EpisodeItemListAdapter { + + public CompletedDownloadsListAdapter(MainActivity mainActivity) { + super(mainActivity); + } + + @Override + public void afterBindViewHolder(EpisodeItemViewHolder holder, int pos) { + if (!inActionMode()) { + if (holder.getFeedItem().isDownloaded()) { + DeleteActionButton actionButton = new DeleteActionButton(getItem(pos)); + actionButton.configure(holder.secondaryActionButton, holder.secondaryActionIcon, getActivity()); + } + } + } + + @Override + public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { + super.onCreateContextMenu(menu, v, menuInfo); + if (!inActionMode()) { + menu.findItem(R.id.multi_select).setVisible(true); + } + MenuItemUtils.setOnClickListeners(menu, CompletedDownloadsFragment.this::onContextItemSelected); + } + } + + public static class DownloadsSortDialog extends ItemSortDialog { + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + sortOrder = UserPreferences.getDownloadsSortedOrder(); + } + + @Override + protected void onAddItem(int title, SortOrder ascending, SortOrder descending, boolean ascendingIsDefault) { + if (ascending == SortOrder.DATE_OLD_NEW || ascending == SortOrder.DURATION_SHORT_LONG + || ascending == SortOrder.EPISODE_TITLE_A_Z || ascending == SortOrder.SIZE_SMALL_LARGE) { + super.onAddItem(title, ascending, descending, ascendingIsDefault); + } + } + + @Override + protected void onSelectionChanged() { + super.onSelectionChanged(); + UserPreferences.setDownloadsSortedOrder(sortOrder); + EventBus.getDefault().post(DownloadLogEvent.listUpdated()); + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/screen/download/DownloadErrorLabel.java b/app/src/main/java/de/danoeh/antennapod/ui/screen/download/DownloadErrorLabel.java new file mode 100644 index 000000000..e5eedd54b --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/screen/download/DownloadErrorLabel.java @@ -0,0 +1,46 @@ +package de.danoeh.antennapod.ui.screen.download; + +import androidx.annotation.StringRes; +import de.danoeh.antennapod.core.BuildConfig; +import de.danoeh.antennapod.core.R; +import de.danoeh.antennapod.model.download.DownloadError; + +/** + * Provides user-visible labels for download errors. + */ +public class DownloadErrorLabel { + + @StringRes + public static int from(DownloadError error) { + switch (error) { + case SUCCESS: return R.string.download_successful; + case ERROR_PARSER_EXCEPTION: return R.string.download_error_parser_exception; + case ERROR_UNSUPPORTED_TYPE: return R.string.download_error_unsupported_type; + case ERROR_CONNECTION_ERROR: return R.string.download_error_connection_error; + case ERROR_MALFORMED_URL: return R.string.download_error_error_unknown; + case ERROR_IO_ERROR: return R.string.download_error_io_error; + case ERROR_FILE_EXISTS: return R.string.download_error_error_unknown; + case ERROR_DOWNLOAD_CANCELLED: return R.string.download_canceled_msg; + case ERROR_DEVICE_NOT_FOUND: return R.string.download_error_device_not_found; + case ERROR_HTTP_DATA_ERROR: return R.string.download_error_http_data_error; + case ERROR_NOT_ENOUGH_SPACE: return R.string.download_error_insufficient_space; + case ERROR_UNKNOWN_HOST: return R.string.download_error_unknown_host; + case ERROR_REQUEST_ERROR: return R.string.download_error_request_error; + case ERROR_DB_ACCESS_ERROR: return R.string.download_error_db_access; + case ERROR_UNAUTHORIZED: return R.string.download_error_unauthorized; + case ERROR_FILE_TYPE: return R.string.download_error_file_type_type; + case ERROR_FORBIDDEN: return R.string.download_error_forbidden; + case ERROR_IO_WRONG_SIZE: return R.string.download_error_wrong_size; + case ERROR_IO_BLOCKED: return R.string.download_error_blocked; + case ERROR_UNSUPPORTED_TYPE_HTML: return R.string.download_error_unsupported_type_html; + case ERROR_NOT_FOUND: return R.string.download_error_not_found; + case ERROR_CERTIFICATE: return R.string.download_error_certificate; + case ERROR_PARSER_EXCEPTION_DUPLICATE: return R.string.download_error_parser_exception; + default: + if (BuildConfig.DEBUG) { + throw new IllegalArgumentException("No mapping from download error to label"); + } + return R.string.download_error_error_unknown; + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/screen/download/DownloadLogAdapter.java b/app/src/main/java/de/danoeh/antennapod/ui/screen/download/DownloadLogAdapter.java new file mode 100644 index 000000000..88472bcbf --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/screen/download/DownloadLogAdapter.java @@ -0,0 +1,153 @@ +package de.danoeh.antennapod.ui.screen.download; + +import android.app.Activity; +import android.text.format.DateUtils; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.Toast; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.actionbutton.DownloadActionButton; +import de.danoeh.antennapod.net.download.serviceinterface.FeedUpdateManager; +import de.danoeh.antennapod.storage.database.DBReader; +import de.danoeh.antennapod.model.download.DownloadError; +import de.danoeh.antennapod.model.download.DownloadResult; +import de.danoeh.antennapod.model.feed.Feed; +import de.danoeh.antennapod.model.feed.FeedMedia; + +import java.util.ArrayList; +import java.util.List; + +/** + * Displays a list of DownloadStatus entries. + */ +public class DownloadLogAdapter extends BaseAdapter { + private static final String TAG = "DownloadLogAdapter"; + + private final Activity context; + private List downloadLog = new ArrayList<>(); + + public DownloadLogAdapter(Activity context) { + super(); + this.context = context; + } + + public void setDownloadLog(List downloadLog) { + this.downloadLog = downloadLog; + notifyDataSetChanged(); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + DownloadLogItemViewHolder holder; + if (convertView == null) { + holder = new DownloadLogItemViewHolder(context, parent); + holder.itemView.setTag(holder); + } else { + holder = (DownloadLogItemViewHolder) convertView.getTag(); + } + bind(holder, getItem(position), position); + return holder.itemView; + } + + private void bind(DownloadLogItemViewHolder holder, DownloadResult status, int position) { + String statusText = ""; + if (status.getFeedfileType() == Feed.FEEDFILETYPE_FEED) { + statusText += context.getString(R.string.download_type_feed); + } else if (status.getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA) { + statusText += context.getString(R.string.download_type_media); + } + statusText += " · "; + statusText += DateUtils.getRelativeTimeSpanString(status.getCompletionDate().getTime(), + System.currentTimeMillis(), DateUtils.MINUTE_IN_MILLIS, 0); + holder.status.setText(statusText); + + if (status.getTitle() != null) { + holder.title.setText(status.getTitle()); + } else { + holder.title.setText(R.string.download_log_title_unknown); + } + + if (status.isSuccessful()) { + holder.icon.setImageResource(R.drawable.ic_check); + holder.icon.setContentDescription(context.getString(R.string.download_successful)); + holder.secondaryActionButton.setVisibility(View.INVISIBLE); + holder.reason.setVisibility(View.GONE); + holder.tapForDetails.setVisibility(View.GONE); + } else { + if (status.getReason() == DownloadError.ERROR_PARSER_EXCEPTION_DUPLICATE) { + holder.icon.setImageResource(R.drawable.ic_info); + } else { + holder.icon.setImageResource(R.drawable.ic_error); + } + holder.icon.setContentDescription(context.getString(R.string.error_label)); + holder.reason.setText(DownloadErrorLabel.from(status.getReason())); + holder.reason.setVisibility(View.VISIBLE); + holder.tapForDetails.setVisibility(View.VISIBLE); + + if (newerWasSuccessful(position, status.getFeedfileType(), status.getFeedfileId())) { + holder.secondaryActionButton.setVisibility(View.INVISIBLE); + holder.secondaryActionButton.setOnClickListener(null); + holder.secondaryActionButton.setTag(null); + } else { + holder.secondaryActionIcon.setImageResource(R.drawable.ic_refresh); + holder.secondaryActionButton.setVisibility(View.VISIBLE); + + if (status.getFeedfileType() == Feed.FEEDFILETYPE_FEED) { + holder.secondaryActionButton.setOnClickListener(v -> { + holder.secondaryActionButton.setVisibility(View.INVISIBLE); + Feed feed = DBReader.getFeed(status.getFeedfileId()); + if (feed == null) { + Log.e(TAG, "Could not find feed for feed id: " + status.getFeedfileId()); + return; + } + FeedUpdateManager.getInstance().runOnce(context, feed); + }); + } else if (status.getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA) { + holder.secondaryActionButton.setOnClickListener(v -> { + holder.secondaryActionButton.setVisibility(View.INVISIBLE); + FeedMedia media = DBReader.getFeedMedia(status.getFeedfileId()); + if (media == null) { + Log.e(TAG, "Could not find feed media for feed id: " + status.getFeedfileId()); + return; + } + new DownloadActionButton(media.getItem()).onClick(context); + ((MainActivity) context).showSnackbarAbovePlayer( + R.string.status_downloading_label, Toast.LENGTH_SHORT); + }); + } + } + } + } + + private boolean newerWasSuccessful(int downloadStatusIndex, int feedTypeId, long id) { + for (int i = 0; i < downloadStatusIndex; i++) { + DownloadResult status = downloadLog.get(i); + if (status.getFeedfileType() == feedTypeId && status.getFeedfileId() == id && status.isSuccessful()) { + return true; + } + } + return false; + } + + @Override + public int getCount() { + return downloadLog.size(); + } + + @Override + public DownloadResult getItem(int position) { + if (position < downloadLog.size()) { + return downloadLog.get(position); + } + return null; + } + + @Override + public long getItemId(int position) { + return position; + } + +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/screen/download/DownloadLogDetailsDialog.java b/app/src/main/java/de/danoeh/antennapod/ui/screen/download/DownloadLogDetailsDialog.java new file mode 100644 index 000000000..a035f58ff --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/screen/download/DownloadLogDetailsDialog.java @@ -0,0 +1,64 @@ +package de.danoeh.antennapod.ui.screen.download; + +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; +import android.os.Build; +import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.model.download.DownloadResult; +import de.danoeh.antennapod.storage.database.DBReader; +import de.danoeh.antennapod.event.MessageEvent; +import de.danoeh.antennapod.model.feed.Feed; +import de.danoeh.antennapod.model.feed.FeedMedia; +import org.greenrobot.eventbus.EventBus; + +public class DownloadLogDetailsDialog extends MaterialAlertDialogBuilder { + + public DownloadLogDetailsDialog(@NonNull Context context, DownloadResult status) { + super(context); + + String url = "unknown"; + if (status.getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA) { + FeedMedia media = DBReader.getFeedMedia(status.getFeedfileId()); + if (media != null) { + url = media.getDownloadUrl(); + } + } else if (status.getFeedfileType() == Feed.FEEDFILETYPE_FEED) { + Feed feed = DBReader.getFeed(status.getFeedfileId()); + if (feed != null) { + url = feed.getDownloadUrl(); + } + } + + String message = context.getString(R.string.download_successful); + if (!status.isSuccessful()) { + message = status.getReasonDetailed(); + } + + String messageFull = context.getString(R.string.download_log_details_message, + context.getString(DownloadErrorLabel.from(status.getReason())), message, url); + setTitle(R.string.download_error_details); + setMessage(messageFull); + setPositiveButton(android.R.string.ok, null); + setNeutralButton(R.string.copy_to_clipboard, (dialog, which) -> { + ClipboardManager clipboard = (ClipboardManager) getContext() + .getSystemService(Context.CLIPBOARD_SERVICE); + ClipData clip = ClipData.newPlainText(context.getString(R.string.download_error_details), messageFull); + clipboard.setPrimaryClip(clip); + if (Build.VERSION.SDK_INT < 32) { + EventBus.getDefault().post(new MessageEvent(context.getString(R.string.copied_to_clipboard))); + } + }); + } + + @Override + public AlertDialog show() { + AlertDialog dialog = super.show(); + ((TextView) dialog.findViewById(android.R.id.message)).setTextIsSelectable(true); + return dialog; + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/screen/download/DownloadLogFragment.java b/app/src/main/java/de/danoeh/antennapod/ui/screen/download/DownloadLogFragment.java new file mode 100644 index 000000000..b6c8875e1 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/screen/download/DownloadLogFragment.java @@ -0,0 +1,121 @@ +package de.danoeh.antennapod.ui.screen.download; + +import android.os.Bundle; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.android.material.appbar.MaterialToolbar; +import com.google.android.material.bottomsheet.BottomSheetDialogFragment; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.event.DownloadLogEvent; +import de.danoeh.antennapod.storage.database.DBReader; +import de.danoeh.antennapod.storage.database.DBWriter; +import de.danoeh.antennapod.databinding.DownloadLogFragmentBinding; +import de.danoeh.antennapod.model.download.DownloadResult; +import de.danoeh.antennapod.ui.view.EmptyViewHandler; +import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; +import io.reactivex.schedulers.Schedulers; +import org.greenrobot.eventbus.EventBus; +import org.greenrobot.eventbus.Subscribe; + +import java.util.ArrayList; +import java.util.List; + +/** + * Shows the download log + */ +public class DownloadLogFragment extends BottomSheetDialogFragment + implements AdapterView.OnItemClickListener, MaterialToolbar.OnMenuItemClickListener { + private static final String TAG = "DownloadLogFragment"; + + private List downloadLog = new ArrayList<>(); + private DownloadLogAdapter adapter; + private Disposable disposable; + private DownloadLogFragmentBinding viewBinding; + + @Override + public void onStart() { + super.onStart(); + loadDownloadLog(); + } + + @Override + public void onStop() { + super.onStop(); + if (disposable != null) { + disposable.dispose(); + } + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, + @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + viewBinding = DownloadLogFragmentBinding.inflate(inflater); + viewBinding.toolbar.inflateMenu(R.menu.download_log); + viewBinding.toolbar.setOnMenuItemClickListener(this); + + EmptyViewHandler emptyView = new EmptyViewHandler(getActivity()); + emptyView.setIcon(R.drawable.ic_download); + emptyView.setTitle(R.string.no_log_downloads_head_label); + emptyView.setMessage(R.string.no_log_downloads_label); + emptyView.attachToListView(viewBinding.list); + + adapter = new DownloadLogAdapter(getActivity()); + viewBinding.list.setAdapter(adapter); + viewBinding.list.setOnItemClickListener(this); + viewBinding.list.setNestedScrollingEnabled(true); + EventBus.getDefault().register(this); + return viewBinding.getRoot(); + } + + @Override + public void onDestroyView() { + EventBus.getDefault().unregister(this); + super.onDestroyView(); + } + + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + final DownloadResult item = adapter.getItem(position); + if (item != null) { + new DownloadLogDetailsDialog(getContext(), item).show(); + } + } + + @Subscribe + public void onDownloadLogChanged(DownloadLogEvent event) { + loadDownloadLog(); + } + + @Override + public boolean onMenuItemClick(MenuItem item) { + if (item.getItemId() == R.id.clear_logs_item) { + DBWriter.clearDownloadLog(); + return true; + } + return false; + } + + private void loadDownloadLog() { + if (disposable != null) { + disposable.dispose(); + } + disposable = Observable.fromCallable(DBReader::getDownloadLog) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(result -> { + if (result != null) { + downloadLog = result; + adapter.setDownloadLog(downloadLog); + } + }, error -> Log.e(TAG, Log.getStackTraceString(error))); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/screen/download/DownloadLogItemViewHolder.java b/app/src/main/java/de/danoeh/antennapod/ui/screen/download/DownloadLogItemViewHolder.java new file mode 100644 index 000000000..97d368fac --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/screen/download/DownloadLogItemViewHolder.java @@ -0,0 +1,40 @@ +package de.danoeh.antennapod.ui.screen.download; + +import android.content.Context; +import android.os.Build; +import android.text.Layout; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; +import androidx.recyclerview.widget.RecyclerView; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.ui.common.CircularProgressBar; + +public class DownloadLogItemViewHolder extends RecyclerView.ViewHolder { + public final View secondaryActionButton; + public final ImageView secondaryActionIcon; + public final CircularProgressBar secondaryActionProgress; + public final ImageView icon; + public final TextView title; + public final TextView status; + public final TextView reason; + public final TextView tapForDetails; + + public DownloadLogItemViewHolder(Context context, ViewGroup parent) { + super(LayoutInflater.from(context).inflate(R.layout.downloadlog_item, parent, false)); + status = itemView.findViewById(R.id.status); + icon = itemView.findViewById(R.id.icon); + reason = itemView.findViewById(R.id.txtvReason); + tapForDetails = itemView.findViewById(R.id.txtvTapForDetails); + secondaryActionButton = itemView.findViewById(R.id.secondaryActionButton); + secondaryActionProgress = itemView.findViewById(R.id.secondaryActionProgress); + secondaryActionIcon = itemView.findViewById(R.id.secondaryActionIcon); + title = itemView.findViewById(R.id.txtvTitle); + if (Build.VERSION.SDK_INT >= 23) { + title.setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_FULL); + } + itemView.setTag(this); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/screen/drawer/DrawerPreferencesDialog.java b/app/src/main/java/de/danoeh/antennapod/ui/screen/drawer/DrawerPreferencesDialog.java new file mode 100644 index 000000000..4923df19c --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/screen/drawer/DrawerPreferencesDialog.java @@ -0,0 +1,50 @@ +package de.danoeh.antennapod.ui.screen.drawer; + +import android.content.Context; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.storage.preferences.UserPreferences; +import de.danoeh.antennapod.ui.screen.drawer.NavDrawerFragment; + +import java.util.List; + +public class DrawerPreferencesDialog { + public static void show(Context context, Runnable callback) { + final List hiddenDrawerItems = UserPreferences.getHiddenDrawerItems(); + final String[] navTitles = context.getResources().getStringArray(R.array.nav_drawer_titles); + boolean[] checked = new boolean[NavDrawerFragment.NAV_DRAWER_TAGS.length]; + for (int i = 0; i < NavDrawerFragment.NAV_DRAWER_TAGS.length; i++) { + String tag = NavDrawerFragment.NAV_DRAWER_TAGS[i]; + if (!hiddenDrawerItems.contains(tag)) { + checked[i] = true; + } + } + MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(context); + builder.setTitle(R.string.drawer_preferences); + builder.setMultiChoiceItems(navTitles, checked, (dialog, which, isChecked) -> { + if (isChecked) { + hiddenDrawerItems.remove(NavDrawerFragment.NAV_DRAWER_TAGS[which]); + } else { + hiddenDrawerItems.add(NavDrawerFragment.NAV_DRAWER_TAGS[which]); + } + }); + builder.setPositiveButton(R.string.confirm_label, (dialog, which) -> { + UserPreferences.setHiddenDrawerItems(hiddenDrawerItems); + + if (hiddenDrawerItems.contains(UserPreferences.getDefaultPage())) { + for (String tag : NavDrawerFragment.NAV_DRAWER_TAGS) { + if (!hiddenDrawerItems.contains(tag)) { + UserPreferences.setDefaultPage(tag); + break; + } + } + } + + if (callback != null) { + callback.run(); + } + }); + builder.setNegativeButton(R.string.cancel_label, null); + builder.create().show(); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/screen/drawer/NavDrawerFragment.java b/app/src/main/java/de/danoeh/antennapod/ui/screen/drawer/NavDrawerFragment.java new file mode 100644 index 000000000..fabcaf652 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/screen/drawer/NavDrawerFragment.java @@ -0,0 +1,484 @@ +package de.danoeh.antennapod.ui.screen.drawer; + +import android.app.Activity; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.res.ColorStateList; +import android.graphics.Color; +import android.os.Build; +import android.os.Bundle; +import android.util.Log; +import android.view.ContextMenu; +import android.view.LayoutInflater; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ProgressBar; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import androidx.core.graphics.Insets; +import androidx.core.util.Pair; +import androidx.core.view.ViewCompat; +import androidx.core.view.WindowInsetsCompat; +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.google.android.material.bottomsheet.BottomSheetBehavior; +import com.google.android.material.shape.MaterialShapeDrawable; +import com.google.android.material.shape.ShapeAppearanceModel; + +import de.danoeh.antennapod.core.storage.EpisodeCleanupAlgorithmFactory; +import de.danoeh.antennapod.ui.screen.AddFeedFragment; +import de.danoeh.antennapod.ui.screen.AllEpisodesFragment; +import de.danoeh.antennapod.ui.screen.InboxFragment; +import de.danoeh.antennapod.ui.screen.PlaybackHistoryFragment; +import de.danoeh.antennapod.ui.screen.queue.QueueFragment; +import de.danoeh.antennapod.ui.screen.download.CompletedDownloadsFragment; +import de.danoeh.antennapod.ui.screen.subscriptions.SubscriptionFragment; +import org.apache.commons.lang3.StringUtils; +import org.greenrobot.eventbus.EventBus; +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.ui.screen.preferences.PreferenceActivity; +import de.danoeh.antennapod.core.util.ConfirmationDialog; +import de.danoeh.antennapod.ui.MenuItemUtils; +import de.danoeh.antennapod.storage.database.DBReader; +import de.danoeh.antennapod.storage.database.DBWriter; +import de.danoeh.antennapod.storage.database.NavDrawerData; +import de.danoeh.antennapod.ui.screen.feed.RemoveFeedDialog; +import de.danoeh.antennapod.ui.screen.feed.RenameFeedDialog; +import de.danoeh.antennapod.ui.screen.subscriptions.SubscriptionsFilterDialog; +import de.danoeh.antennapod.ui.screen.feed.preferences.TagSettingsDialog; +import de.danoeh.antennapod.event.FeedListUpdateEvent; +import de.danoeh.antennapod.event.QueueEvent; +import de.danoeh.antennapod.event.UnreadItemsUpdateEvent; +import de.danoeh.antennapod.model.feed.Feed; +import de.danoeh.antennapod.storage.preferences.UserPreferences; +import de.danoeh.antennapod.ui.appstartintent.MainActivityStarter; +import de.danoeh.antennapod.ui.common.ThemeUtils; +import de.danoeh.antennapod.ui.screen.home.HomeFragment; +import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; +import io.reactivex.schedulers.Schedulers; + +public class NavDrawerFragment extends Fragment implements SharedPreferences.OnSharedPreferenceChangeListener { + @VisibleForTesting + public static final String PREF_LAST_FRAGMENT_TAG = "prefLastFragmentTag"; + private static final String PREF_OPEN_FOLDERS = "prefOpenFolders"; + @VisibleForTesting + public static final String PREF_NAME = "NavDrawerPrefs"; + public static final String TAG = "NavDrawerFragment"; + + public static final String[] NAV_DRAWER_TAGS = { + HomeFragment.TAG, + QueueFragment.TAG, + InboxFragment.TAG, + AllEpisodesFragment.TAG, + SubscriptionFragment.TAG, + CompletedDownloadsFragment.TAG, + PlaybackHistoryFragment.TAG, + AddFeedFragment.TAG, + NavListAdapter.SUBSCRIPTION_LIST_TAG + }; + + private NavDrawerData navDrawerData; + private int reclaimableSpace = 0; + private List flatItemList; + private NavDrawerData.DrawerItem contextPressedItem = null; + private NavListAdapter navAdapter; + private Disposable disposable; + private ProgressBar progressBar; + private Set openFolders = new HashSet<>(); + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + super.onCreateView(inflater, container, savedInstanceState); + View root = inflater.inflate(R.layout.nav_list, container, false); + setupDrawerRoundBackground(root); + ViewCompat.setOnApplyWindowInsetsListener(root, (view, insets) -> { + Insets bars = insets.getInsets(WindowInsetsCompat.Type.systemBars()); + view.setPadding(bars.left, bars.top, bars.right, 0); + float navigationBarHeight = 0; + Activity activity = getActivity(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && activity != null) { + navigationBarHeight = getActivity().getWindow().getNavigationBarDividerColor() == Color.TRANSPARENT + ? 0 : 1 * getResources().getDisplayMetrics().density; // Assuming the divider is 1dp in height + } + float bottomInset = Math.max(0f, Math.round(bars.bottom - navigationBarHeight)); + ((ViewGroup.MarginLayoutParams) view.getLayoutParams()).bottomMargin = (int) bottomInset; + return insets; + }); + + SharedPreferences preferences = getContext().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); + openFolders = new HashSet<>(preferences.getStringSet(PREF_OPEN_FOLDERS, new HashSet<>())); // Must not modify + + progressBar = root.findViewById(R.id.progressBar); + RecyclerView navList = root.findViewById(R.id.nav_list); + navAdapter = new NavListAdapter(itemAccess, getActivity()); + navAdapter.setHasStableIds(true); + navList.setAdapter(navAdapter); + navList.setLayoutManager(new LinearLayoutManager(getContext())); + + root.findViewById(R.id.nav_settings).setOnClickListener(v -> + startActivity(new Intent(getActivity(), PreferenceActivity.class))); + + preferences.registerOnSharedPreferenceChangeListener(this); + return root; + } + + private void setupDrawerRoundBackground(View root) { + // Akin to this logic: + // https://github.com/material-components/material-components-android/blob/8938da8c/lib/java/com/google/android/material/navigation/NavigationView.java#L405 + ShapeAppearanceModel.Builder shapeBuilder = ShapeAppearanceModel.builder(); + float cornerSize = getResources().getDimension(R.dimen.drawer_corner_size); + boolean isRtl = getResources().getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; + if (isRtl) { + shapeBuilder.setTopLeftCornerSize(cornerSize).setBottomLeftCornerSize(cornerSize); + } else { + shapeBuilder.setTopRightCornerSize(cornerSize).setBottomRightCornerSize(cornerSize); + } + MaterialShapeDrawable drawable = new MaterialShapeDrawable(shapeBuilder.build()); + int themeColor = ThemeUtils.getColorFromAttr(root.getContext(), android.R.attr.colorBackground); + drawable.setFillColor(ColorStateList.valueOf(themeColor)); + root.setBackground(drawable); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + EventBus.getDefault().register(this); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + EventBus.getDefault().unregister(this); + if (disposable != null) { + disposable.dispose(); + } + getContext().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) + .unregisterOnSharedPreferenceChangeListener(this); + } + + @Override + public void onCreateContextMenu(@NonNull ContextMenu menu, @NonNull View v, ContextMenu.ContextMenuInfo menuInfo) { + super.onCreateContextMenu(menu, v, menuInfo); + MenuInflater inflater = getActivity().getMenuInflater(); + menu.setHeaderTitle(contextPressedItem.getTitle()); + if (contextPressedItem.type == NavDrawerData.DrawerItem.Type.FEED) { + inflater.inflate(R.menu.nav_feed_context, menu); + // episodes are not loaded, so we cannot check if the podcast has new or unplayed ones! + } else { + inflater.inflate(R.menu.nav_folder_context, menu); + } + MenuItemUtils.setOnClickListeners(menu, this::onContextItemSelected); + } + + @Override + public boolean onContextItemSelected(@NonNull MenuItem item) { + NavDrawerData.DrawerItem pressedItem = contextPressedItem; + contextPressedItem = null; + if (pressedItem == null) { + return false; + } + if (pressedItem.type == NavDrawerData.DrawerItem.Type.FEED) { + return onFeedContextMenuClicked(((NavDrawerData.FeedDrawerItem) pressedItem).feed, item); + } else { + return onTagContextMenuClicked(pressedItem, item); + } + } + + private boolean onFeedContextMenuClicked(Feed feed, MenuItem item) { + final int itemId = item.getItemId(); + if (itemId == R.id.remove_all_inbox_item) { + ConfirmationDialog removeAllNewFlagsConfirmationDialog = new ConfirmationDialog(getContext(), + R.string.remove_all_inbox_label, + R.string.remove_all_inbox_confirmation_msg) { + @Override + public void onConfirmButtonPressed(DialogInterface dialog) { + dialog.dismiss(); + DBWriter.removeFeedNewFlag(feed.getId()); + } + }; + removeAllNewFlagsConfirmationDialog.createNewDialog().show(); + return true; + } else if (itemId == R.id.edit_tags) { + TagSettingsDialog.newInstance(Collections.singletonList(feed.getPreferences())) + .show(getChildFragmentManager(), TagSettingsDialog.TAG); + return true; + } else if (itemId == R.id.rename_item) { + new RenameFeedDialog(getActivity(), feed).show(); + return true; + } else if (itemId == R.id.remove_feed) { + RemoveFeedDialog.show(getContext(), feed, () -> { + if (String.valueOf(feed.getId()).equals(getLastNavFragment(getContext()))) { + ((MainActivity) getActivity()).loadFragment(UserPreferences.getDefaultPage(), null); + // Make sure fragment is hidden before actually starting to delete + getActivity().getSupportFragmentManager().executePendingTransactions(); + } + }); + return true; + } + return super.onContextItemSelected(item); + } + + private boolean onTagContextMenuClicked(NavDrawerData.DrawerItem drawerItem, MenuItem item) { + final int itemId = item.getItemId(); + if (itemId == R.id.rename_folder_item) { + new RenameFeedDialog(getActivity(), drawerItem).show(); + return true; + } + return super.onContextItemSelected(item); + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onUnreadItemsChanged(UnreadItemsUpdateEvent event) { + loadData(); + } + + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onFeedListChanged(FeedListUpdateEvent event) { + loadData(); + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onQueueChanged(QueueEvent event) { + Log.d(TAG, "onQueueChanged(" + event + ")"); + // we are only interested in the number of queue items, not download status or position + if (event.action == QueueEvent.Action.DELETED_MEDIA + || event.action == QueueEvent.Action.SORTED + || event.action == QueueEvent.Action.MOVED) { + return; + } + loadData(); + } + + @Override + public void onResume() { + super.onResume(); + loadData(); + } + + private final NavListAdapter.ItemAccess itemAccess = new NavListAdapter.ItemAccess() { + @Override + public int getCount() { + if (flatItemList != null) { + return flatItemList.size(); + } else { + return 0; + } + } + + @Override + public NavDrawerData.DrawerItem getItem(int position) { + if (flatItemList != null && 0 <= position && position < flatItemList.size()) { + return flatItemList.get(position); + } else { + return null; + } + } + + @Override + public boolean isSelected(int position) { + String lastNavFragment = getLastNavFragment(getContext()); + if (position < navAdapter.getSubscriptionOffset()) { + return navAdapter.getFragmentTags().get(position).equals(lastNavFragment); + } else if (StringUtils.isNumeric(lastNavFragment)) { // last fragment was not a list, but a feed + long feedId = Long.parseLong(lastNavFragment); + if (navDrawerData != null) { + NavDrawerData.DrawerItem itemToCheck = flatItemList.get( + position - navAdapter.getSubscriptionOffset()); + if (itemToCheck.type == NavDrawerData.DrawerItem.Type.FEED) { + // When the same feed is displayed multiple times, it should be highlighted multiple times. + return ((NavDrawerData.FeedDrawerItem) itemToCheck).feed.getId() == feedId; + } + } + } + return false; + } + + @Override + public int getQueueSize() { + return (navDrawerData != null) ? navDrawerData.queueSize : 0; + } + + @Override + public int getNumberOfNewItems() { + return (navDrawerData != null) ? navDrawerData.numNewItems : 0; + } + + @Override + public int getNumberOfDownloadedItems() { + return (navDrawerData != null) ? navDrawerData.numDownloadedItems : 0; + } + + @Override + public int getReclaimableItems() { + return reclaimableSpace; + } + + @Override + public int getFeedCounterSum() { + if (navDrawerData == null) { + return 0; + } + int sum = 0; + for (int counter : navDrawerData.feedCounters.values()) { + sum += counter; + } + return sum; + } + + @Override + public void onItemClick(int position) { + int viewType = navAdapter.getItemViewType(position); + if (viewType != NavListAdapter.VIEW_TYPE_SECTION_DIVIDER) { + if (position < navAdapter.getSubscriptionOffset()) { + String tag = navAdapter.getFragmentTags().get(position); + ((MainActivity) getActivity()).loadFragment(tag, null); + ((MainActivity) getActivity()).getBottomSheet().setState(BottomSheetBehavior.STATE_COLLAPSED); + } else { + int pos = position - navAdapter.getSubscriptionOffset(); + NavDrawerData.DrawerItem clickedItem = flatItemList.get(pos); + + if (clickedItem.type == NavDrawerData.DrawerItem.Type.FEED) { + long feedId = ((NavDrawerData.FeedDrawerItem) clickedItem).feed.getId(); + ((MainActivity) getActivity()).loadFeedFragmentById(feedId, null); + ((MainActivity) getActivity()).getBottomSheet() + .setState(BottomSheetBehavior.STATE_COLLAPSED); + } else { + NavDrawerData.TagDrawerItem folder = ((NavDrawerData.TagDrawerItem) clickedItem); + if (openFolders.contains(folder.getTitle())) { + openFolders.remove(folder.getTitle()); + } else { + openFolders.add(folder.getTitle()); + } + + getContext().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) + .edit() + .putStringSet(PREF_OPEN_FOLDERS, openFolders) + .apply(); + + disposable = Observable.fromCallable(() -> makeFlatDrawerData(navDrawerData.items, 0)) + .subscribeOn(Schedulers.computation()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + result -> { + flatItemList = result; + navAdapter.notifyDataSetChanged(); + }, error -> Log.e(TAG, Log.getStackTraceString(error))); + } + } + } else if (UserPreferences.getSubscriptionsFilter().isEnabled() + && navAdapter.showSubscriptionList) { + new SubscriptionsFilterDialog().show(getChildFragmentManager(), "filter"); + } + } + + @Override + public boolean onItemLongClick(int position) { + if (position < navAdapter.getFragmentTags().size()) { + DrawerPreferencesDialog.show(getContext(), () -> { + navAdapter.notifyDataSetChanged(); + if (UserPreferences.getHiddenDrawerItems().contains(getLastNavFragment(getContext()))) { + new MainActivityStarter(getContext()) + .withFragmentLoaded(UserPreferences.getDefaultPage()) + .withDrawerOpen() + .start(); + } + }); + return true; + } else { + contextPressedItem = flatItemList.get(position - navAdapter.getSubscriptionOffset()); + return false; + } + } + + @Override + public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { + NavDrawerFragment.this.onCreateContextMenu(menu, v, menuInfo); + } + }; + + private void loadData() { + disposable = Observable.fromCallable( + () -> { + NavDrawerData data = DBReader.getNavDrawerData(UserPreferences.getSubscriptionsFilter(), + UserPreferences.getFeedOrder(), UserPreferences.getFeedCounterSetting()); + reclaimableSpace = EpisodeCleanupAlgorithmFactory.build().getReclaimableItems(); + return new Pair<>(data, makeFlatDrawerData(data.items, 0)); + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + result -> { + navDrawerData = result.first; + flatItemList = result.second; + navAdapter.notifyDataSetChanged(); + progressBar.setVisibility(View.GONE); // Stays hidden once there is something in the list + }, error -> { + Log.e(TAG, Log.getStackTraceString(error)); + progressBar.setVisibility(View.GONE); + }); + } + + private List makeFlatDrawerData(List items, int layer) { + List flatItems = new ArrayList<>(); + for (NavDrawerData.DrawerItem item : items) { + item.setLayer(layer); + flatItems.add(item); + if (item.type == NavDrawerData.DrawerItem.Type.TAG) { + NavDrawerData.TagDrawerItem folder = ((NavDrawerData.TagDrawerItem) item); + folder.setOpen(openFolders.contains(folder.getTitle())); + if (folder.isOpen()) { + flatItems.addAll(makeFlatDrawerData(((NavDrawerData.TagDrawerItem) item).children, layer + 1)); + } + } + } + return flatItems; + } + + public static void saveLastNavFragment(Context context, String tag) { + Log.d(TAG, "saveLastNavFragment(tag: " + tag + ")"); + SharedPreferences prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); + SharedPreferences.Editor edit = prefs.edit(); + if (tag != null) { + edit.putString(PREF_LAST_FRAGMENT_TAG, tag); + } else { + edit.remove(PREF_LAST_FRAGMENT_TAG); + } + edit.apply(); + } + + public static String getLastNavFragment(Context context) { + SharedPreferences prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); + String lastFragment = prefs.getString(PREF_LAST_FRAGMENT_TAG, HomeFragment.TAG); + Log.d(TAG, "getLastNavFragment() -> " + lastFragment); + return lastFragment; + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + if (PREF_LAST_FRAGMENT_TAG.equals(key)) { + navAdapter.notifyDataSetChanged(); // Update selection + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/screen/drawer/NavListAdapter.java b/app/src/main/java/de/danoeh/antennapod/ui/screen/drawer/NavListAdapter.java new file mode 100644 index 000000000..aaf872460 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/screen/drawer/NavListAdapter.java @@ -0,0 +1,419 @@ +package de.danoeh.antennapod.ui.screen.drawer; + +import android.app.Activity; +import android.content.Intent; +import android.content.SharedPreferences; +import android.os.Build; +import android.view.ContextMenu; +import android.view.InputDevice; +import android.view.LayoutInflater; +import androidx.annotation.DrawableRes; +import androidx.annotation.NonNull; +import androidx.preference.PreferenceManager; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.RelativeLayout; +import android.widget.TextView; +import com.bumptech.glide.load.resource.bitmap.FitCenter; +import com.bumptech.glide.load.resource.bitmap.RoundedCorners; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import androidx.recyclerview.widget.RecyclerView; +import com.bumptech.glide.Glide; +import com.bumptech.glide.request.RequestOptions; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.ui.screen.preferences.PreferenceActivity; +import de.danoeh.antennapod.ui.screen.AllEpisodesFragment; +import de.danoeh.antennapod.ui.screen.download.CompletedDownloadsFragment; +import de.danoeh.antennapod.ui.screen.InboxFragment; +import de.danoeh.antennapod.model.feed.Feed; +import de.danoeh.antennapod.storage.preferences.UserPreferences; +import de.danoeh.antennapod.storage.database.NavDrawerData; +import de.danoeh.antennapod.ui.screen.AddFeedFragment; +import de.danoeh.antennapod.ui.screen.PlaybackHistoryFragment; +import de.danoeh.antennapod.ui.screen.queue.QueueFragment; +import de.danoeh.antennapod.ui.screen.subscriptions.SubscriptionFragment; +import de.danoeh.antennapod.ui.screen.home.HomeFragment; +import org.apache.commons.lang3.ArrayUtils; + +import java.lang.ref.WeakReference; +import java.text.NumberFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * BaseAdapter for the navigation drawer + */ +public class NavListAdapter extends RecyclerView.Adapter + implements SharedPreferences.OnSharedPreferenceChangeListener { + + public static final int VIEW_TYPE_NAV = 0; + public static final int VIEW_TYPE_SECTION_DIVIDER = 1; + private static final int VIEW_TYPE_SUBSCRIPTION = 2; + + /** + * a tag used as a placeholder to indicate if the subscription list should be displayed or not + * This tag doesn't correspond to any specific activity. + */ + public static final String SUBSCRIPTION_LIST_TAG = "SubscriptionList"; + + private final List fragmentTags = new ArrayList<>(); + private final String[] titles; + private final ItemAccess itemAccess; + private final WeakReference activity; + public boolean showSubscriptionList = true; + + public NavListAdapter(ItemAccess itemAccess, Activity context) { + this.itemAccess = itemAccess; + this.activity = new WeakReference<>(context); + + titles = context.getResources().getStringArray(R.array.nav_drawer_titles); + loadItems(); + + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + prefs.registerOnSharedPreferenceChangeListener(this); + } + + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + if (UserPreferences.PREF_HIDDEN_DRAWER_ITEMS.equals(key)) { + loadItems(); + } + } + + private void loadItems() { + List newTags = new ArrayList<>(Arrays.asList(NavDrawerFragment.NAV_DRAWER_TAGS)); + List hiddenFragments = UserPreferences.getHiddenDrawerItems(); + newTags.removeAll(hiddenFragments); + + if (newTags.contains(SUBSCRIPTION_LIST_TAG)) { + // we never want SUBSCRIPTION_LIST_TAG to be in 'tags' + // since it doesn't actually correspond to a position in the list, but is + // a placeholder that indicates if we should show the subscription list in the + // nav drawer at all. + showSubscriptionList = true; + newTags.remove(SUBSCRIPTION_LIST_TAG); + } else { + showSubscriptionList = false; + } + + fragmentTags.clear(); + fragmentTags.addAll(newTags); + notifyDataSetChanged(); + } + + public String getLabel(String tag) { + int index = ArrayUtils.indexOf(NavDrawerFragment.NAV_DRAWER_TAGS, tag); + return titles[index]; + } + + private @DrawableRes int getDrawable(String tag) { + switch (tag) { + case HomeFragment.TAG: + return R.drawable.ic_home; + case QueueFragment.TAG: + return R.drawable.ic_playlist_play; + case InboxFragment.TAG: + return R.drawable.ic_inbox; + case AllEpisodesFragment.TAG: + return R.drawable.ic_feed; + case CompletedDownloadsFragment.TAG: + return R.drawable.ic_download; + case PlaybackHistoryFragment.TAG: + return R.drawable.ic_history; + case SubscriptionFragment.TAG: + return R.drawable.ic_subscriptions; + case AddFeedFragment.TAG: + return R.drawable.ic_add; + default: + return 0; + } + } + + public List getFragmentTags() { + return Collections.unmodifiableList(fragmentTags); + } + + @Override + public int getItemCount() { + int baseCount = getSubscriptionOffset(); + if (showSubscriptionList) { + baseCount += itemAccess.getCount(); + } + return baseCount; + } + + @Override + public long getItemId(int position) { + int viewType = getItemViewType(position); + if (viewType == VIEW_TYPE_SUBSCRIPTION) { + return itemAccess.getItem(position - getSubscriptionOffset()).id; + } else if (viewType == VIEW_TYPE_NAV) { + return -Math.abs((long) fragmentTags.get(position).hashCode()) - 1; // Folder IDs are >0 + } else { + return 0; + } + } + + @Override + public int getItemViewType(int position) { + if (0 <= position && position < fragmentTags.size()) { + return VIEW_TYPE_NAV; + } else if (position < getSubscriptionOffset()) { + return VIEW_TYPE_SECTION_DIVIDER; + } else { + return VIEW_TYPE_SUBSCRIPTION; + } + } + + public int getSubscriptionOffset() { + return fragmentTags.size() > 0 ? fragmentTags.size() + 1 : 0; + } + + @NonNull + @Override + public Holder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + LayoutInflater inflater = LayoutInflater.from(activity.get()); + if (viewType == VIEW_TYPE_NAV) { + return new NavHolder(inflater.inflate(R.layout.nav_listitem, parent, false)); + } else if (viewType == VIEW_TYPE_SECTION_DIVIDER) { + return new DividerHolder(inflater.inflate(R.layout.nav_section_item, parent, false)); + } else { + return new FeedHolder(inflater.inflate(R.layout.nav_listitem, parent, false)); + } + } + + @Override + public void onBindViewHolder(@NonNull Holder holder, int position) { + int viewType = getItemViewType(position); + + holder.itemView.setOnCreateContextMenuListener(null); + if (viewType == VIEW_TYPE_NAV) { + bindNavView(getLabel(fragmentTags.get(position)), position, (NavHolder) holder); + } else if (viewType == VIEW_TYPE_SECTION_DIVIDER) { + bindSectionDivider((DividerHolder) holder); + } else { + int itemPos = position - getSubscriptionOffset(); + NavDrawerData.DrawerItem item = itemAccess.getItem(itemPos); + bindListItem(item, (FeedHolder) holder); + if (item.type == NavDrawerData.DrawerItem.Type.FEED) { + bindFeedView((NavDrawerData.FeedDrawerItem) item, (FeedHolder) holder); + } else { + bindTagView((NavDrawerData.TagDrawerItem) item, (FeedHolder) holder); + } + holder.itemView.setOnCreateContextMenuListener(itemAccess); + } + if (viewType != VIEW_TYPE_SECTION_DIVIDER) { + holder.itemView.setSelected(itemAccess.isSelected(position)); + holder.itemView.setOnClickListener(v -> itemAccess.onItemClick(position)); + holder.itemView.setOnLongClickListener(v -> itemAccess.onItemLongClick(position)); + holder.itemView.setOnTouchListener((v, e) -> { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (e.isFromSource(InputDevice.SOURCE_MOUSE) + && e.getButtonState() == MotionEvent.BUTTON_SECONDARY) { + itemAccess.onItemLongClick(position); + return false; + } + } + return false; + }); + } + } + + private void bindNavView(String title, int position, NavHolder holder) { + Activity context = activity.get(); + if (context == null) { + return; + } + holder.title.setText(title); + + // reset for re-use + holder.count.setVisibility(View.GONE); + holder.count.setOnClickListener(null); + holder.count.setClickable(false); + + String tag = fragmentTags.get(position); + if (tag.equals(QueueFragment.TAG)) { + int queueSize = itemAccess.getQueueSize(); + if (queueSize > 0) { + holder.count.setText(NumberFormat.getInstance().format(queueSize)); + holder.count.setVisibility(View.VISIBLE); + } + } else if (tag.equals(InboxFragment.TAG)) { + int unreadItems = itemAccess.getNumberOfNewItems(); + if (unreadItems > 0) { + holder.count.setText(NumberFormat.getInstance().format(unreadItems)); + holder.count.setVisibility(View.VISIBLE); + } + } else if (tag.equals(SubscriptionFragment.TAG)) { + int sum = itemAccess.getFeedCounterSum(); + if (sum > 0) { + holder.count.setText(NumberFormat.getInstance().format(sum)); + holder.count.setVisibility(View.VISIBLE); + } + } else if (tag.equals(CompletedDownloadsFragment.TAG) && UserPreferences.isEnableAutodownload()) { + int epCacheSize = UserPreferences.getEpisodeCacheSize(); + // don't count episodes that can be reclaimed + int spaceUsed = itemAccess.getNumberOfDownloadedItems() + - itemAccess.getReclaimableItems(); + if (epCacheSize > 0 && spaceUsed >= epCacheSize) { + holder.count.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_disc_alert, 0); + holder.count.setVisibility(View.VISIBLE); + holder.count.setOnClickListener(v -> + new MaterialAlertDialogBuilder(context) + .setTitle(R.string.episode_cache_full_title) + .setMessage(R.string.episode_cache_full_message) + .setPositiveButton(android.R.string.ok, null) + .setNeutralButton(R.string.open_autodownload_settings, (dialog, which) -> { + Intent intent = new Intent(context, PreferenceActivity.class); + intent.putExtra(PreferenceActivity.OPEN_AUTO_DOWNLOAD_SETTINGS, true); + context.startActivity(intent); + }) + .show() + ); + } + } + + holder.image.setImageResource(getDrawable(fragmentTags.get(position))); + } + + private void bindSectionDivider(DividerHolder holder) { + Activity context = activity.get(); + if (context == null) { + return; + } + + if (UserPreferences.getSubscriptionsFilter().isEnabled() && showSubscriptionList) { + holder.itemView.setEnabled(true); + holder.feedsFilteredMsg.setVisibility(View.VISIBLE); + } else { + holder.itemView.setEnabled(false); + holder.feedsFilteredMsg.setVisibility(View.GONE); + } + } + + private void bindListItem(NavDrawerData.DrawerItem item, FeedHolder holder) { + if (item.getCounter() > 0) { + holder.count.setVisibility(View.VISIBLE); + holder.count.setText(NumberFormat.getInstance().format(item.getCounter())); + } else { + holder.count.setVisibility(View.GONE); + } + holder.title.setText(item.getTitle()); + int padding = (int) (activity.get().getResources().getDimension(R.dimen.thumbnail_length_navlist) / 2); + holder.itemView.setPadding(item.getLayer() * padding, 0, 0, 0); + } + + private void bindFeedView(NavDrawerData.FeedDrawerItem drawerItem, FeedHolder holder) { + Feed feed = drawerItem.feed; + Activity context = activity.get(); + if (context == null) { + return; + } + + Glide.with(context) + .load(feed.getImageUrl()) + .apply(new RequestOptions() + .placeholder(R.color.light_gray) + .error(R.color.light_gray) + .transform(new FitCenter(), + new RoundedCorners((int) (4 * context.getResources().getDisplayMetrics().density))) + .dontAnimate()) + .into(holder.image); + + if (feed.hasLastUpdateFailed()) { + RelativeLayout.LayoutParams p = (RelativeLayout.LayoutParams) holder.title.getLayoutParams(); + p.addRule(RelativeLayout.LEFT_OF, R.id.itxtvFailure); + holder.failure.setVisibility(View.VISIBLE); + } else { + RelativeLayout.LayoutParams p = (RelativeLayout.LayoutParams) holder.title.getLayoutParams(); + p.addRule(RelativeLayout.LEFT_OF, R.id.txtvCount); + holder.failure.setVisibility(View.GONE); + } + } + + private void bindTagView(NavDrawerData.TagDrawerItem tag, FeedHolder holder) { + Activity context = activity.get(); + if (context == null) { + return; + } + if (tag.isOpen()) { + holder.count.setVisibility(View.GONE); + } + Glide.with(context).clear(holder.image); + holder.image.setImageResource(R.drawable.ic_tag); + holder.failure.setVisibility(View.GONE); + } + + static class Holder extends RecyclerView.ViewHolder { + public Holder(@NonNull View itemView) { + super(itemView); + } + } + + static class DividerHolder extends Holder { + final LinearLayout feedsFilteredMsg; + + public DividerHolder(@NonNull View itemView) { + super(itemView); + feedsFilteredMsg = itemView.findViewById(R.id.nav_feeds_filtered_message); + } + } + + static class NavHolder extends Holder { + final ImageView image; + final TextView title; + final TextView count; + + public NavHolder(@NonNull View itemView) { + super(itemView); + image = itemView.findViewById(R.id.imgvCover); + title = itemView.findViewById(R.id.txtvTitle); + count = itemView.findViewById(R.id.txtvCount); + } + } + + static class FeedHolder extends Holder { + final ImageView image; + final TextView title; + final ImageView failure; + final TextView count; + + public FeedHolder(@NonNull View itemView) { + super(itemView); + image = itemView.findViewById(R.id.imgvCover); + title = itemView.findViewById(R.id.txtvTitle); + failure = itemView.findViewById(R.id.itxtvFailure); + count = itemView.findViewById(R.id.txtvCount); + } + } + + public interface ItemAccess extends View.OnCreateContextMenuListener { + int getCount(); + + NavDrawerData.DrawerItem getItem(int position); + + boolean isSelected(int position); + + int getQueueSize(); + + int getNumberOfNewItems(); + + int getNumberOfDownloadedItems(); + + int getReclaimableItems(); + + int getFeedCounterSum(); + + void onItemClick(int position); + + boolean onItemLongClick(int position); + + @Override + void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo); + } + +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/screen/episode/ItemFragment.java b/app/src/main/java/de/danoeh/antennapod/ui/screen/episode/ItemFragment.java new file mode 100644 index 000000000..b3a5cbbf0 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/screen/episode/ItemFragment.java @@ -0,0 +1,436 @@ +package de.danoeh.antennapod.ui.screen.episode; + +import android.content.Context; +import android.os.Build; +import android.os.Bundle; +import android.text.Layout; +import android.text.TextUtils; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.ImageView; +import android.widget.ProgressBar; +import android.widget.TextView; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import com.bumptech.glide.Glide; +import com.bumptech.glide.load.resource.bitmap.FitCenter; +import com.bumptech.glide.load.resource.bitmap.RoundedCorners; +import com.bumptech.glide.request.RequestOptions; +import com.google.android.material.snackbar.Snackbar; +import com.skydoves.balloon.ArrowOrientation; +import com.skydoves.balloon.ArrowOrientationRules; +import com.skydoves.balloon.Balloon; +import com.skydoves.balloon.BalloonAnimation; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.actionbutton.CancelDownloadActionButton; +import de.danoeh.antennapod.actionbutton.DeleteActionButton; +import de.danoeh.antennapod.actionbutton.DownloadActionButton; +import de.danoeh.antennapod.actionbutton.ItemActionButton; +import de.danoeh.antennapod.actionbutton.MarkAsPlayedActionButton; +import de.danoeh.antennapod.actionbutton.PauseActionButton; +import de.danoeh.antennapod.actionbutton.PlayActionButton; +import de.danoeh.antennapod.actionbutton.PlayLocalActionButton; +import de.danoeh.antennapod.actionbutton.StreamActionButton; +import de.danoeh.antennapod.actionbutton.VisitWebsiteActionButton; +import de.danoeh.antennapod.event.EpisodeDownloadEvent; +import de.danoeh.antennapod.playback.service.PlaybackStatus; +import de.danoeh.antennapod.event.FeedItemEvent; +import de.danoeh.antennapod.event.PlayerStatusEvent; +import de.danoeh.antennapod.event.UnreadItemsUpdateEvent; +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedMedia; +import de.danoeh.antennapod.playback.service.PlaybackController; +import de.danoeh.antennapod.storage.preferences.UsageStatistics; +import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterface; +import de.danoeh.antennapod.storage.preferences.UserPreferences; +import de.danoeh.antennapod.storage.database.DBReader; +import de.danoeh.antennapod.ui.common.Converter; +import de.danoeh.antennapod.ui.common.DateFormatter; +import de.danoeh.antennapod.ui.common.CircularProgressBar; +import de.danoeh.antennapod.ui.common.ThemeUtils; +import de.danoeh.antennapod.ui.cleaner.ShownotesCleaner; +import de.danoeh.antennapod.ui.episodes.ImageResourceUtils; +import de.danoeh.antennapod.ui.screen.feed.FeedItemlistFragment; +import de.danoeh.antennapod.ui.view.ShownotesWebView; +import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; +import io.reactivex.schedulers.Schedulers; +import org.greenrobot.eventbus.EventBus; +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; + +import java.util.Locale; +import java.util.Objects; + +/** + * Displays information about a FeedItem and actions. + */ +public class ItemFragment extends Fragment { + + private static final String TAG = "ItemFragment"; + private static final String ARG_FEEDITEM = "feeditem"; + + /** + * Creates a new instance of an ItemFragment + * + * @param feeditem The ID of the FeedItem to show + * @return The ItemFragment instance + */ + public static ItemFragment newInstance(long feeditem) { + ItemFragment fragment = new ItemFragment(); + Bundle args = new Bundle(); + args.putLong(ARG_FEEDITEM, feeditem); + fragment.setArguments(args); + return fragment; + } + + private boolean itemsLoaded = false; + private long itemId; + private FeedItem item; + private String webviewData; + + private ViewGroup root; + private ShownotesWebView webvDescription; + private TextView txtvPodcast; + private TextView txtvTitle; + private TextView txtvDuration; + private TextView txtvPublished; + private ImageView imgvCover; + private CircularProgressBar progbarDownload; + private ProgressBar progbarLoading; + private TextView butAction1Text; + private TextView butAction2Text; + private ImageView butAction1Icon; + private ImageView butAction2Icon; + private View butAction1; + private View butAction2; + private ItemActionButton actionButton1; + private ItemActionButton actionButton2; + private View noMediaLabel; + + private Disposable disposable; + private PlaybackController controller; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + itemId = getArguments().getLong(ARG_FEEDITEM); + } + + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + super.onCreateView(inflater, container, savedInstanceState); + View layout = inflater.inflate(R.layout.feeditem_fragment, container, false); + + root = layout.findViewById(R.id.content_root); + + txtvPodcast = layout.findViewById(R.id.txtvPodcast); + txtvPodcast.setOnClickListener(v -> openPodcast()); + txtvTitle = layout.findViewById(R.id.txtvTitle); + if (Build.VERSION.SDK_INT >= 23) { + txtvTitle.setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_FULL); + } + txtvDuration = layout.findViewById(R.id.txtvDuration); + txtvPublished = layout.findViewById(R.id.txtvPublished); + txtvTitle.setEllipsize(TextUtils.TruncateAt.END); + webvDescription = layout.findViewById(R.id.webvDescription); + webvDescription.setTimecodeSelectedListener(time -> { + if (controller != null && item.getMedia() != null && controller.getMedia() != null + && Objects.equals(item.getMedia().getIdentifier(), controller.getMedia().getIdentifier())) { + controller.seekTo(time); + } else { + ((MainActivity) getActivity()).showSnackbarAbovePlayer(R.string.play_this_to_seek_position, + Snackbar.LENGTH_LONG); + } + }); + registerForContextMenu(webvDescription); + + imgvCover = layout.findViewById(R.id.imgvCover); + imgvCover.setOnClickListener(v -> openPodcast()); + progbarDownload = layout.findViewById(R.id.circularProgressBar); + progbarLoading = layout.findViewById(R.id.progbarLoading); + butAction1 = layout.findViewById(R.id.butAction1); + butAction2 = layout.findViewById(R.id.butAction2); + butAction1Icon = layout.findViewById(R.id.butAction1Icon); + butAction2Icon = layout.findViewById(R.id.butAction2Icon); + butAction1Text = layout.findViewById(R.id.butAction1Text); + butAction2Text = layout.findViewById(R.id.butAction2Text); + noMediaLabel = layout.findViewById(R.id.noMediaLabel); + + butAction1.setOnClickListener(v -> { + if (actionButton1 instanceof StreamActionButton && !UserPreferences.isStreamOverDownload() + && UsageStatistics.hasSignificantBiasTo(UsageStatistics.ACTION_STREAM)) { + showOnDemandConfigBalloon(true); + return; + } else if (actionButton1 == null) { + return; // Not loaded yet + } + actionButton1.onClick(getContext()); + }); + butAction2.setOnClickListener(v -> { + if (actionButton2 instanceof DownloadActionButton && UserPreferences.isStreamOverDownload() + && UsageStatistics.hasSignificantBiasTo(UsageStatistics.ACTION_DOWNLOAD)) { + showOnDemandConfigBalloon(false); + return; + } else if (actionButton2 == null) { + return; // Not loaded yet + } + actionButton2.onClick(getContext()); + }); + return layout; + } + + private void showOnDemandConfigBalloon(boolean offerStreaming) { + final boolean isLocaleRtl = TextUtils.getLayoutDirectionFromLocale(Locale.getDefault()) + == View.LAYOUT_DIRECTION_RTL; + final Balloon balloon = new Balloon.Builder(getContext()) + .setArrowOrientation(ArrowOrientation.TOP) + .setArrowOrientationRules(ArrowOrientationRules.ALIGN_FIXED) + .setArrowPosition(0.25f + ((isLocaleRtl ^ offerStreaming) ? 0f : 0.5f)) + .setWidthRatio(1.0f) + .setMarginLeft(8) + .setMarginRight(8) + .setBackgroundColor(ThemeUtils.getColorFromAttr(getContext(), R.attr.colorSecondary)) + .setBalloonAnimation(BalloonAnimation.OVERSHOOT) + .setLayout(R.layout.popup_bubble_view) + .setDismissWhenTouchOutside(true) + .setLifecycleOwner(this) + .build(); + final Button positiveButton = balloon.getContentView().findViewById(R.id.balloon_button_positive); + final Button negativeButton = balloon.getContentView().findViewById(R.id.balloon_button_negative); + final TextView message = balloon.getContentView().findViewById(R.id.balloon_message); + message.setText(offerStreaming + ? R.string.on_demand_config_stream_text : R.string.on_demand_config_download_text); + positiveButton.setOnClickListener(v1 -> { + UserPreferences.setStreamOverDownload(offerStreaming); + // Update all visible lists to reflect new streaming action button + EventBus.getDefault().post(new UnreadItemsUpdateEvent()); + ((MainActivity) getActivity()).showSnackbarAbovePlayer( + R.string.on_demand_config_setting_changed, Snackbar.LENGTH_SHORT); + balloon.dismiss(); + }); + negativeButton.setOnClickListener(v1 -> { + UsageStatistics.doNotAskAgain(UsageStatistics.ACTION_STREAM); // Type does not matter. Both are silenced. + balloon.dismiss(); + }); + balloon.showAlignBottom(butAction1, 0, (int) (-12 * getResources().getDisplayMetrics().density)); + } + + @Override + public void onStart() { + super.onStart(); + EventBus.getDefault().register(this); + controller = new PlaybackController(getActivity()) { + @Override + public void loadMediaInfo() { + // Do nothing + } + }; + controller.init(); + load(); + } + + @Override + public void onResume() { + super.onResume(); + if (itemsLoaded) { + progbarLoading.setVisibility(View.GONE); + updateAppearance(); + } + } + + @Override + public void onStop() { + super.onStop(); + EventBus.getDefault().unregister(this); + controller.release(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + if (disposable != null) { + disposable.dispose(); + } + if (webvDescription != null && root != null) { + root.removeView(webvDescription); + webvDescription.destroy(); + } + } + + private void onFragmentLoaded() { + if (webviewData != null && !itemsLoaded) { + webvDescription.loadDataWithBaseURL("https://127.0.0.1", webviewData, "text/html", "utf-8", "about:blank"); + } + updateAppearance(); + } + + private void updateAppearance() { + if (item == null) { + Log.d(TAG, "updateAppearance item is null"); + return; + } + txtvPodcast.setText(item.getFeed().getTitle()); + txtvTitle.setText(item.getTitle()); + + if (item.getPubDate() != null) { + String pubDateStr = DateFormatter.formatAbbrev(getActivity(), item.getPubDate()); + txtvPublished.setText(pubDateStr); + txtvPublished.setContentDescription(DateFormatter.formatForAccessibility(item.getPubDate())); + } + + RequestOptions options = new RequestOptions() + .error(R.color.light_gray) + .transform(new FitCenter(), + new RoundedCorners((int) (8 * getResources().getDisplayMetrics().density))) + .dontAnimate(); + + Glide.with(this) + .load(item.getImageLocation()) + .error(Glide.with(this) + .load(ImageResourceUtils.getFallbackImageLocation(item)) + .apply(options)) + .apply(options) + .into(imgvCover); + updateButtons(); + } + + private void updateButtons() { + progbarDownload.setVisibility(View.GONE); + if (item.hasMedia()) { + if (DownloadServiceInterface.get().isDownloadingEpisode(item.getMedia().getDownloadUrl())) { + progbarDownload.setVisibility(View.VISIBLE); + progbarDownload.setPercentage(0.01f * Math.max(1, + DownloadServiceInterface.get().getProgress(item.getMedia().getDownloadUrl())), item); + progbarDownload.setIndeterminate( + DownloadServiceInterface.get().isEpisodeQueued(item.getMedia().getDownloadUrl())); + } + } + + FeedMedia media = item.getMedia(); + if (media == null) { + actionButton1 = new MarkAsPlayedActionButton(item); + actionButton2 = new VisitWebsiteActionButton(item); + noMediaLabel.setVisibility(View.VISIBLE); + } else { + noMediaLabel.setVisibility(View.GONE); + if (media.getDuration() > 0) { + txtvDuration.setText(Converter.getDurationStringLong(media.getDuration())); + txtvDuration.setContentDescription( + Converter.getDurationStringLocalized(getContext(), media.getDuration())); + } + if (PlaybackStatus.isCurrentlyPlaying(media)) { + actionButton1 = new PauseActionButton(item); + } else if (item.getFeed().isLocalFeed()) { + actionButton1 = new PlayLocalActionButton(item); + } else if (media.isDownloaded()) { + actionButton1 = new PlayActionButton(item); + } else { + actionButton1 = new StreamActionButton(item); + } + if (DownloadServiceInterface.get().isDownloadingEpisode(media.getDownloadUrl())) { + actionButton2 = new CancelDownloadActionButton(item); + } else if (!media.isDownloaded()) { + actionButton2 = new DownloadActionButton(item); + } else { + actionButton2 = new DeleteActionButton(item); + } + } + + butAction1Text.setText(actionButton1.getLabel()); + butAction1Text.setTransformationMethod(null); + butAction1Icon.setImageResource(actionButton1.getDrawable()); + butAction1.setVisibility(actionButton1.getVisibility()); + + butAction2Text.setText(actionButton2.getLabel()); + butAction2Text.setTransformationMethod(null); + butAction2Icon.setImageResource(actionButton2.getDrawable()); + butAction2.setVisibility(actionButton2.getVisibility()); + } + + @Override + public boolean onContextItemSelected(MenuItem item) { + return webvDescription.onContextItemSelected(item); + } + + private void openPodcast() { + if (item == null) { + return; + } + Fragment fragment = FeedItemlistFragment.newInstance(item.getFeedId()); + ((MainActivity) getActivity()).loadChildFragment(fragment); + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onEventMainThread(FeedItemEvent event) { + Log.d(TAG, "onEventMainThread() called with: " + "event = [" + event + "]"); + for (FeedItem item : event.items) { + if (this.item.getId() == item.getId()) { + load(); + return; + } + } + } + + @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) + public void onEventMainThread(EpisodeDownloadEvent event) { + if (item == null || item.getMedia() == null) { + return; + } + if (!event.getUrls().contains(item.getMedia().getDownloadUrl())) { + return; + } + if (itemsLoaded && getActivity() != null) { + updateButtons(); + } + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onPlayerStatusChanged(PlayerStatusEvent event) { + updateButtons(); + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onUnreadItemsChanged(UnreadItemsUpdateEvent event) { + load(); + } + + private void load() { + if (disposable != null) { + disposable.dispose(); + } + if (!itemsLoaded) { + progbarLoading.setVisibility(View.VISIBLE); + } + disposable = Observable.fromCallable(this::loadInBackground) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(result -> { + progbarLoading.setVisibility(View.GONE); + item = result; + onFragmentLoaded(); + itemsLoaded = true; + }, error -> Log.e(TAG, Log.getStackTraceString(error))); + } + + @Nullable + private FeedItem loadInBackground() { + FeedItem feedItem = DBReader.getFeedItem(itemId); + Context context = getContext(); + if (feedItem != null && context != null) { + int duration = feedItem.getMedia() != null ? feedItem.getMedia().getDuration() : Integer.MAX_VALUE; + DBReader.loadDescriptionOfFeedItem(feedItem); + ShownotesCleaner t = new ShownotesCleaner(context, feedItem.getDescription(), duration); + webviewData = t.processShownotes(); + } + return feedItem; + } + +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/screen/episode/ItemPagerFragment.java b/app/src/main/java/de/danoeh/antennapod/ui/screen/episode/ItemPagerFragment.java new file mode 100644 index 000000000..524066587 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/screen/episode/ItemPagerFragment.java @@ -0,0 +1,189 @@ +package de.danoeh.antennapod.ui.screen.episode; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.android.material.appbar.MaterialToolbar; +import androidx.fragment.app.Fragment; +import androidx.viewpager2.adapter.FragmentStateAdapter; +import androidx.viewpager2.widget.ViewPager2; + +import de.danoeh.antennapod.ui.screen.feed.FeedItemlistFragment; +import org.greenrobot.eventbus.EventBus; +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; + +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.event.FeedItemEvent; +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.storage.database.DBReader; +import de.danoeh.antennapod.ui.episodeslist.FeedItemMenuHandler; +import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; +import io.reactivex.schedulers.Schedulers; + +/** + * Displays information about a list of FeedItems. + */ +public class ItemPagerFragment extends Fragment implements MaterialToolbar.OnMenuItemClickListener { + private static final String ARG_FEEDITEMS = "feeditems"; + private static final String ARG_FEEDITEM_POS = "feeditem_pos"; + private static final String KEY_PAGER_ID = "pager_id"; + private ViewPager2 pager; + + /** + * Creates a new instance of an ItemPagerFragment. + * + * @param feeditems The IDs of the FeedItems that belong to the same list + * @param feedItemPos The position of the FeedItem that is currently shown + * @return The ItemFragment instance + */ + public static ItemPagerFragment newInstance(long[] feeditems, int feedItemPos) { + ItemPagerFragment fragment = new ItemPagerFragment(); + Bundle args = new Bundle(); + args.putLongArray(ARG_FEEDITEMS, feeditems); + args.putInt(ARG_FEEDITEM_POS, Math.max(0, feedItemPos)); + fragment.setArguments(args); + return fragment; + } + + private long[] feedItems; + private FeedItem item; + private Disposable disposable; + private MaterialToolbar toolbar; + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + super.onCreateView(inflater, container, savedInstanceState); + View layout = inflater.inflate(R.layout.feeditem_pager_fragment, container, false); + toolbar = layout.findViewById(R.id.toolbar); + toolbar.setTitle(""); + toolbar.inflateMenu(R.menu.feeditem_options); + toolbar.setNavigationOnClickListener(v -> getParentFragmentManager().popBackStack()); + toolbar.setOnMenuItemClickListener(this); + + feedItems = getArguments().getLongArray(ARG_FEEDITEMS); + final int feedItemPos = Math.max(0, getArguments().getInt(ARG_FEEDITEM_POS)); + + pager = layout.findViewById(R.id.pager); + // FragmentStatePagerAdapter documentation: + // > When using FragmentStatePagerAdapter the host ViewPager must have a valid ID set. + // When opening multiple ItemPagerFragments by clicking "item" -> "visit podcast" -> "item" -> etc, + // the ID is no longer unique and FragmentStatePagerAdapter does not display any pages. + int newId = View.generateViewId(); + if (savedInstanceState != null && savedInstanceState.getInt(KEY_PAGER_ID, 0) != 0) { + // Restore state by using the same ID as before. ID collisions are prevented in MainActivity. + newId = savedInstanceState.getInt(KEY_PAGER_ID, 0); + } + pager.setId(newId); + pager.setAdapter(new ItemPagerAdapter(this)); + pager.setCurrentItem(feedItemPos, false); + pager.setOffscreenPageLimit(1); + loadItem(feedItems[feedItemPos]); + pager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() { + @Override + public void onPageSelected(int position) { + loadItem(feedItems[position]); + } + }); + + EventBus.getDefault().register(this); + return layout; + } + + @Override + public void onSaveInstanceState(@NonNull Bundle outState) { + super.onSaveInstanceState(outState); + outState.putInt(KEY_PAGER_ID, pager.getId()); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + EventBus.getDefault().unregister(this); + if (disposable != null) { + disposable.dispose(); + } + } + + private void loadItem(long itemId) { + if (disposable != null) { + disposable.dispose(); + } + + disposable = Observable.fromCallable(() -> DBReader.getFeedItem(itemId)) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(result -> { + item = result; + refreshToolbarState(); + }, Throwable::printStackTrace); + } + + public void refreshToolbarState() { + if (item == null) { + return; + } + if (item.hasMedia()) { + FeedItemMenuHandler.onPrepareMenu(toolbar.getMenu(), item); + } else { + // these are already available via button1 and button2 + FeedItemMenuHandler.onPrepareMenu(toolbar.getMenu(), item, + R.id.mark_read_item, R.id.visit_website_item); + } + } + + @Override + public boolean onMenuItemClick(MenuItem menuItem) { + if (menuItem.getItemId() == R.id.open_podcast) { + openPodcast(); + return true; + } + return FeedItemMenuHandler.onMenuItemClicked(this, menuItem.getItemId(), item); + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onEventMainThread(FeedItemEvent event) { + for (FeedItem item : event.items) { + if (this.item != null && this.item.getId() == item.getId()) { + this.item = item; + refreshToolbarState(); + return; + } + } + } + + private void openPodcast() { + if (item == null) { + return; + } + Fragment fragment = FeedItemlistFragment.newInstance(item.getFeedId()); + ((MainActivity) getActivity()).loadChildFragment(fragment); + } + + private class ItemPagerAdapter extends FragmentStateAdapter { + + ItemPagerAdapter(@NonNull Fragment fragment) { + super(fragment); + } + + @NonNull + @Override + public Fragment createFragment(int position) { + return ItemFragment.newInstance(feedItems[position]); + } + + @Override + public int getItemCount() { + return feedItems.length; + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/screen/feed/FeedInfoFragment.java b/app/src/main/java/de/danoeh/antennapod/ui/screen/feed/FeedInfoFragment.java new file mode 100644 index 000000000..3e7fd3cdc --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/screen/feed/FeedInfoFragment.java @@ -0,0 +1,362 @@ +package de.danoeh.antennapod.ui.screen.feed; + +import android.content.ActivityNotFoundException; +import android.content.ClipData; +import android.content.Context; +import android.content.Intent; +import android.content.res.Configuration; +import android.graphics.LightingColorFilter; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.text.TextUtils; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; +import android.widget.Toast; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.content.res.AppCompatResources; +import androidx.documentfile.provider.DocumentFile; +import androidx.fragment.app.Fragment; +import com.bumptech.glide.Glide; +import com.bumptech.glide.request.RequestOptions; +import com.google.android.material.appbar.AppBarLayout; +import com.google.android.material.appbar.CollapsingToolbarLayout; +import com.google.android.material.appbar.MaterialToolbar; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import com.google.android.material.snackbar.Snackbar; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.ui.TransitionEffect; +import de.danoeh.antennapod.storage.database.DBReader; +import de.danoeh.antennapod.storage.database.FeedDatabaseWriter; +import de.danoeh.antennapod.core.util.IntentUtils; +import de.danoeh.antennapod.ui.share.ShareUtils; +import de.danoeh.antennapod.ui.cleaner.HtmlToPlainText; +import de.danoeh.antennapod.model.feed.Feed; +import de.danoeh.antennapod.model.feed.FeedFunding; +import de.danoeh.antennapod.ui.glide.FastBlurTransformation; +import de.danoeh.antennapod.ui.screen.feed.preferences.EditUrlSettingsDialog; +import de.danoeh.antennapod.ui.statistics.StatisticsFragment; +import de.danoeh.antennapod.ui.statistics.feed.FeedStatisticsFragment; +import io.reactivex.Completable; +import io.reactivex.Maybe; +import io.reactivex.MaybeOnSubscribe; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; +import io.reactivex.schedulers.Schedulers; +import org.apache.commons.lang3.StringUtils; + +import java.util.ArrayList; +import java.util.Iterator; + +/** + * Displays information about a feed. + */ +public class FeedInfoFragment extends Fragment implements MaterialToolbar.OnMenuItemClickListener { + + private static final String EXTRA_FEED_ID = "de.danoeh.antennapod.extra.feedId"; + private static final String TAG = "FeedInfoActivity"; + private final ActivityResultLauncher addLocalFolderLauncher = + registerForActivityResult(new AddLocalFolder(), this::addLocalFolderResult); + + private Feed feed; + private Disposable disposable; + private ImageView imgvCover; + private TextView txtvTitle; + private TextView txtvDescription; + private TextView txtvFundingUrl; + private TextView lblSupport; + private TextView txtvUrl; + private TextView txtvAuthorHeader; + private ImageView imgvBackground; + private View infoContainer; + private View header; + private MaterialToolbar toolbar; + + public static FeedInfoFragment newInstance(Feed feed) { + FeedInfoFragment fragment = new FeedInfoFragment(); + Bundle arguments = new Bundle(); + arguments.putLong(EXTRA_FEED_ID, feed.getId()); + fragment.setArguments(arguments); + return fragment; + } + + private final View.OnClickListener copyUrlToClipboard = new View.OnClickListener() { + @Override + public void onClick(View v) { + if (feed != null && feed.getDownloadUrl() != null) { + String url = feed.getDownloadUrl(); + ClipData clipData = ClipData.newPlainText(url, url); + android.content.ClipboardManager cm = (android.content.ClipboardManager) getContext() + .getSystemService(Context.CLIPBOARD_SERVICE); + cm.setPrimaryClip(clipData); + if (Build.VERSION.SDK_INT <= 32) { + ((MainActivity) getActivity()).showSnackbarAbovePlayer(R.string.copied_to_clipboard, + Snackbar.LENGTH_SHORT); + } + } + } + }; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + View root = inflater.inflate(R.layout.feedinfo, null); + toolbar = root.findViewById(R.id.toolbar); + toolbar.setTitle(""); + toolbar.inflateMenu(R.menu.feedinfo); + toolbar.setNavigationOnClickListener(v -> getParentFragmentManager().popBackStack()); + toolbar.setOnMenuItemClickListener(this); + refreshToolbarState(); + + AppBarLayout appBar = root.findViewById(R.id.appBar); + CollapsingToolbarLayout collapsingToolbar = root.findViewById(R.id.collapsing_toolbar); + ToolbarIconTintManager iconTintManager = new ToolbarIconTintManager(getContext(), toolbar, collapsingToolbar) { + @Override + protected void doTint(Context themedContext) { + toolbar.getMenu().findItem(R.id.visit_website_item) + .setIcon(AppCompatResources.getDrawable(themedContext, R.drawable.ic_web)); + toolbar.getMenu().findItem(R.id.share_item) + .setIcon(AppCompatResources.getDrawable(themedContext, R.drawable.ic_share)); + } + }; + iconTintManager.updateTint(); + appBar.addOnOffsetChangedListener(iconTintManager); + + imgvCover = root.findViewById(R.id.imgvCover); + txtvTitle = root.findViewById(R.id.txtvTitle); + txtvAuthorHeader = root.findViewById(R.id.txtvAuthor); + imgvBackground = root.findViewById(R.id.imgvBackground); + header = root.findViewById(R.id.headerContainer); + infoContainer = root.findViewById(R.id.infoContainer); + root.findViewById(R.id.butShowInfo).setVisibility(View.INVISIBLE); + root.findViewById(R.id.butShowSettings).setVisibility(View.INVISIBLE); + root.findViewById(R.id.butFilter).setVisibility(View.INVISIBLE); + // https://github.com/bumptech/glide/issues/529 + imgvBackground.setColorFilter(new LightingColorFilter(0xff828282, 0x000000)); + + txtvDescription = root.findViewById(R.id.txtvDescription); + txtvUrl = root.findViewById(R.id.txtvUrl); + lblSupport = root.findViewById(R.id.lblSupport); + txtvFundingUrl = root.findViewById(R.id.txtvFundingUrl); + + txtvUrl.setOnClickListener(copyUrlToClipboard); + + long feedId = getArguments().getLong(EXTRA_FEED_ID); + getParentFragmentManager().beginTransaction().replace(R.id.statisticsFragmentContainer, + FeedStatisticsFragment.newInstance(feedId, false), "feed_statistics_fragment") + .commitAllowingStateLoss(); + + root.findViewById(R.id.btnvOpenStatistics).setOnClickListener(view -> { + StatisticsFragment fragment = new StatisticsFragment(); + ((MainActivity) getActivity()).loadChildFragment(fragment, TransitionEffect.SLIDE); + }); + + return root; + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + long feedId = getArguments().getLong(EXTRA_FEED_ID); + disposable = Maybe.create((MaybeOnSubscribe) emitter -> { + Feed feed = DBReader.getFeed(feedId); + if (feed != null) { + emitter.onSuccess(feed); + } else { + emitter.onComplete(); + } + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(result -> { + feed = result; + showFeed(); + }, error -> Log.d(TAG, Log.getStackTraceString(error)), () -> { }); + } + + @Override + public void onConfigurationChanged(@NonNull Configuration newConfig) { + super.onConfigurationChanged(newConfig); + if (header == null || infoContainer == null) { + return; + } + int horizontalSpacing = (int) getResources().getDimension(R.dimen.additional_horizontal_spacing); + header.setPadding(horizontalSpacing, header.getPaddingTop(), horizontalSpacing, header.getPaddingBottom()); + infoContainer.setPadding(horizontalSpacing, infoContainer.getPaddingTop(), + horizontalSpacing, infoContainer.getPaddingBottom()); + } + + private void showFeed() { + Log.d(TAG, "Language is " + feed.getLanguage()); + Log.d(TAG, "Author is " + feed.getAuthor()); + Log.d(TAG, "URL is " + feed.getDownloadUrl()); + Glide.with(this) + .load(feed.getImageUrl()) + .apply(new RequestOptions() + .placeholder(R.color.light_gray) + .error(R.color.light_gray) + .fitCenter() + .dontAnimate()) + .into(imgvCover); + Glide.with(this) + .load(feed.getImageUrl()) + .apply(new RequestOptions() + .placeholder(R.color.image_readability_tint) + .error(R.color.image_readability_tint) + .transform(new FastBlurTransformation()) + .dontAnimate()) + .into(imgvBackground); + + txtvTitle.setText(feed.getTitle()); + txtvTitle.setMaxLines(6); + + String description = HtmlToPlainText.getPlainText(feed.getDescription()); + + txtvDescription.setText(description); + + if (!TextUtils.isEmpty(feed.getAuthor())) { + txtvAuthorHeader.setText(feed.getAuthor()); + } + + txtvUrl.setText(feed.getDownloadUrl()); + txtvUrl.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_paperclip, 0); + + if (feed.getPaymentLinks() == null || feed.getPaymentLinks().size() == 0) { + lblSupport.setVisibility(View.GONE); + txtvFundingUrl.setVisibility(View.GONE); + } else { + lblSupport.setVisibility(View.VISIBLE); + ArrayList fundingList = feed.getPaymentLinks(); + + // Filter for duplicates, but keep items in the order that they have in the feed. + Iterator i = fundingList.iterator(); + while (i.hasNext()) { + FeedFunding funding = i.next(); + for (FeedFunding other : fundingList) { + if (TextUtils.equals(other.url, funding.url)) { + if (other.content != null && funding.content != null + && other.content.length() > funding.content.length()) { + i.remove(); + break; + } + } + } + } + + StringBuilder str = new StringBuilder(); + for (FeedFunding funding : fundingList) { + str.append(funding.content.isEmpty() + ? getContext().getResources().getString(R.string.support_podcast) + : funding.content).append(" ").append(funding.url); + str.append("\n"); + } + str = new StringBuilder(StringUtils.trim(str.toString())); + txtvFundingUrl.setText(str.toString()); + } + + refreshToolbarState(); + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (disposable != null) { + disposable.dispose(); + } + } + + private void refreshToolbarState() { + toolbar.getMenu().findItem(R.id.reconnect_local_folder).setVisible(feed != null && feed.isLocalFeed()); + toolbar.getMenu().findItem(R.id.share_item).setVisible(feed != null && !feed.isLocalFeed()); + toolbar.getMenu().findItem(R.id.visit_website_item).setVisible(feed != null && feed.getLink() != null + && IntentUtils.isCallable(getContext(), new Intent(Intent.ACTION_VIEW, Uri.parse(feed.getLink())))); + toolbar.getMenu().findItem(R.id.edit_feed_url_item).setVisible(feed != null && !feed.isLocalFeed()); + } + + @Override + public boolean onMenuItemClick(MenuItem item) { + if (feed == null) { + ((MainActivity) getActivity()).showSnackbarAbovePlayer( + R.string.please_wait_for_data, Toast.LENGTH_LONG); + return false; + } + if (item.getItemId() == R.id.visit_website_item) { + IntentUtils.openInBrowser(getContext(), feed.getLink()); + } else if (item.getItemId() == R.id.share_item) { + ShareUtils.shareFeedLink(getContext(), feed); + } else if (item.getItemId() == R.id.reconnect_local_folder) { + MaterialAlertDialogBuilder alert = new MaterialAlertDialogBuilder(getContext()); + alert.setMessage(R.string.reconnect_local_folder_warning); + alert.setPositiveButton(android.R.string.ok, (dialog, which) -> { + try { + addLocalFolderLauncher.launch(null); + } catch (ActivityNotFoundException e) { + Log.e(TAG, "No activity found. Should never happen..."); + } + }); + alert.setNegativeButton(android.R.string.cancel, null); + alert.show(); + } else if (item.getItemId() == R.id.edit_feed_url_item) { + new EditUrlSettingsDialog(getActivity(), feed) { + @Override + protected void setUrl(String url) { + feed.setDownloadUrl(url); + txtvUrl.setText(feed.getDownloadUrl()); + txtvUrl.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_paperclip, 0); + } + }.show(); + } else { + return false; + } + return true; + } + + private void addLocalFolderResult(final Uri uri) { + if (uri == null) { + return; + } + reconnectLocalFolder(uri); + } + + private void reconnectLocalFolder(Uri uri) { + if (feed == null) { + return; + } + + Completable.fromAction(() -> { + getActivity().getContentResolver() + .takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION); + DocumentFile documentFile = DocumentFile.fromTreeUri(getContext(), uri); + if (documentFile == null) { + throw new IllegalArgumentException("Unable to retrieve document tree"); + } + feed.setDownloadUrl(Feed.PREFIX_LOCAL_FOLDER + uri.toString()); + FeedDatabaseWriter.updateFeed(getContext(), feed, true); + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + () -> ((MainActivity) getActivity()) + .showSnackbarAbovePlayer(android.R.string.ok, Snackbar.LENGTH_SHORT), + error -> ((MainActivity) getActivity()) + .showSnackbarAbovePlayer(error.getLocalizedMessage(), Snackbar.LENGTH_LONG)); + } + + private static class AddLocalFolder extends ActivityResultContracts.OpenDocumentTree { + @NonNull + @Override + public Intent createIntent(@NonNull final Context context, @Nullable final Uri input) { + return super.createIntent(context, input) + .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/screen/feed/FeedItemFilterGroup.java b/app/src/main/java/de/danoeh/antennapod/ui/screen/feed/FeedItemFilterGroup.java new file mode 100644 index 000000000..5cb0f36fd --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/screen/feed/FeedItemFilterGroup.java @@ -0,0 +1,37 @@ +package de.danoeh.antennapod.ui.screen.feed; + +import de.danoeh.antennapod.core.R; +import de.danoeh.antennapod.model.feed.FeedItemFilter; + +public enum FeedItemFilterGroup { + PLAYED(new ItemProperties(R.string.hide_played_episodes_label, FeedItemFilter.PLAYED), + new ItemProperties(R.string.not_played, FeedItemFilter.UNPLAYED)), + PAUSED(new ItemProperties(R.string.hide_paused_episodes_label, FeedItemFilter.PAUSED), + new ItemProperties(R.string.not_paused, FeedItemFilter.NOT_PAUSED)), + FAVORITE(new ItemProperties(R.string.hide_is_favorite_label, FeedItemFilter.IS_FAVORITE), + new ItemProperties(R.string.not_favorite, FeedItemFilter.NOT_FAVORITE)), + MEDIA(new ItemProperties(R.string.has_media, FeedItemFilter.HAS_MEDIA), + new ItemProperties(R.string.no_media, FeedItemFilter.NO_MEDIA)), + QUEUED(new ItemProperties(R.string.queued_label, FeedItemFilter.QUEUED), + new ItemProperties(R.string.not_queued_label, FeedItemFilter.NOT_QUEUED)), + DOWNLOADED(new ItemProperties(R.string.hide_downloaded_episodes_label, FeedItemFilter.DOWNLOADED), + new ItemProperties(R.string.hide_not_downloaded_episodes_label, FeedItemFilter.NOT_DOWNLOADED)); + + public final ItemProperties[] values; + + FeedItemFilterGroup(ItemProperties... values) { + this.values = values; + } + + public static class ItemProperties { + + public final int displayName; + public final String filterId; + + public ItemProperties(int displayName, String filterId) { + this.displayName = displayName; + this.filterId = filterId; + } + + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/screen/feed/FeedItemlistFragment.java b/app/src/main/java/de/danoeh/antennapod/ui/screen/feed/FeedItemlistFragment.java new file mode 100644 index 000000000..7743f1cd9 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/screen/feed/FeedItemlistFragment.java @@ -0,0 +1,654 @@ +package de.danoeh.antennapod.ui.screen.feed; + +import android.content.Context; +import android.content.res.Configuration; +import android.graphics.LightingColorFilter; +import android.os.Bundle; +import android.util.Log; +import android.view.ContextMenu; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.content.res.AppCompatResources; +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.RecyclerView; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.request.RequestOptions; +import com.google.android.material.appbar.MaterialToolbar; +import com.google.android.material.snackbar.Snackbar; +import com.leinardi.android.speeddial.SpeedDialView; + +import de.danoeh.antennapod.ui.screen.episode.ItemPagerFragment; +import de.danoeh.antennapod.ui.screen.SearchFragment; +import de.danoeh.antennapod.ui.TransitionEffect; +import de.danoeh.antennapod.net.download.serviceinterface.FeedUpdateManager; +import de.danoeh.antennapod.ui.screen.download.DownloadLogFragment; +import de.danoeh.antennapod.ui.screen.feed.preferences.FeedSettingsFragment; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.greenrobot.eventbus.EventBus; +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; + +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ExecutionException; + +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.ui.episodeslist.EpisodeItemListAdapter; +import de.danoeh.antennapod.event.FeedEvent; +import de.danoeh.antennapod.ui.MenuItemUtils; +import de.danoeh.antennapod.storage.database.DBReader; +import de.danoeh.antennapod.storage.database.DBWriter; +import de.danoeh.antennapod.storage.database.FeedItemPermutors; +import de.danoeh.antennapod.core.util.FeedItemUtil; +import de.danoeh.antennapod.core.util.IntentUtils; +import de.danoeh.antennapod.ui.share.ShareUtils; +import de.danoeh.antennapod.ui.episodeslist.MoreContentListFooterUtil; +import de.danoeh.antennapod.databinding.FeedItemListFragmentBinding; +import de.danoeh.antennapod.databinding.MultiSelectSpeedDialBinding; +import de.danoeh.antennapod.ui.screen.download.DownloadLogDetailsDialog; +import de.danoeh.antennapod.ui.FeedItemFilterDialog; +import de.danoeh.antennapod.event.EpisodeDownloadEvent; +import de.danoeh.antennapod.event.FavoritesEvent; +import de.danoeh.antennapod.event.FeedItemEvent; +import de.danoeh.antennapod.event.FeedListUpdateEvent; +import de.danoeh.antennapod.event.FeedUpdateRunningEvent; +import de.danoeh.antennapod.event.PlayerStatusEvent; +import de.danoeh.antennapod.event.QueueEvent; +import de.danoeh.antennapod.event.UnreadItemsUpdateEvent; +import de.danoeh.antennapod.event.playback.PlaybackPositionEvent; +import de.danoeh.antennapod.ui.episodeslist.EpisodeMultiSelectActionHandler; +import de.danoeh.antennapod.ui.swipeactions.SwipeActions; +import de.danoeh.antennapod.ui.episodeslist.FeedItemMenuHandler; +import de.danoeh.antennapod.model.download.DownloadResult; +import de.danoeh.antennapod.model.feed.Feed; +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedItemFilter; +import de.danoeh.antennapod.model.feed.SortOrder; +import de.danoeh.antennapod.storage.preferences.UserPreferences; +import de.danoeh.antennapod.ui.glide.FastBlurTransformation; +import de.danoeh.antennapod.ui.episodeslist.EpisodeItemViewHolder; +import io.reactivex.Maybe; +import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; +import io.reactivex.schedulers.Schedulers; + +/** + * Displays a list of FeedItems. + */ +public class FeedItemlistFragment extends Fragment implements AdapterView.OnItemClickListener, + MaterialToolbar.OnMenuItemClickListener, EpisodeItemListAdapter.OnSelectModeListener { + public static final String TAG = "ItemlistFragment"; + private static final String ARGUMENT_FEED_ID = "argument.de.danoeh.antennapod.feed_id"; + private static final String KEY_UP_ARROW = "up_arrow"; + + private FeedItemListAdapter adapter; + private SwipeActions swipeActions; + private MoreContentListFooterUtil nextPageLoader; + private boolean displayUpArrow; + private long feedID; + private Feed feed; + private boolean headerCreated = false; + private Disposable disposable; + private FeedItemListFragmentBinding viewBinding; + private MultiSelectSpeedDialBinding speedDialBinding; + + /** + * Creates new ItemlistFragment which shows the Feeditems of a specific + * feed. Sets 'showFeedtitle' to false + * + * @param feedId The id of the feed to show + * @return the newly created instance of an ItemlistFragment + */ + public static FeedItemlistFragment newInstance(long feedId) { + FeedItemlistFragment i = new FeedItemlistFragment(); + Bundle b = new Bundle(); + b.putLong(ARGUMENT_FEED_ID, feedId); + i.setArguments(b); + return i; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + Bundle args = getArguments(); + Validate.notNull(args); + feedID = args.getLong(ARGUMENT_FEED_ID); + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + viewBinding = FeedItemListFragmentBinding.inflate(inflater); + speedDialBinding = MultiSelectSpeedDialBinding.bind(viewBinding.getRoot()); + viewBinding.toolbar.inflateMenu(R.menu.feedlist); + viewBinding.toolbar.setOnMenuItemClickListener(this); + viewBinding.toolbar.setOnLongClickListener(v -> { + viewBinding.recyclerView.scrollToPosition(5); + viewBinding.recyclerView.post(() -> viewBinding.recyclerView.smoothScrollToPosition(0)); + viewBinding.appBar.setExpanded(true); + return false; + }); + displayUpArrow = getParentFragmentManager().getBackStackEntryCount() != 0; + if (savedInstanceState != null) { + displayUpArrow = savedInstanceState.getBoolean(KEY_UP_ARROW); + } + ((MainActivity) getActivity()).setupToolbarToggle(viewBinding.toolbar, displayUpArrow); + updateToolbar(); + + viewBinding.recyclerView.setRecycledViewPool(((MainActivity) getActivity()).getRecycledViewPool()); + adapter = new FeedItemListAdapter((MainActivity) getActivity()); + adapter.setOnSelectModeListener(this); + viewBinding.recyclerView.setAdapter(adapter); + swipeActions = new SwipeActions(this, TAG).attachTo(viewBinding.recyclerView); + viewBinding.progressBar.setVisibility(View.VISIBLE); + + ToolbarIconTintManager iconTintManager = new ToolbarIconTintManager( + getContext(), viewBinding.toolbar, viewBinding.collapsingToolbar) { + @Override + protected void doTint(Context themedContext) { + viewBinding.toolbar.getMenu().findItem(R.id.refresh_item) + .setIcon(AppCompatResources.getDrawable(themedContext, R.drawable.ic_refresh)); + viewBinding.toolbar.getMenu().findItem(R.id.action_search) + .setIcon(AppCompatResources.getDrawable(themedContext, R.drawable.ic_search)); + } + }; + iconTintManager.updateTint(); + viewBinding.appBar.addOnOffsetChangedListener(iconTintManager); + + nextPageLoader = new MoreContentListFooterUtil(viewBinding.moreContent.moreContentListFooter); + nextPageLoader.setClickListener(() -> { + if (feed != null) { + FeedUpdateManager.getInstance().runOnce(getContext(), feed, true); + } + }); + viewBinding.recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { + @Override + public void onScrolled(@NonNull RecyclerView view, int deltaX, int deltaY) { + super.onScrolled(view, deltaX, deltaY); + boolean hasMorePages = feed != null && feed.isPaged() && feed.getNextPageLink() != null; + boolean pageLoaderVisible = viewBinding.recyclerView.isScrolledToBottom() && hasMorePages; + nextPageLoader.getRoot().setVisibility(pageLoaderVisible ? View.VISIBLE : View.GONE); + viewBinding.recyclerView.setPadding( + viewBinding.recyclerView.getPaddingLeft(), 0, viewBinding.recyclerView.getPaddingRight(), + pageLoaderVisible ? nextPageLoader.getRoot().getMeasuredHeight() : 0); + } + }); + + EventBus.getDefault().register(this); + + viewBinding.swipeRefresh.setDistanceToTriggerSync(getResources().getInteger(R.integer.swipe_refresh_distance)); + viewBinding.swipeRefresh.setOnRefreshListener(() -> + FeedUpdateManager.getInstance().runOnceOrAsk(requireContext(), feed)); + + loadItems(); + + // Init action UI (via a FAB Speed Dial) + speedDialBinding.fabSD.setOverlayLayout(speedDialBinding.fabSDOverlay); + speedDialBinding.fabSD.inflate(R.menu.episodes_apply_action_speeddial); + speedDialBinding.fabSD.setOnChangeListener(new SpeedDialView.OnChangeListener() { + @Override + public boolean onMainActionSelected() { + return false; + } + + @Override + public void onToggleChanged(boolean open) { + if (open && adapter.getSelectedCount() == 0) { + ((MainActivity) getActivity()).showSnackbarAbovePlayer(R.string.no_items_selected, + Snackbar.LENGTH_SHORT); + speedDialBinding.fabSD.close(); + } + } + }); + speedDialBinding.fabSD.setOnActionSelectedListener(actionItem -> { + new EpisodeMultiSelectActionHandler(((MainActivity) getActivity()), actionItem.getId()) + .handleAction(adapter.getSelectedItems()); + adapter.endSelectMode(); + return true; + }); + return viewBinding.getRoot(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + + EventBus.getDefault().unregister(this); + if (disposable != null) { + disposable.dispose(); + } + adapter.endSelectMode(); + } + + @Override + public void onSaveInstanceState(@NonNull Bundle outState) { + outState.putBoolean(KEY_UP_ARROW, displayUpArrow); + super.onSaveInstanceState(outState); + } + + private void updateToolbar() { + if (feed == null) { + return; + } + viewBinding.toolbar.getMenu().findItem(R.id.visit_website_item).setVisible(feed.getLink() != null); + viewBinding.toolbar.getMenu().findItem(R.id.refresh_complete_item).setVisible(feed.isPaged()); + if (StringUtils.isBlank(feed.getLink())) { + viewBinding.toolbar.getMenu().findItem(R.id.visit_website_item).setVisible(false); + } + if (feed.isLocalFeed()) { + viewBinding.toolbar.getMenu().findItem(R.id.share_item).setVisible(false); + } + } + + @Override + public void onConfigurationChanged(@NonNull Configuration newConfig) { + super.onConfigurationChanged(newConfig); + int horizontalSpacing = (int) getResources().getDimension(R.dimen.additional_horizontal_spacing); + viewBinding.header.headerContainer.setPadding( + horizontalSpacing, viewBinding.header.headerContainer.getPaddingTop(), + horizontalSpacing, viewBinding.header.headerContainer.getPaddingBottom()); + } + + @Override + public boolean onMenuItemClick(MenuItem item) { + if (feed == null) { + ((MainActivity) getActivity()).showSnackbarAbovePlayer( + R.string.please_wait_for_data, Toast.LENGTH_LONG); + return true; + } + if (item.getItemId() == R.id.visit_website_item) { + IntentUtils.openInBrowser(getContext(), feed.getLink()); + } else if (item.getItemId() == R.id.share_item) { + ShareUtils.shareFeedLink(getContext(), feed); + } else if (item.getItemId() == R.id.refresh_item) { + FeedUpdateManager.getInstance().runOnceOrAsk(getContext(), feed); + } else if (item.getItemId() == R.id.refresh_complete_item) { + new Thread(() -> { + feed.setNextPageLink(feed.getDownloadUrl()); + feed.setPageNr(0); + try { + DBWriter.resetPagedFeedPage(feed).get(); + FeedUpdateManager.getInstance().runOnce(getContext(), feed); + } catch (ExecutionException | InterruptedException e) { + throw new RuntimeException(e); + } + }).start(); + } else if (item.getItemId() == R.id.sort_items) { + SingleFeedSortDialog.newInstance(feed).show(getChildFragmentManager(), "SortDialog"); + } else if (item.getItemId() == R.id.rename_item) { + new RenameFeedDialog(getActivity(), feed).show(); + } else if (item.getItemId() == R.id.remove_feed) { + RemoveFeedDialog.show(getContext(), feed, () -> { + ((MainActivity) getActivity()).loadFragment(UserPreferences.getDefaultPage(), null); + // Make sure fragment is hidden before actually starting to delete + getActivity().getSupportFragmentManager().executePendingTransactions(); + }); + } else if (item.getItemId() == R.id.action_search) { + ((MainActivity) getActivity()).loadChildFragment(SearchFragment.newInstance(feed.getId(), feed.getTitle())); + } else { + return false; + } + return true; + } + + @Override + public boolean onContextItemSelected(@NonNull MenuItem item) { + FeedItem selectedItem = adapter.getLongPressedItem(); + if (selectedItem == null) { + Log.i(TAG, "Selected item at current position was null, ignoring selection"); + return super.onContextItemSelected(item); + } + if (adapter.onContextItemSelected(item)) { + return true; + } + return FeedItemMenuHandler.onMenuItemClicked(this, item.getItemId(), selectedItem); + } + + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + MainActivity activity = (MainActivity) getActivity(); + long[] ids = FeedItemUtil.getIds(feed.getItems()); + activity.loadChildFragment(ItemPagerFragment.newInstance(ids, position)); + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onEvent(FeedEvent event) { + Log.d(TAG, "onEvent() called with: " + "event = [" + event + "]"); + if (event.feedId == feedID) { + loadItems(); + } + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onEventMainThread(FeedItemEvent event) { + Log.d(TAG, "onEventMainThread() called with: " + "event = [" + event + "]"); + if (feed == null || feed.getItems() == null) { + return; + } + for (int i = 0, size = event.items.size(); i < size; i++) { + FeedItem item = event.items.get(i); + int pos = FeedItemUtil.indexOfItemWithId(feed.getItems(), item.getId()); + if (pos >= 0) { + feed.getItems().remove(pos); + feed.getItems().add(pos, item); + adapter.notifyItemChangedCompat(pos); + } + } + } + + @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) + public void onEventMainThread(EpisodeDownloadEvent event) { + if (feed == null) { + return; + } + for (String downloadUrl : event.getUrls()) { + int pos = FeedItemUtil.indexOfItemWithDownloadUrl(feed.getItems(), downloadUrl); + if (pos >= 0) { + adapter.notifyItemChangedCompat(pos); + } + } + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onEventMainThread(PlaybackPositionEvent event) { + for (int i = 0; i < adapter.getItemCount(); i++) { + EpisodeItemViewHolder holder = (EpisodeItemViewHolder) + viewBinding.recyclerView.findViewHolderForAdapterPosition(i); + if (holder != null && holder.isCurrentlyPlayingItem()) { + holder.notifyPlaybackPositionUpdated(event); + break; + } + } + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void favoritesChanged(FavoritesEvent event) { + updateUi(); + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onQueueChanged(QueueEvent event) { + updateUi(); + } + + @Override + public void onStartSelectMode() { + swipeActions.detach(); + if (feed.isLocalFeed()) { + speedDialBinding.fabSD.removeActionItemById(R.id.download_batch); + } + speedDialBinding.fabSD.removeActionItemById(R.id.remove_all_inbox_item); + speedDialBinding.fabSD.setVisibility(View.VISIBLE); + updateToolbar(); + } + + @Override + public void onEndSelectMode() { + speedDialBinding.fabSD.close(); + speedDialBinding.fabSD.setVisibility(View.GONE); + swipeActions.attachTo(viewBinding.recyclerView); + } + + private void updateUi() { + loadItems(); + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onPlayerStatusChanged(PlayerStatusEvent event) { + updateUi(); + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onUnreadItemsChanged(UnreadItemsUpdateEvent event) { + updateUi(); + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onFeedListChanged(FeedListUpdateEvent event) { + if (feed != null && event.contains(feed)) { + updateUi(); + } + } + + @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) + public void onEventMainThread(FeedUpdateRunningEvent event) { + nextPageLoader.setLoadingState(event.isFeedUpdateRunning); + if (!event.isFeedUpdateRunning) { + nextPageLoader.getRoot().setVisibility(View.GONE); + } + viewBinding.swipeRefresh.setRefreshing(event.isFeedUpdateRunning); + } + + private void refreshHeaderView() { + setupHeaderView(); + if (viewBinding == null || feed == null) { + Log.e(TAG, "Unable to refresh header view"); + return; + } + loadFeedImage(); + if (feed.hasLastUpdateFailed()) { + viewBinding.header.txtvFailure.setVisibility(View.VISIBLE); + } else { + viewBinding.header.txtvFailure.setVisibility(View.GONE); + } + if (!feed.getPreferences().getKeepUpdated()) { + viewBinding.header.txtvUpdatesDisabled.setText(R.string.updates_disabled_label); + viewBinding.header.txtvUpdatesDisabled.setVisibility(View.VISIBLE); + } else { + viewBinding.header.txtvUpdatesDisabled.setVisibility(View.GONE); + } + viewBinding.header.txtvTitle.setText(feed.getTitle()); + viewBinding.header.txtvAuthor.setText(feed.getAuthor()); + if (feed.getItemFilter() != null) { + FeedItemFilter filter = feed.getItemFilter(); + if (filter.getValues().length > 0) { + viewBinding.header.txtvInformation.setText(R.string.filtered_label); + viewBinding.header.txtvInformation.setOnClickListener(l -> + FeedItemFilterDialog.newInstance(feed).show(getChildFragmentManager(), null)); + viewBinding.header.txtvInformation.setVisibility(View.VISIBLE); + } else { + viewBinding.header.txtvInformation.setVisibility(View.GONE); + } + } else { + viewBinding.header.txtvInformation.setVisibility(View.GONE); + } + } + + private void setupHeaderView() { + if (feed == null || headerCreated) { + return; + } + + // https://github.com/bumptech/glide/issues/529 + viewBinding.imgvBackground.setColorFilter(new LightingColorFilter(0xff666666, 0x000000)); + viewBinding.header.butShowInfo.setOnClickListener(v -> showFeedInfo()); + viewBinding.header.imgvCover.setOnClickListener(v -> showFeedInfo()); + viewBinding.header.butShowSettings.setOnClickListener(v -> { + if (feed != null) { + FeedSettingsFragment fragment = FeedSettingsFragment.newInstance(feed); + ((MainActivity) getActivity()).loadChildFragment(fragment, TransitionEffect.SLIDE); + } + }); + viewBinding.header.butFilter.setOnClickListener(v -> + FeedItemFilterDialog.newInstance(feed).show(getChildFragmentManager(), null)); + viewBinding.header.txtvFailure.setOnClickListener(v -> showErrorDetails()); + headerCreated = true; + } + + private void showErrorDetails() { + Maybe.fromCallable( + () -> { + List feedDownloadLog = DBReader.getFeedDownloadLog(feedID); + if (feedDownloadLog.size() == 0 || feedDownloadLog.get(0).isSuccessful()) { + return null; + } + return feedDownloadLog.get(0); + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + downloadStatus -> new DownloadLogDetailsDialog(getContext(), downloadStatus).show(), + error -> error.printStackTrace(), + () -> new DownloadLogFragment().show(getChildFragmentManager(), null)); + } + + private void showFeedInfo() { + if (feed != null) { + FeedInfoFragment fragment = FeedInfoFragment.newInstance(feed); + ((MainActivity) getActivity()).loadChildFragment(fragment, TransitionEffect.SLIDE); + } + } + + private void loadFeedImage() { + Glide.with(this) + .load(feed.getImageUrl()) + .apply(new RequestOptions() + .placeholder(R.color.image_readability_tint) + .error(R.color.image_readability_tint) + .transform(new FastBlurTransformation()) + .dontAnimate()) + .into(viewBinding.imgvBackground); + + Glide.with(this) + .load(feed.getImageUrl()) + .apply(new RequestOptions() + .placeholder(R.color.light_gray) + .error(R.color.light_gray) + .fitCenter() + .dontAnimate()) + .into(viewBinding.header.imgvCover); + } + + private void loadItems() { + if (disposable != null) { + disposable.dispose(); + } + disposable = Observable.fromCallable(this::loadData) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + result -> { + feed = result; + swipeActions.setFilter(feed.getItemFilter()); + refreshHeaderView(); + viewBinding.progressBar.setVisibility(View.GONE); + adapter.setDummyViews(0); + adapter.updateItems(feed.getItems()); + updateToolbar(); + }, error -> { + feed = null; + refreshHeaderView(); + adapter.setDummyViews(0); + adapter.updateItems(Collections.emptyList()); + updateToolbar(); + Log.e(TAG, Log.getStackTraceString(error)); + }); + } + + @Nullable + private Feed loadData() { + Feed feed = DBReader.getFeed(feedID, true); + if (feed == null) { + return null; + } + DBReader.loadAdditionalFeedItemListData(feed.getItems()); + if (feed.getSortOrder() != null) { + List feedItems = feed.getItems(); + FeedItemPermutors.getPermutor(feed.getSortOrder()).reorder(feedItems); + feed.setItems(feedItems); + } + return feed; + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onKeyUp(KeyEvent event) { + if (!isAdded() || !isVisible() || !isMenuVisible()) { + return; + } + switch (event.getKeyCode()) { + case KeyEvent.KEYCODE_T: + viewBinding.recyclerView.smoothScrollToPosition(0); + break; + case KeyEvent.KEYCODE_B: + viewBinding.recyclerView.smoothScrollToPosition(adapter.getItemCount() - 1); + break; + default: + break; + } + } + + private class FeedItemListAdapter extends EpisodeItemListAdapter { + public FeedItemListAdapter(MainActivity mainActivity) { + super(mainActivity); + } + + @Override + protected void beforeBindViewHolder(EpisodeItemViewHolder holder, int pos) { + holder.coverHolder.setVisibility(View.GONE); + } + + @Override + public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { + super.onCreateContextMenu(menu, v, menuInfo); + if (!inActionMode()) { + menu.findItem(R.id.multi_select).setVisible(true); + } + MenuItemUtils.setOnClickListeners(menu, FeedItemlistFragment.this::onContextItemSelected); + } + } + + public static class SingleFeedSortDialog extends ItemSortDialog { + private static final String ARG_FEED_ID = "feedId"; + private static final String ARG_FEED_IS_LOCAL = "isLocal"; + private static final String ARG_SORT_ORDER = "sortOrder"; + + private static SingleFeedSortDialog newInstance(Feed feed) { + Bundle bundle = new Bundle(); + bundle.putLong(ARG_FEED_ID, feed.getId()); + bundle.putBoolean(ARG_FEED_IS_LOCAL, feed.isLocalFeed()); + if (feed.getSortOrder() == null) { + bundle.putString(ARG_SORT_ORDER, String.valueOf(SortOrder.DATE_NEW_OLD.code)); + } else { + bundle.putString(ARG_SORT_ORDER, String.valueOf(feed.getSortOrder().code)); + } + SingleFeedSortDialog dialog = new SingleFeedSortDialog(); + dialog.setArguments(bundle); + return dialog; + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + sortOrder = SortOrder.fromCodeString(getArguments().getString(ARG_SORT_ORDER)); + } + + @Override + protected void onAddItem(int title, SortOrder ascending, SortOrder descending, boolean ascendingIsDefault) { + if (ascending == SortOrder.DATE_OLD_NEW || ascending == SortOrder.DURATION_SHORT_LONG + || ascending == SortOrder.EPISODE_TITLE_A_Z + || (getArguments().getBoolean(ARG_FEED_IS_LOCAL) && ascending == SortOrder.EPISODE_FILENAME_A_Z)) { + super.onAddItem(title, ascending, descending, ascendingIsDefault); + } + } + + @Override + protected void onSelectionChanged() { + super.onSelectionChanged(); + DBWriter.setFeedItemSortOrder(getArguments().getLong(ARG_FEED_ID), sortOrder); + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/screen/feed/ItemFilterDialog.java b/app/src/main/java/de/danoeh/antennapod/ui/screen/feed/ItemFilterDialog.java new file mode 100644 index 000000000..8dbcce699 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/screen/feed/ItemFilterDialog.java @@ -0,0 +1,122 @@ +package de.danoeh.antennapod.ui.screen.feed; + +import android.app.Dialog; +import android.os.Bundle; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.FrameLayout; +import android.widget.LinearLayout; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.google.android.material.bottomsheet.BottomSheetBehavior; +import com.google.android.material.bottomsheet.BottomSheetDialog; +import com.google.android.material.bottomsheet.BottomSheetDialogFragment; +import com.google.android.material.button.MaterialButtonToggleGroup; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.databinding.FilterDialogBinding; +import de.danoeh.antennapod.databinding.FilterDialogRowBinding; +import de.danoeh.antennapod.model.feed.FeedItemFilter; + +public abstract class ItemFilterDialog extends BottomSheetDialogFragment { + protected static final String ARGUMENT_FILTER = "filter"; + + private LinearLayout rows; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + View layout = inflater.inflate(R.layout.filter_dialog, null, false); + FilterDialogBinding binding = FilterDialogBinding.bind(layout); + rows = binding.filterRows; + FeedItemFilter filter = (FeedItemFilter) getArguments().getSerializable(ARGUMENT_FILTER); + + //add filter rows + for (FeedItemFilterGroup item : FeedItemFilterGroup.values()) { + FilterDialogRowBinding rowBinding = FilterDialogRowBinding.inflate(inflater); + rowBinding.getRoot().addOnButtonCheckedListener( + (group, checkedId, isChecked) -> onFilterChanged(getNewFilterValues())); + rowBinding.filterButton1.setText(item.values[0].displayName); + rowBinding.filterButton1.setTag(item.values[0].filterId); + rowBinding.filterButton2.setText(item.values[1].displayName); + rowBinding.filterButton2.setTag(item.values[1].filterId); + rowBinding.filterButton1.setMaxLines(3); + rowBinding.filterButton1.setSingleLine(false); + rowBinding.filterButton2.setMaxLines(3); + rowBinding.filterButton2.setSingleLine(false); + rows.addView(rowBinding.getRoot(), rows.getChildCount() - 1); + } + + binding.confirmFiltermenu.setOnClickListener(view1 -> dismiss()); + binding.resetFiltermenu.setOnClickListener(view1 -> { + onFilterChanged(Collections.emptySet()); + for (int i = 0; i < rows.getChildCount(); i++) { + if (rows.getChildAt(i) instanceof MaterialButtonToggleGroup) { + ((MaterialButtonToggleGroup) rows.getChildAt(i)).clearChecked(); + } + } + }); + + for (String filterId : filter.getValues()) { + if (!TextUtils.isEmpty(filterId)) { + Button button = layout.findViewWithTag(filterId); + if (button != null) { + ((MaterialButtonToggleGroup) button.getParent()).check(button.getId()); + } + } + } + return layout; + } + + @NonNull + @Override + public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { + Dialog dialog = super.onCreateDialog(savedInstanceState); + dialog.setOnShowListener(dialogInterface -> { + BottomSheetDialog bottomSheetDialog = (BottomSheetDialog) dialogInterface; + setupFullHeight(bottomSheetDialog); + }); + return dialog; + } + + private void setupFullHeight(BottomSheetDialog bottomSheetDialog) { + FrameLayout bottomSheet = (FrameLayout) bottomSheetDialog.findViewById(R.id.design_bottom_sheet); + if (bottomSheet != null) { + BottomSheetBehavior behavior = BottomSheetBehavior.from(bottomSheet); + ViewGroup.LayoutParams layoutParams = bottomSheet.getLayoutParams(); + bottomSheet.setLayoutParams(layoutParams); + behavior.setState(BottomSheetBehavior.STATE_EXPANDED); + } + } + + protected Set getNewFilterValues() { + final Set newFilterValues = new HashSet<>(); + for (int i = 0; i < rows.getChildCount(); i++) { + if (!(rows.getChildAt(i) instanceof MaterialButtonToggleGroup)) { + continue; + } + MaterialButtonToggleGroup group = (MaterialButtonToggleGroup) rows.getChildAt(i); + if (group.getCheckedButtonId() == View.NO_ID) { + continue; + } + String tag = (String) group.findViewById(group.getCheckedButtonId()).getTag(); + if (tag == null) { // Clear buttons use no tag + continue; + } + newFilterValues.add(tag); + } + return newFilterValues; + } + + public abstract void onFilterChanged(Set newFilterValues); +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/screen/feed/ItemSortDialog.java b/app/src/main/java/de/danoeh/antennapod/ui/screen/feed/ItemSortDialog.java new file mode 100644 index 000000000..bb68477df --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/screen/feed/ItemSortDialog.java @@ -0,0 +1,104 @@ +package de.danoeh.antennapod.ui.screen.feed; + +import android.app.Dialog; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.android.material.bottomsheet.BottomSheetBehavior; +import com.google.android.material.bottomsheet.BottomSheetDialog; +import com.google.android.material.bottomsheet.BottomSheetDialogFragment; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.databinding.SortDialogBinding; +import de.danoeh.antennapod.databinding.SortDialogItemActiveBinding; +import de.danoeh.antennapod.databinding.SortDialogItemBinding; +import de.danoeh.antennapod.model.feed.SortOrder; + +public class ItemSortDialog extends BottomSheetDialogFragment { + protected SortOrder sortOrder; + protected SortDialogBinding viewBinding; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + viewBinding = SortDialogBinding.inflate(inflater); + populateList(); + viewBinding.keepSortedCheckbox.setOnCheckedChangeListener( + (buttonView, isChecked) -> ItemSortDialog.this.onSelectionChanged()); + return viewBinding.getRoot(); + } + + private void populateList() { + viewBinding.gridLayout.removeAllViews(); + onAddItem(R.string.episode_title, SortOrder.EPISODE_TITLE_A_Z, SortOrder.EPISODE_TITLE_Z_A, true); + onAddItem(R.string.feed_title, SortOrder.FEED_TITLE_A_Z, SortOrder.FEED_TITLE_Z_A, true); + onAddItem(R.string.duration, SortOrder.DURATION_SHORT_LONG, SortOrder.DURATION_LONG_SHORT, true); + onAddItem(R.string.date, SortOrder.DATE_OLD_NEW, SortOrder.DATE_NEW_OLD, false); + onAddItem(R.string.size, SortOrder.SIZE_SMALL_LARGE, SortOrder.SIZE_LARGE_SMALL, false); + onAddItem(R.string.filename, SortOrder.EPISODE_FILENAME_A_Z, SortOrder.EPISODE_FILENAME_Z_A, true); + onAddItem(R.string.random, SortOrder.RANDOM, SortOrder.RANDOM, true); + onAddItem(R.string.smart_shuffle, SortOrder.SMART_SHUFFLE_OLD_NEW, SortOrder.SMART_SHUFFLE_NEW_OLD, false); + } + + protected void onAddItem(int title, SortOrder ascending, SortOrder descending, boolean ascendingIsDefault) { + if (sortOrder == ascending || sortOrder == descending) { + SortDialogItemActiveBinding item = SortDialogItemActiveBinding.inflate( + getLayoutInflater(), viewBinding.gridLayout, false); + SortOrder other; + if (ascending == descending) { + item.button.setText(title); + other = ascending; + } else if (sortOrder == ascending) { + item.button.setText(getString(title) + "\u00A0▲"); + other = descending; + } else { + item.button.setText(getString(title) + "\u00A0▼"); + other = ascending; + } + item.button.setOnClickListener(v -> { + sortOrder = other; + populateList(); + onSelectionChanged(); + }); + viewBinding.gridLayout.addView(item.getRoot()); + } else { + SortDialogItemBinding item = SortDialogItemBinding.inflate( + getLayoutInflater(), viewBinding.gridLayout, false); + item.button.setText(title); + item.button.setOnClickListener(v -> { + sortOrder = ascendingIsDefault ? ascending : descending; + populateList(); + onSelectionChanged(); + }); + viewBinding.gridLayout.addView(item.getRoot()); + } + } + + protected void onSelectionChanged() { + } + + @NonNull + @Override + public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { + Dialog dialog = super.onCreateDialog(savedInstanceState); + dialog.setOnShowListener(dialogInterface -> { + BottomSheetDialog bottomSheetDialog = (BottomSheetDialog) dialogInterface; + setupFullHeight(bottomSheetDialog); + }); + return dialog; + } + + private void setupFullHeight(BottomSheetDialog bottomSheetDialog) { + FrameLayout bottomSheet = bottomSheetDialog.findViewById(R.id.design_bottom_sheet); + if (bottomSheet != null) { + BottomSheetBehavior behavior = BottomSheetBehavior.from(bottomSheet); + ViewGroup.LayoutParams layoutParams = bottomSheet.getLayoutParams(); + bottomSheet.setLayoutParams(layoutParams); + behavior.setState(BottomSheetBehavior.STATE_EXPANDED); + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/screen/feed/RemoveFeedDialog.java b/app/src/main/java/de/danoeh/antennapod/ui/screen/feed/RemoveFeedDialog.java new file mode 100644 index 000000000..415948a81 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/screen/feed/RemoveFeedDialog.java @@ -0,0 +1,84 @@ +package de.danoeh.antennapod.ui.screen.feed; + +import android.app.ProgressDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.util.Log; + +import androidx.annotation.Nullable; + +import java.util.Collections; +import java.util.List; + +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.util.ConfirmationDialog; +import de.danoeh.antennapod.model.feed.Feed; +import de.danoeh.antennapod.storage.database.DBWriter; +import io.reactivex.Completable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.schedulers.Schedulers; + +public class RemoveFeedDialog { + private static final String TAG = "RemoveFeedDialog"; + + public static void show(Context context, Feed feed, @Nullable Runnable callback) { + List feeds = Collections.singletonList(feed); + String message = getMessageId(context, feeds); + showDialog(context, feeds, message, callback); + } + + public static void show(Context context, List feeds) { + String message = getMessageId(context, feeds); + showDialog(context, feeds, message, null); + } + + private static void showDialog(Context context, List feeds, String message, @Nullable Runnable callback) { + ConfirmationDialog dialog = new ConfirmationDialog(context, R.string.remove_feed_label, message) { + @Override + public void onConfirmButtonPressed(DialogInterface clickedDialog) { + + if (callback != null) { + callback.run(); + } + + clickedDialog.dismiss(); + + ProgressDialog progressDialog = new ProgressDialog(context); + progressDialog.setMessage(context.getString(R.string.feed_remover_msg)); + progressDialog.setIndeterminate(true); + progressDialog.setCancelable(false); + progressDialog.show(); + + Completable.fromAction(() -> { + for (Feed feed : feeds) { + DBWriter.deleteFeed(context, feed.getId()).get(); + } + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + () -> { + Log.d(TAG, "Feed(s) deleted"); + progressDialog.dismiss(); + }, error -> { + Log.e(TAG, Log.getStackTraceString(error)); + progressDialog.dismiss(); + }); + } + }; + dialog.createNewDialog().show(); + } + + private static String getMessageId(Context context, List feeds) { + if (feeds.size() == 1) { + if (feeds.get(0).isLocalFeed()) { + return context.getString(R.string.feed_delete_confirmation_local_msg, feeds.get(0).getTitle()); + } else { + return context.getString(R.string.feed_delete_confirmation_msg, feeds.get(0).getTitle()); + } + } else { + return context.getString(R.string.feed_delete_confirmation_msg_batch); + } + + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/screen/feed/RenameFeedDialog.java b/app/src/main/java/de/danoeh/antennapod/ui/screen/feed/RenameFeedDialog.java new file mode 100644 index 000000000..4bc63f732 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/screen/feed/RenameFeedDialog.java @@ -0,0 +1,81 @@ +package de.danoeh.antennapod.ui.screen.feed; + +import android.app.Activity; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.List; + +import android.view.LayoutInflater; +import androidx.appcompat.app.AlertDialog; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.storage.database.NavDrawerData; +import de.danoeh.antennapod.model.feed.Feed; +import de.danoeh.antennapod.storage.database.DBWriter; +import de.danoeh.antennapod.databinding.EditTextDialogBinding; +import de.danoeh.antennapod.model.feed.FeedPreferences; + +public class RenameFeedDialog { + + private final WeakReference activityRef; + private Feed feed = null; + private NavDrawerData.DrawerItem drawerItem = null; + + public RenameFeedDialog(Activity activity, Feed feed) { + this.activityRef = new WeakReference<>(activity); + this.feed = feed; + } + + public RenameFeedDialog(Activity activity, NavDrawerData.DrawerItem drawerItem) { + this.activityRef = new WeakReference<>(activity); + this.drawerItem = drawerItem; + } + + public void show() { + Activity activity = activityRef.get(); + if (activity == null) { + return; + } + + final EditTextDialogBinding binding = EditTextDialogBinding.inflate(LayoutInflater.from(activity)); + String title = feed != null ? feed.getTitle() : drawerItem.getTitle(); + + binding.urlEditText.setText(title); + AlertDialog dialog = new MaterialAlertDialogBuilder(activity) + .setView(binding.getRoot()) + .setTitle(feed != null ? R.string.rename_feed_label : R.string.rename_tag_label) + .setPositiveButton(android.R.string.ok, (d, input) -> { + String newTitle = binding.urlEditText.getText().toString(); + if (feed != null) { + feed.setCustomTitle(newTitle); + DBWriter.setFeedCustomTitle(feed); + } else { + renameTag(newTitle); + } + }) + .setNeutralButton(de.danoeh.antennapod.core.R.string.reset, null) + .setNegativeButton(de.danoeh.antennapod.core.R.string.cancel_label, null) + .show(); + + // To prevent cancelling the dialog on button click + dialog.getButton(AlertDialog.BUTTON_NEUTRAL).setOnClickListener( + (view) -> binding.urlEditText.setText(title)); + } + + private void renameTag(String title) { + if (NavDrawerData.DrawerItem.Type.TAG == drawerItem.type) { + List feedPreferences = new ArrayList<>(); + for (NavDrawerData.DrawerItem item : ((NavDrawerData.TagDrawerItem) drawerItem).children) { + feedPreferences.add(((NavDrawerData.FeedDrawerItem) item).feed.getPreferences()); + } + + for (FeedPreferences preferences : feedPreferences) { + preferences.getTags().remove(drawerItem.getTitle()); + preferences.getTags().add(title); + DBWriter.setFeedPreferences(preferences); + } + } + } + +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/screen/feed/ToolbarIconTintManager.java b/app/src/main/java/de/danoeh/antennapod/ui/screen/feed/ToolbarIconTintManager.java new file mode 100644 index 000000000..e158053d0 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/screen/feed/ToolbarIconTintManager.java @@ -0,0 +1,59 @@ +package de.danoeh.antennapod.ui.screen.feed; + +import android.content.Context; +import android.graphics.PorterDuff.Mode; +import android.graphics.PorterDuffColorFilter; +import android.graphics.drawable.Drawable; +import android.view.ContextThemeWrapper; +import com.google.android.material.appbar.MaterialToolbar; +import com.google.android.material.appbar.AppBarLayout; +import com.google.android.material.appbar.CollapsingToolbarLayout; +import de.danoeh.antennapod.R; + +public abstract class ToolbarIconTintManager implements AppBarLayout.OnOffsetChangedListener { + private final Context context; + private final CollapsingToolbarLayout collapsingToolbar; + private final MaterialToolbar toolbar; + private boolean isTinted = false; + + public ToolbarIconTintManager(Context context, MaterialToolbar toolbar, CollapsingToolbarLayout collapsingToolbar) { + this.context = context; + this.collapsingToolbar = collapsingToolbar; + this.toolbar = toolbar; + } + + @Override + public void onOffsetChanged(AppBarLayout appBarLayout, int offset) { + boolean tint = (collapsingToolbar.getHeight() + offset) > (2 * collapsingToolbar.getMinimumHeight()); + if (isTinted != tint) { + isTinted = tint; + updateTint(); + } + } + + public void updateTint() { + if (isTinted) { + doTint(new ContextThemeWrapper(context, R.style.Theme_AntennaPod_Dark)); + safeSetColorFilter(toolbar.getNavigationIcon(), new PorterDuffColorFilter(0xffffffff, Mode.SRC_ATOP)); + safeSetColorFilter(toolbar.getOverflowIcon(), new PorterDuffColorFilter(0xffffffff, Mode.SRC_ATOP)); + safeSetColorFilter(toolbar.getCollapseIcon(), new PorterDuffColorFilter(0xffffffff, Mode.SRC_ATOP)); + } else { + doTint(context); + safeSetColorFilter(toolbar.getNavigationIcon(), null); + safeSetColorFilter(toolbar.getOverflowIcon(), null); + safeSetColorFilter(toolbar.getCollapseIcon(), null); + } + } + + private void safeSetColorFilter(Drawable icon, PorterDuffColorFilter filter) { + if (icon != null) { + icon.setColorFilter(filter); + } + } + + /** + * View expansion was changed. Icons need to be tinted + * @param themedContext ContextThemeWrapper with dark theme while expanded + */ + protected abstract void doTint(Context themedContext); +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/screen/feed/preferences/EditUrlSettingsDialog.java b/app/src/main/java/de/danoeh/antennapod/ui/screen/feed/preferences/EditUrlSettingsDialog.java new file mode 100644 index 000000000..767de3ecc --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/screen/feed/preferences/EditUrlSettingsDialog.java @@ -0,0 +1,88 @@ +package de.danoeh.antennapod.ui.screen.feed.preferences; + +import android.app.Activity; +import android.os.CountDownTimer; +import android.view.LayoutInflater; +import androidx.appcompat.app.AlertDialog; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.net.download.serviceinterface.FeedUpdateManager; +import de.danoeh.antennapod.storage.database.DBWriter; +import de.danoeh.antennapod.databinding.EditTextDialogBinding; +import de.danoeh.antennapod.model.feed.Feed; + +import java.lang.ref.WeakReference; +import java.util.Locale; +import java.util.concurrent.ExecutionException; + +public abstract class EditUrlSettingsDialog { + public static final String TAG = "EditUrlSettingsDialog"; + private final WeakReference activityRef; + private final Feed feed; + + public EditUrlSettingsDialog(Activity activity, Feed feed) { + this.activityRef = new WeakReference<>(activity); + this.feed = feed; + } + + public void show() { + Activity activity = activityRef.get(); + if (activity == null) { + return; + } + + final EditTextDialogBinding binding = EditTextDialogBinding.inflate(LayoutInflater.from(activity)); + + binding.urlEditText.setText(feed.getDownloadUrl()); + + new MaterialAlertDialogBuilder(activity) + .setView(binding.getRoot()) + .setTitle(R.string.edit_url_menu) + .setPositiveButton(android.R.string.ok, (d, input) -> + showConfirmAlertDialog(String.valueOf(binding.urlEditText.getText()))) + .setNegativeButton(R.string.cancel_label, null) + .show(); + } + + private void onConfirmed(String original, String updated) { + try { + DBWriter.updateFeedDownloadURL(original, updated).get(); + feed.setDownloadUrl(updated); + FeedUpdateManager.getInstance().runOnce(activityRef.get(), feed); + } catch (ExecutionException | InterruptedException e) { + throw new RuntimeException(e); + } + } + + private void showConfirmAlertDialog(String url) { + Activity activity = activityRef.get(); + + AlertDialog alertDialog = new MaterialAlertDialogBuilder(activity) + .setTitle(R.string.edit_url_menu) + .setMessage(R.string.edit_url_confirmation_msg) + .setPositiveButton(android.R.string.ok, (d, input) -> { + onConfirmed(feed.getDownloadUrl(), url); + setUrl(url); + }) + .setNegativeButton(R.string.cancel_label, null) + .show(); + alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false); + + new CountDownTimer(15000, 1000) { + @Override + public void onTick(long millisUntilFinished) { + alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setText( + String.format(Locale.getDefault(), "%s (%d)", + activity.getString(android.R.string.ok), millisUntilFinished / 1000 + 1)); + } + + @Override + public void onFinish() { + alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(true); + alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setText(android.R.string.ok); + } + }.start(); + } + + protected abstract void setUrl(String url); +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/screen/feed/preferences/EpisodeFilterDialog.java b/app/src/main/java/de/danoeh/antennapod/ui/screen/feed/preferences/EpisodeFilterDialog.java new file mode 100644 index 000000000..f07d37fec --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/screen/feed/preferences/EpisodeFilterDialog.java @@ -0,0 +1,112 @@ +package de.danoeh.antennapod.ui.screen.feed.preferences; + +import android.content.Context; +import android.content.DialogInterface; +import android.text.TextUtils; +import android.view.LayoutInflater; +import androidx.recyclerview.widget.GridLayoutManager; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.ui.SimpleChipAdapter; +import de.danoeh.antennapod.databinding.EpisodeFilterDialogBinding; +import de.danoeh.antennapod.model.feed.FeedFilter; +import de.danoeh.antennapod.ui.view.ItemOffsetDecoration; + +import java.util.List; + +/** + * Displays a dialog with a text box for filtering episodes and two radio buttons for exclusion/inclusion + */ +public abstract class EpisodeFilterDialog extends MaterialAlertDialogBuilder { + private final EpisodeFilterDialogBinding viewBinding; + private final List termList; + + public EpisodeFilterDialog(Context context, FeedFilter filter) { + super(context); + viewBinding = EpisodeFilterDialogBinding.inflate(LayoutInflater.from(context)); + + setTitle(R.string.episode_filters_label); + setView(viewBinding.getRoot()); + + viewBinding.durationCheckBox.setOnCheckedChangeListener( + (buttonView, isChecked) -> viewBinding.episodeFilterDurationText.setEnabled(isChecked)); + if (filter.hasMinimalDurationFilter()) { + viewBinding.durationCheckBox.setChecked(true); + // Store minimal duration in seconds, show in minutes + viewBinding.episodeFilterDurationText + .setText(String.valueOf(filter.getMinimalDurationFilter() / 60)); + } else { + viewBinding.episodeFilterDurationText.setEnabled(false); + } + + if (filter.excludeOnly()) { + termList = filter.getExcludeFilter(); + viewBinding.excludeRadio.setChecked(true); + } else { + termList = filter.getIncludeFilter(); + viewBinding.includeRadio.setChecked(true); + } + setupWordsList(); + + setNegativeButton(R.string.cancel_label, null); + setPositiveButton(R.string.confirm_label, this::onConfirmClick); + } + + private void setupWordsList() { + viewBinding.termsRecycler.setLayoutManager(new GridLayoutManager(getContext(), 2)); + viewBinding.termsRecycler.addItemDecoration(new ItemOffsetDecoration(getContext(), 4)); + SimpleChipAdapter adapter = new SimpleChipAdapter(getContext()) { + @Override + protected List getChips() { + return termList; + } + + @Override + protected void onRemoveClicked(int position) { + termList.remove(position); + notifyDataSetChanged(); + } + }; + viewBinding.termsRecycler.setAdapter(adapter); + viewBinding.termsTextInput.setEndIconOnClickListener(v -> { + String newWord = viewBinding.termsTextInput.getEditText().getText().toString().replace("\"", "").trim(); + if (TextUtils.isEmpty(newWord) || termList.contains(newWord)) { + return; + } + termList.add(newWord); + viewBinding.termsTextInput.getEditText().setText(""); + adapter.notifyDataSetChanged(); + }); + } + + protected abstract void onConfirmed(FeedFilter filter); + + private void onConfirmClick(DialogInterface dialog, int which) { + int minimalDuration = -1; + if (viewBinding.durationCheckBox.isChecked()) { + try { + // Store minimal duration in seconds + minimalDuration = Integer.parseInt( + viewBinding.episodeFilterDurationText.getText().toString()) * 60; + } catch (NumberFormatException e) { + // Do not change anything on error + } + } + String excludeFilter = ""; + String includeFilter = ""; + if (viewBinding.includeRadio.isChecked()) { + includeFilter = toFilterString(termList); + } else { + excludeFilter = toFilterString(termList); + } + onConfirmed(new FeedFilter(includeFilter, excludeFilter, minimalDuration)); + } + + private String toFilterString(List words) { + StringBuilder result = new StringBuilder(); + for (String word : words) { + result.append("\"").append(word).append("\" "); + } + return result.toString(); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/screen/feed/preferences/FeedPreferenceSkipDialog.java b/app/src/main/java/de/danoeh/antennapod/ui/screen/feed/preferences/FeedPreferenceSkipDialog.java new file mode 100644 index 000000000..bac336d2f --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/screen/feed/preferences/FeedPreferenceSkipDialog.java @@ -0,0 +1,48 @@ +package de.danoeh.antennapod.ui.screen.feed.preferences; + +import android.content.Context; +import android.view.View; +import android.widget.EditText; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import de.danoeh.antennapod.R; + +/** + * Displays a dialog with a username and password text field and an optional checkbox to save username and preferences. + */ +public abstract class FeedPreferenceSkipDialog extends MaterialAlertDialogBuilder { + + public FeedPreferenceSkipDialog(Context context, int skipIntroInitialValue, + int skipEndInitialValue) { + super(context); + setTitle(R.string.pref_feed_skip); + View rootView = View.inflate(context, R.layout.feed_pref_skip_dialog, null); + setView(rootView); + + final EditText etxtSkipIntro = rootView.findViewById(R.id.etxtSkipIntro); + final EditText etxtSkipEnd = rootView.findViewById(R.id.etxtSkipEnd); + + etxtSkipIntro.setText(String.valueOf(skipIntroInitialValue)); + etxtSkipEnd.setText(String.valueOf(skipEndInitialValue)); + + setNegativeButton(R.string.cancel_label, null); + setPositiveButton(R.string.confirm_label, (dialog, which) + -> { + int skipIntro; + int skipEnding; + try { + skipIntro = Integer.parseInt(etxtSkipIntro.getText().toString()); + } catch (NumberFormatException e) { + skipIntro = 0; + } + + try { + skipEnding = Integer.parseInt(etxtSkipEnd.getText().toString()); + } catch (NumberFormatException e) { + skipEnding = 0; + } + onConfirmed(skipIntro, skipEnding); + }); + } + + protected abstract void onConfirmed(int skipIntro, int skipEndig); +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/screen/feed/preferences/FeedSettingsFragment.java b/app/src/main/java/de/danoeh/antennapod/ui/screen/feed/preferences/FeedSettingsFragment.java new file mode 100644 index 000000000..4ac844479 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/screen/feed/preferences/FeedSettingsFragment.java @@ -0,0 +1,526 @@ +package de.danoeh.antennapod.ui.screen.feed.preferences; + +import android.Manifest; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.provider.Settings; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Toast; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import com.google.android.material.appbar.MaterialToolbar; +import androidx.fragment.app.Fragment; +import androidx.preference.ListPreference; +import androidx.preference.Preference; +import androidx.preference.PreferenceFragmentCompat; +import androidx.preference.SwitchPreferenceCompat; +import androidx.recyclerview.widget.RecyclerView; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.event.settings.SkipIntroEndingChangedEvent; +import de.danoeh.antennapod.event.settings.SpeedPresetChangedEvent; +import de.danoeh.antennapod.event.settings.VolumeAdaptionChangedEvent; +import de.danoeh.antennapod.databinding.PlaybackSpeedFeedSettingDialogBinding; +import de.danoeh.antennapod.model.feed.Feed; +import de.danoeh.antennapod.model.feed.FeedFilter; +import de.danoeh.antennapod.model.feed.FeedPreferences; +import de.danoeh.antennapod.model.feed.VolumeAdaptionSetting; +import de.danoeh.antennapod.net.download.serviceinterface.FeedUpdateManager; +import de.danoeh.antennapod.storage.preferences.UserPreferences; +import de.danoeh.antennapod.storage.database.DBReader; +import de.danoeh.antennapod.storage.database.DBWriter; +import de.danoeh.antennapod.ui.preferences.screen.synchronization.AuthenticationDialog; +import io.reactivex.Maybe; +import io.reactivex.MaybeOnSubscribe; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; +import io.reactivex.schedulers.Schedulers; + +import org.greenrobot.eventbus.EventBus; + +import java.util.Collections; +import java.util.Locale; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; + +public class FeedSettingsFragment extends Fragment { + private static final String TAG = "FeedSettingsFragment"; + private static final String EXTRA_FEED_ID = "de.danoeh.antennapod.extra.feedId"; + + private Disposable disposable; + + public static FeedSettingsFragment newInstance(Feed feed) { + FeedSettingsFragment fragment = new FeedSettingsFragment(); + Bundle arguments = new Bundle(); + arguments.putLong(EXTRA_FEED_ID, feed.getId()); + fragment.setArguments(arguments); + return fragment; + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View root = inflater.inflate(R.layout.feedsettings, container, false); + long feedId = getArguments().getLong(EXTRA_FEED_ID); + + MaterialToolbar toolbar = root.findViewById(R.id.toolbar); + toolbar.setNavigationOnClickListener(v -> getParentFragmentManager().popBackStack()); + + getParentFragmentManager().beginTransaction() + .replace(R.id.settings_fragment_container, + FeedSettingsPreferenceFragment.newInstance(feedId), "settings_fragment") + .commitAllowingStateLoss(); + + disposable = Maybe.create((MaybeOnSubscribe) emitter -> { + Feed feed = DBReader.getFeed(feedId); + if (feed != null) { + emitter.onSuccess(feed); + } else { + emitter.onComplete(); + } + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(result -> toolbar.setSubtitle(result.getTitle()), + error -> Log.d(TAG, Log.getStackTraceString(error)), + () -> { }); + + + return root; + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (disposable != null) { + disposable.dispose(); + } + } + + public static class FeedSettingsPreferenceFragment extends PreferenceFragmentCompat { + private static final CharSequence PREF_EPISODE_FILTER = "episodeFilter"; + private static final CharSequence PREF_SCREEN = "feedSettingsScreen"; + private static final CharSequence PREF_AUTHENTICATION = "authentication"; + private static final CharSequence PREF_AUTO_DELETE = "autoDelete"; + private static final CharSequence PREF_CATEGORY_AUTO_DOWNLOAD = "autoDownloadCategory"; + private static final CharSequence PREF_NEW_EPISODES_ACTION = "feedNewEpisodesAction"; + private static final String PREF_FEED_PLAYBACK_SPEED = "feedPlaybackSpeed"; + private static final String PREF_AUTO_SKIP = "feedAutoSkip"; + private static final String PREF_TAGS = "tags"; + + private Feed feed; + private Disposable disposable; + private FeedPreferences feedPreferences; + + public static FeedSettingsPreferenceFragment newInstance(long feedId) { + FeedSettingsPreferenceFragment fragment = new FeedSettingsPreferenceFragment(); + Bundle arguments = new Bundle(); + arguments.putLong(EXTRA_FEED_ID, feedId); + fragment.setArguments(arguments); + return fragment; + } + + boolean notificationPermissionDenied = false; + private final ActivityResultLauncher requestPermissionLauncher = + registerForActivityResult(new ActivityResultContracts.RequestPermission(), isGranted -> { + if (isGranted) { + return; + } + if (notificationPermissionDenied) { + Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); + Uri uri = Uri.fromParts("package", getContext().getPackageName(), null); + intent.setData(uri); + startActivity(intent); + return; + } + Toast.makeText(getContext(), R.string.notification_permission_denied, Toast.LENGTH_LONG).show(); + notificationPermissionDenied = true; + }); + + @Override + public RecyclerView onCreateRecyclerView(LayoutInflater inflater, ViewGroup parent, Bundle state) { + final RecyclerView view = super.onCreateRecyclerView(inflater, parent, state); + // To prevent transition animation because of summary update + view.setItemAnimator(null); + view.setLayoutAnimation(null); + return view; + } + + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + addPreferencesFromResource(R.xml.feed_settings); + // To prevent displaying partially loaded data + findPreference(PREF_SCREEN).setVisible(false); + + long feedId = getArguments().getLong(EXTRA_FEED_ID); + disposable = Maybe.create((MaybeOnSubscribe) emitter -> { + Feed feed = DBReader.getFeed(feedId); + if (feed != null) { + emitter.onSuccess(feed); + } else { + emitter.onComplete(); + } + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(result -> { + feed = result; + feedPreferences = feed.getPreferences(); + + setupAutoDownloadGlobalPreference(); + setupAutoDownloadPreference(); + setupKeepUpdatedPreference(); + setupAutoDeletePreference(); + setupVolumeAdaptationPreferences(); + setupNewEpisodesAction(); + setupAuthentificationPreference(); + setupEpisodeFilterPreference(); + setupPlaybackSpeedPreference(); + setupFeedAutoSkipPreference(); + setupEpisodeNotificationPreference(); + setupTags(); + + updateAutoDeleteSummary(); + updateVolumeAdaptationValue(); + updateAutoDownloadEnabled(); + updateNewEpisodesAction(); + + if (feed.isLocalFeed()) { + findPreference(PREF_AUTHENTICATION).setVisible(false); + findPreference(PREF_CATEGORY_AUTO_DOWNLOAD).setVisible(false); + } + + findPreference(PREF_SCREEN).setVisible(true); + }, error -> Log.d(TAG, Log.getStackTraceString(error)), () -> { }); + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (disposable != null) { + disposable.dispose(); + } + } + + private void setupFeedAutoSkipPreference() { + findPreference(PREF_AUTO_SKIP).setOnPreferenceClickListener(preference -> { + new FeedPreferenceSkipDialog(getContext(), + feedPreferences.getFeedSkipIntro(), + feedPreferences.getFeedSkipEnding()) { + @Override + protected void onConfirmed(int skipIntro, int skipEnding) { + feedPreferences.setFeedSkipIntro(skipIntro); + feedPreferences.setFeedSkipEnding(skipEnding); + DBWriter.setFeedPreferences(feedPreferences); + EventBus.getDefault().post( + new SkipIntroEndingChangedEvent(feedPreferences.getFeedSkipIntro(), + feedPreferences.getFeedSkipEnding(), + feed.getId())); + } + }.show(); + return false; + }); + } + + private void setupPlaybackSpeedPreference() { + Preference feedPlaybackSpeedPreference = findPreference(PREF_FEED_PLAYBACK_SPEED); + feedPlaybackSpeedPreference.setOnPreferenceClickListener(preference -> { + PlaybackSpeedFeedSettingDialogBinding viewBinding = + PlaybackSpeedFeedSettingDialogBinding.inflate(getLayoutInflater()); + viewBinding.seekBar.setProgressChangedListener(speed -> + viewBinding.currentSpeedLabel.setText(String.format(Locale.getDefault(), "%.2fx", speed))); + viewBinding.useGlobalCheckbox.setOnCheckedChangeListener((buttonView, isChecked) -> { + viewBinding.seekBar.setEnabled(!isChecked); + viewBinding.seekBar.setAlpha(isChecked ? 0.4f : 1f); + viewBinding.currentSpeedLabel.setAlpha(isChecked ? 0.4f : 1f); + + viewBinding.skipSilenceFeed.setEnabled(!isChecked); + viewBinding.skipSilenceFeed.setAlpha(isChecked ? 0.4f : 1f); + }); + float speed = feedPreferences.getFeedPlaybackSpeed(); + FeedPreferences.SkipSilence skipSilence = feedPreferences.getFeedSkipSilence(); + boolean isGlobal = speed == FeedPreferences.SPEED_USE_GLOBAL; + viewBinding.useGlobalCheckbox.setChecked(isGlobal); + viewBinding.seekBar.updateSpeed(isGlobal ? 1 : speed); + viewBinding.skipSilenceFeed.setChecked(!isGlobal + && skipSilence == FeedPreferences.SkipSilence.AGGRESSIVE); + new MaterialAlertDialogBuilder(getContext()) + .setTitle(R.string.playback_speed) + .setView(viewBinding.getRoot()) + .setPositiveButton(android.R.string.ok, (dialog, which) -> { + float newSpeed = viewBinding.useGlobalCheckbox.isChecked() + ? FeedPreferences.SPEED_USE_GLOBAL : viewBinding.seekBar.getCurrentSpeed(); + feedPreferences.setFeedPlaybackSpeed(newSpeed); + FeedPreferences.SkipSilence newSkipSilence; + if (viewBinding.useGlobalCheckbox.isChecked()) { + newSkipSilence = FeedPreferences.SkipSilence.GLOBAL; + } else if (viewBinding.skipSilenceFeed.isChecked()) { + newSkipSilence = FeedPreferences.SkipSilence.AGGRESSIVE; + } else { + newSkipSilence = FeedPreferences.SkipSilence.OFF; + } + feedPreferences.setFeedSkipSilence(newSkipSilence); + DBWriter.setFeedPreferences(feedPreferences); + EventBus.getDefault().post(new SpeedPresetChangedEvent( + feedPreferences.getFeedPlaybackSpeed(), + feed.getId(), feedPreferences.getFeedSkipSilence())); + }) + .setNegativeButton(R.string.cancel_label, null) + .show(); + return true; + }); + } + + private void setupEpisodeFilterPreference() { + findPreference(PREF_EPISODE_FILTER).setOnPreferenceClickListener(preference -> { + new EpisodeFilterDialog(getContext(), feedPreferences.getFilter()) { + @Override + protected void onConfirmed(FeedFilter filter) { + feedPreferences.setFilter(filter); + DBWriter.setFeedPreferences(feedPreferences); + } + }.show(); + return false; + }); + } + + private void setupAuthentificationPreference() { + findPreference(PREF_AUTHENTICATION).setOnPreferenceClickListener(preference -> { + new AuthenticationDialog(getContext(), + R.string.authentication_label, true, + feedPreferences.getUsername(), feedPreferences.getPassword()) { + @Override + protected void onConfirmed(String username, String password) { + feedPreferences.setUsername(username); + feedPreferences.setPassword(password); + Future setPreferencesFuture = DBWriter.setFeedPreferences(feedPreferences); + + new Thread(() -> { + try { + setPreferencesFuture.get(); + } catch (InterruptedException | ExecutionException e) { + e.printStackTrace(); + } + FeedUpdateManager.getInstance().runOnce(getContext(), feed); + }, "RefreshAfterCredentialChange").start(); + } + }.show(); + return false; + }); + } + + private void setupAutoDeletePreference() { + findPreference(PREF_AUTO_DELETE).setOnPreferenceChangeListener((preference, newValue) -> { + switch ((String) newValue) { + case "global": + feedPreferences.setAutoDeleteAction(FeedPreferences.AutoDeleteAction.GLOBAL); + break; + case "always": + feedPreferences.setAutoDeleteAction(FeedPreferences.AutoDeleteAction.ALWAYS); + break; + case "never": + feedPreferences.setAutoDeleteAction(FeedPreferences.AutoDeleteAction.NEVER); + break; + default: + } + DBWriter.setFeedPreferences(feedPreferences); + updateAutoDeleteSummary(); + return false; + }); + } + + private void updateAutoDeleteSummary() { + ListPreference autoDeletePreference = findPreference(PREF_AUTO_DELETE); + + switch (feedPreferences.getAutoDeleteAction()) { + case GLOBAL: + autoDeletePreference.setSummary(R.string.global_default); + autoDeletePreference.setValue("global"); + break; + case ALWAYS: + autoDeletePreference.setSummary(R.string.feed_auto_download_always); + autoDeletePreference.setValue("always"); + break; + case NEVER: + autoDeletePreference.setSummary(R.string.feed_auto_download_never); + autoDeletePreference.setValue("never"); + break; + } + } + + private void setupVolumeAdaptationPreferences() { + ListPreference volumeAdaptationPreference = findPreference("volumeReduction"); + volumeAdaptationPreference.setOnPreferenceChangeListener((preference, newValue) -> { + switch ((String) newValue) { + case "off": + feedPreferences.setVolumeAdaptionSetting(VolumeAdaptionSetting.OFF); + break; + case "light": + feedPreferences.setVolumeAdaptionSetting(VolumeAdaptionSetting.LIGHT_REDUCTION); + break; + case "heavy": + feedPreferences.setVolumeAdaptionSetting(VolumeAdaptionSetting.HEAVY_REDUCTION); + break; + case "light_boost": + feedPreferences.setVolumeAdaptionSetting(VolumeAdaptionSetting.LIGHT_BOOST); + break; + case "medium_boost": + feedPreferences.setVolumeAdaptionSetting(VolumeAdaptionSetting.MEDIUM_BOOST); + break; + case "heavy_boost": + feedPreferences.setVolumeAdaptionSetting(VolumeAdaptionSetting.HEAVY_BOOST); + break; + default: + } + DBWriter.setFeedPreferences(feedPreferences); + updateVolumeAdaptationValue(); + EventBus.getDefault().post( + new VolumeAdaptionChangedEvent(feedPreferences.getVolumeAdaptionSetting(), feed.getId())); + return false; + }); + } + + private void updateVolumeAdaptationValue() { + ListPreference volumeAdaptationPreference = findPreference("volumeReduction"); + + switch (feedPreferences.getVolumeAdaptionSetting()) { + case OFF: + volumeAdaptationPreference.setValue("off"); + break; + case LIGHT_REDUCTION: + volumeAdaptationPreference.setValue("light"); + break; + case HEAVY_REDUCTION: + volumeAdaptationPreference.setValue("heavy"); + break; + case LIGHT_BOOST: + volumeAdaptationPreference.setValue("light_boost"); + break; + case MEDIUM_BOOST: + volumeAdaptationPreference.setValue("medium_boost"); + break; + case HEAVY_BOOST: + volumeAdaptationPreference.setValue("heavy_boost"); + break; + } + } + + private void setupNewEpisodesAction() { + findPreference(PREF_NEW_EPISODES_ACTION).setOnPreferenceChangeListener((preference, newValue) -> { + int code = Integer.parseInt((String) newValue); + feedPreferences.setNewEpisodesAction(FeedPreferences.NewEpisodesAction.fromCode(code)); + DBWriter.setFeedPreferences(feedPreferences); + updateNewEpisodesAction(); + return false; + }); + } + + private void updateNewEpisodesAction() { + ListPreference newEpisodesAction = findPreference(PREF_NEW_EPISODES_ACTION); + newEpisodesAction.setValue("" + feedPreferences.getNewEpisodesAction().code); + + switch (feedPreferences.getNewEpisodesAction()) { + case GLOBAL: + newEpisodesAction.setSummary(R.string.global_default); + break; + case ADD_TO_INBOX: + newEpisodesAction.setSummary(R.string.feed_new_episodes_action_add_to_inbox); + break; + case ADD_TO_QUEUE: + newEpisodesAction.setSummary(R.string.feed_new_episodes_action_add_to_queue); + break; + case NOTHING: + newEpisodesAction.setSummary(R.string.feed_new_episodes_action_nothing); + break; + default: + } + } + + private void setupKeepUpdatedPreference() { + SwitchPreferenceCompat pref = findPreference("keepUpdated"); + + pref.setChecked(feedPreferences.getKeepUpdated()); + pref.setOnPreferenceChangeListener((preference, newValue) -> { + boolean checked = newValue == Boolean.TRUE; + feedPreferences.setKeepUpdated(checked); + DBWriter.setFeedPreferences(feedPreferences); + pref.setChecked(checked); + return false; + }); + } + + private void setupAutoDownloadGlobalPreference() { + if (!UserPreferences.isEnableAutodownload()) { + SwitchPreferenceCompat autodl = findPreference("autoDownload"); + autodl.setChecked(false); + autodl.setEnabled(false); + autodl.setSummary(R.string.auto_download_disabled_globally); + findPreference(PREF_EPISODE_FILTER).setEnabled(false); + } + } + + private void setupAutoDownloadPreference() { + SwitchPreferenceCompat pref = findPreference("autoDownload"); + + pref.setEnabled(UserPreferences.isEnableAutodownload()); + if (UserPreferences.isEnableAutodownload()) { + pref.setChecked(feedPreferences.getAutoDownload()); + } else { + pref.setChecked(false); + pref.setSummary(R.string.auto_download_disabled_globally); + } + + pref.setOnPreferenceChangeListener((preference, newValue) -> { + boolean checked = newValue == Boolean.TRUE; + + feedPreferences.setAutoDownload(checked); + DBWriter.setFeedPreferences(feedPreferences); + updateAutoDownloadEnabled(); + pref.setChecked(checked); + return false; + }); + } + + private void updateAutoDownloadEnabled() { + if (feed != null && feed.getPreferences() != null) { + boolean enabled = feed.getPreferences().getAutoDownload() && UserPreferences.isEnableAutodownload(); + findPreference(PREF_EPISODE_FILTER).setEnabled(enabled); + } + } + + private void setupTags() { + findPreference(PREF_TAGS).setOnPreferenceClickListener(preference -> { + TagSettingsDialog.newInstance(Collections.singletonList(feedPreferences)) + .show(getChildFragmentManager(), TagSettingsDialog.TAG); + return true; + }); + } + + private void setupEpisodeNotificationPreference() { + SwitchPreferenceCompat pref = findPreference("episodeNotification"); + + pref.setChecked(feedPreferences.getShowEpisodeNotification()); + pref.setOnPreferenceChangeListener((preference, newValue) -> { + if (Build.VERSION.SDK_INT >= 33 && ContextCompat.checkSelfPermission(getContext(), + Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { + requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS); + return false; + } + boolean checked = newValue == Boolean.TRUE; + feedPreferences.setShowEpisodeNotification(checked); + DBWriter.setFeedPreferences(feedPreferences); + pref.setChecked(checked); + return false; + }); + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/screen/feed/preferences/SkipPreferenceDialog.java b/app/src/main/java/de/danoeh/antennapod/ui/screen/feed/preferences/SkipPreferenceDialog.java new file mode 100644 index 000000000..983e265c1 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/screen/feed/preferences/SkipPreferenceDialog.java @@ -0,0 +1,64 @@ +package de.danoeh.antennapod.ui.screen.feed.preferences; + +import android.content.Context; +import android.widget.TextView; +import androidx.appcompat.app.AlertDialog; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; + +import java.text.NumberFormat; +import java.util.Locale; + +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.storage.preferences.UserPreferences; + +/** + * Shows the dialog that allows setting the skip time. + */ +public class SkipPreferenceDialog { + public static void showSkipPreference(Context context, SkipDirection direction, TextView textView) { + int checked = 0; + + int skipSecs; + if (direction == SkipDirection.SKIP_FORWARD) { + skipSecs = UserPreferences.getFastForwardSecs(); + } else { + skipSecs = UserPreferences.getRewindSecs(); + } + + final int[] values = context.getResources().getIntArray(R.array.seek_delta_values); + final String[] choices = new String[values.length]; + for (int i = 0; i < values.length; i++) { + if (skipSecs == values[i]) { + checked = i; + } + choices[i] = String.format(Locale.getDefault(), + "%d %s", values[i], context.getString(R.string.time_seconds)); + } + + MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(context); + builder.setTitle(direction == SkipDirection.SKIP_FORWARD ? R.string.pref_fast_forward : R.string.pref_rewind); + builder.setSingleChoiceItems(choices, checked, (dialog, which) -> { + int choice = ((AlertDialog) dialog).getListView().getCheckedItemPosition(); + if (choice < 0 || choice >= values.length) { + System.err.printf("Choice in showSkipPreference is out of bounds %d", choice); + } else { + int seconds = values[choice]; + if (direction == SkipDirection.SKIP_FORWARD) { + UserPreferences.setFastForwardSecs(seconds); + } else { + UserPreferences.setRewindSecs(seconds); + } + if (textView != null) { + textView.setText(NumberFormat.getInstance().format(seconds)); + } + dialog.dismiss(); + } + }); + builder.setNegativeButton(R.string.cancel_label, null); + builder.show(); + } + + public enum SkipDirection { + SKIP_FORWARD, SKIP_REWIND + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/screen/feed/preferences/TagSettingsDialog.java b/app/src/main/java/de/danoeh/antennapod/ui/screen/feed/preferences/TagSettingsDialog.java new file mode 100644 index 000000000..accc5cc60 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/screen/feed/preferences/TagSettingsDialog.java @@ -0,0 +1,156 @@ +package de.danoeh.antennapod.ui.screen.feed.preferences; + +import android.app.Dialog; +import android.os.Bundle; +import android.text.TextUtils; +import android.util.Log; +import android.view.MotionEvent; +import android.view.View; +import android.widget.ArrayAdapter; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.DialogFragment; +import androidx.recyclerview.widget.GridLayoutManager; + +import com.google.android.material.dialog.MaterialAlertDialogBuilder; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.ui.SimpleChipAdapter; +import de.danoeh.antennapod.storage.database.DBReader; +import de.danoeh.antennapod.storage.database.DBWriter; +import de.danoeh.antennapod.storage.database.NavDrawerData; +import de.danoeh.antennapod.databinding.EditTagsDialogBinding; +import de.danoeh.antennapod.model.feed.FeedCounter; +import de.danoeh.antennapod.model.feed.FeedOrder; +import de.danoeh.antennapod.model.feed.FeedPreferences; +import de.danoeh.antennapod.ui.view.ItemOffsetDecoration; +import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.schedulers.Schedulers; + +public class TagSettingsDialog extends DialogFragment { + public static final String TAG = "TagSettingsDialog"; + private static final String ARG_FEED_PREFERENCES = "feed_preferences"; + private List displayedTags; + private EditTagsDialogBinding viewBinding; + private SimpleChipAdapter adapter; + + public static TagSettingsDialog newInstance(List preferencesList) { + TagSettingsDialog fragment = new TagSettingsDialog(); + Bundle args = new Bundle(); + args.putSerializable(ARG_FEED_PREFERENCES, new ArrayList<>(preferencesList)); + fragment.setArguments(args); + return fragment; + } + + @NonNull + @Override + public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { + ArrayList feedPreferencesList = + (ArrayList) getArguments().getSerializable(ARG_FEED_PREFERENCES); + Set commonTags = new HashSet<>(feedPreferencesList.get(0).getTags()); + + for (FeedPreferences preference : feedPreferencesList) { + commonTags.retainAll(preference.getTags()); + } + displayedTags = new ArrayList<>(commonTags); + displayedTags.remove(FeedPreferences.TAG_ROOT); + + viewBinding = EditTagsDialogBinding.inflate(getLayoutInflater()); + viewBinding.tagsRecycler.setLayoutManager(new GridLayoutManager(getContext(), 2)); + viewBinding.tagsRecycler.addItemDecoration(new ItemOffsetDecoration(getContext(), 4)); + adapter = new SimpleChipAdapter(getContext()) { + @Override + protected List getChips() { + return displayedTags; + } + + @Override + protected void onRemoveClicked(int position) { + displayedTags.remove(position); + notifyDataSetChanged(); + } + }; + viewBinding.tagsRecycler.setAdapter(adapter); + viewBinding.rootFolderCheckbox.setChecked(commonTags.contains(FeedPreferences.TAG_ROOT)); + + viewBinding.newTagTextInput.setEndIconOnClickListener(v -> + addTag(viewBinding.newTagEditText.getText().toString().trim())); + + loadTags(); + viewBinding.newTagEditText.setThreshold(1); + viewBinding.newTagEditText.setOnTouchListener(new View.OnTouchListener() { + @Override + public boolean onTouch(View v, MotionEvent event) { + viewBinding.newTagEditText.showDropDown(); + viewBinding.newTagEditText.requestFocus(); + return false; + } + }); + + if (feedPreferencesList.size() > 1) { + viewBinding.commonTagsInfo.setVisibility(View.VISIBLE); + } + + MaterialAlertDialogBuilder dialog = new MaterialAlertDialogBuilder(getContext()); + dialog.setView(viewBinding.getRoot()); + dialog.setTitle(R.string.feed_tags_label); + dialog.setPositiveButton(android.R.string.ok, (d, input) -> { + addTag(viewBinding.newTagEditText.getText().toString().trim()); + updatePreferencesTags(feedPreferencesList, commonTags); + }); + dialog.setNegativeButton(R.string.cancel_label, null); + return dialog.create(); + } + + private void loadTags() { + Observable.fromCallable( + () -> { + NavDrawerData data = DBReader.getNavDrawerData(null, FeedOrder.ALPHABETICAL, FeedCounter.SHOW_NONE); + List items = data.items; + List folders = new ArrayList(); + for (NavDrawerData.DrawerItem item : items) { + if (item.type == NavDrawerData.DrawerItem.Type.TAG) { + folders.add(item.getTitle()); + } + } + return folders; + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + result -> { + ArrayAdapter acAdapter = new ArrayAdapter(getContext(), + R.layout.single_tag_text_view, result); + viewBinding.newTagEditText.setAdapter(acAdapter); + }, error -> { + Log.e(TAG, Log.getStackTraceString(error)); + }); + } + + private void addTag(String name) { + if (TextUtils.isEmpty(name) || displayedTags.contains(name)) { + return; + } + displayedTags.add(name); + viewBinding.newTagEditText.setText(""); + adapter.notifyDataSetChanged(); + } + + private void updatePreferencesTags(List feedPreferencesList, Set commonTags) { + if (viewBinding.rootFolderCheckbox.isChecked()) { + displayedTags.add(FeedPreferences.TAG_ROOT); + } + for (FeedPreferences preferences : feedPreferencesList) { + preferences.getTags().removeAll(commonTags); + preferences.getTags().addAll(displayedTags); + DBWriter.setFeedPreferences(preferences); + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/screen/feed/preferences/VolumeAdaptationPreference.java b/app/src/main/java/de/danoeh/antennapod/ui/screen/feed/preferences/VolumeAdaptationPreference.java new file mode 100644 index 000000000..70f5da5fc --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/screen/feed/preferences/VolumeAdaptationPreference.java @@ -0,0 +1,28 @@ +package de.danoeh.antennapod.ui.screen.feed.preferences; + +import android.content.Context; +import android.util.AttributeSet; + +import java.util.Arrays; + +import de.danoeh.antennapod.model.feed.VolumeAdaptionSetting; +import de.danoeh.antennapod.ui.preferences.preference.MaterialListPreference; + +public class VolumeAdaptationPreference extends MaterialListPreference { + public VolumeAdaptationPreference(Context context) { + super(context); + } + + public VolumeAdaptationPreference(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + public CharSequence[] getEntries() { + if (VolumeAdaptionSetting.isBoostSupported()) { + return super.getEntries(); + } else { + return Arrays.copyOfRange(super.getEntries(), 0, 3); + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/screen/home/HomeFragment.java b/app/src/main/java/de/danoeh/antennapod/ui/screen/home/HomeFragment.java new file mode 100644 index 000000000..bcbc2675a --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/screen/home/HomeFragment.java @@ -0,0 +1,207 @@ +package de.danoeh.antennapod.ui.screen.home; + +import android.Manifest; +import android.content.Context; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.os.Build; +import android.os.Bundle; +import android.text.TextUtils; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.appcompat.widget.Toolbar; +import androidx.core.content.ContextCompat; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentContainerView; + +import de.danoeh.antennapod.net.download.serviceinterface.FeedUpdateManager; +import de.danoeh.antennapod.ui.echo.EchoConfig; +import de.danoeh.antennapod.ui.screen.home.sections.AllowNotificationsSection; +import de.danoeh.antennapod.ui.screen.home.sections.DownloadsSection; +import de.danoeh.antennapod.ui.screen.home.sections.EchoSection; +import de.danoeh.antennapod.ui.screen.home.sections.EpisodesSurpriseSection; +import de.danoeh.antennapod.ui.screen.home.sections.InboxSection; +import de.danoeh.antennapod.ui.screen.home.sections.QueueSection; +import de.danoeh.antennapod.ui.screen.home.sections.SubscriptionsSection; +import org.greenrobot.eventbus.EventBus; +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.List; + +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.storage.database.DBReader; +import de.danoeh.antennapod.databinding.HomeFragmentBinding; +import de.danoeh.antennapod.event.FeedListUpdateEvent; +import de.danoeh.antennapod.event.FeedUpdateRunningEvent; +import de.danoeh.antennapod.ui.screen.SearchFragment; +import de.danoeh.antennapod.storage.preferences.UserPreferences; +import de.danoeh.antennapod.ui.view.LiftOnScrollListener; +import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; +import io.reactivex.schedulers.Schedulers; + +/** + * Shows unread or recently published episodes + */ +public class HomeFragment extends Fragment implements Toolbar.OnMenuItemClickListener { + + public static final String TAG = "HomeFragment"; + public static final String PREF_NAME = "PrefHomeFragment"; + public static final String PREF_HIDDEN_SECTIONS = "PrefHomeSectionsString"; + public static final String PREF_DISABLE_NOTIFICATION_PERMISSION_NAG = "DisableNotificationPermissionNag"; + public static final String PREF_HIDE_ECHO = "HideEcho"; + + private static final String KEY_UP_ARROW = "up_arrow"; + private boolean displayUpArrow; + private HomeFragmentBinding viewBinding; + private Disposable disposable; + + @NonNull + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + super.onCreateView(inflater, container, savedInstanceState); + viewBinding = HomeFragmentBinding.inflate(inflater); + viewBinding.toolbar.inflateMenu(R.menu.home); + viewBinding.toolbar.setOnMenuItemClickListener(this); + if (savedInstanceState != null) { + displayUpArrow = savedInstanceState.getBoolean(KEY_UP_ARROW); + } + viewBinding.homeScrollView.setOnScrollChangeListener(new LiftOnScrollListener(viewBinding.appbar)); + ((MainActivity) requireActivity()).setupToolbarToggle(viewBinding.toolbar, displayUpArrow); + populateSectionList(); + updateWelcomeScreenVisibility(); + + viewBinding.swipeRefresh.setDistanceToTriggerSync(getResources().getInteger(R.integer.swipe_refresh_distance)); + viewBinding.swipeRefresh.setOnRefreshListener(() -> + FeedUpdateManager.getInstance().runOnceOrAsk(requireContext())); + + return viewBinding.getRoot(); + } + + private void populateSectionList() { + viewBinding.homeContainer.removeAllViews(); + + SharedPreferences prefs = getContext().getSharedPreferences(HomeFragment.PREF_NAME, Context.MODE_PRIVATE); + if (Build.VERSION.SDK_INT >= 33 && ContextCompat.checkSelfPermission(getContext(), + Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { + if (!prefs.getBoolean(HomeFragment.PREF_DISABLE_NOTIFICATION_PERMISSION_NAG, false)) { + addSection(new AllowNotificationsSection()); + } + } + if (Calendar.getInstance().get(Calendar.YEAR) == EchoConfig.RELEASE_YEAR + && Calendar.getInstance().get(Calendar.MONTH) == Calendar.DECEMBER + && Calendar.getInstance().get(Calendar.DAY_OF_MONTH) >= 10 + && prefs.getInt(PREF_HIDE_ECHO, 0) != EchoConfig.RELEASE_YEAR) { + addSection(new EchoSection()); + } + + List hiddenSections = getHiddenSections(getContext()); + String[] sectionTags = getResources().getStringArray(R.array.home_section_tags); + for (String sectionTag : sectionTags) { + if (hiddenSections.contains(sectionTag)) { + continue; + } + addSection(getSection(sectionTag)); + } + } + + private void addSection(Fragment section) { + FragmentContainerView containerView = new FragmentContainerView(getContext()); + containerView.setId(View.generateViewId()); + viewBinding.homeContainer.addView(containerView); + getChildFragmentManager().beginTransaction().add(containerView.getId(), section).commit(); + } + + private Fragment getSection(String tag) { + switch (tag) { + case QueueSection.TAG: + return new QueueSection(); + case InboxSection.TAG: + return new InboxSection(); + case EpisodesSurpriseSection.TAG: + return new EpisodesSurpriseSection(); + case SubscriptionsSection.TAG: + return new SubscriptionsSection(); + case DownloadsSection.TAG: + return new DownloadsSection(); + default: + return null; + } + } + + public static List getHiddenSections(Context context) { + SharedPreferences prefs = context.getSharedPreferences(HomeFragment.PREF_NAME, Context.MODE_PRIVATE); + String hiddenSectionsString = prefs.getString(HomeFragment.PREF_HIDDEN_SECTIONS, ""); + return new ArrayList<>(Arrays.asList(TextUtils.split(hiddenSectionsString, ","))); + } + + @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) + public void onEventMainThread(FeedUpdateRunningEvent event) { + viewBinding.swipeRefresh.setRefreshing(event.isFeedUpdateRunning); + } + + @Override + public boolean onMenuItemClick(MenuItem item) { + if (item.getItemId() == R.id.homesettings_items) { + HomeSectionsSettingsDialog.open(getContext(), (dialogInterface, i) -> populateSectionList()); + return true; + } else if (item.getItemId() == R.id.refresh_item) { + FeedUpdateManager.getInstance().runOnceOrAsk(requireContext()); + return true; + } else if (item.getItemId() == R.id.action_search) { + ((MainActivity) getActivity()).loadChildFragment(SearchFragment.newInstance()); + return true; + } + return false; + } + + @Override + public void onSaveInstanceState(@NonNull Bundle outState) { + outState.putBoolean(KEY_UP_ARROW, displayUpArrow); + super.onSaveInstanceState(outState); + } + + @Override + public void onStart() { + super.onStart(); + EventBus.getDefault().register(this); + } + + @Override + public void onStop() { + super.onStop(); + EventBus.getDefault().unregister(this); + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onFeedListChanged(FeedListUpdateEvent event) { + updateWelcomeScreenVisibility(); + } + + private void updateWelcomeScreenVisibility() { + if (disposable != null) { + disposable.dispose(); + } + disposable = Observable.fromCallable(() -> + DBReader.getNavDrawerData(UserPreferences.getSubscriptionsFilter(), + UserPreferences.getFeedOrder(), UserPreferences.getFeedCounterSetting()).items.size()) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(numSubscriptions -> { + viewBinding.welcomeContainer.setVisibility(numSubscriptions == 0 ? View.VISIBLE : View.GONE); + viewBinding.homeContainer.setVisibility(numSubscriptions == 0 ? View.GONE : View.VISIBLE); + }, error -> Log.e(TAG, Log.getStackTraceString(error))); + } + +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/screen/home/HomeSection.java b/app/src/main/java/de/danoeh/antennapod/ui/screen/home/HomeSection.java new file mode 100644 index 000000000..908267f47 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/screen/home/HomeSection.java @@ -0,0 +1,105 @@ +package de.danoeh.antennapod.ui.screen.home; + +import android.os.Bundle; +import android.text.TextUtils; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.DefaultItemAnimator; +import de.danoeh.antennapod.ui.episodeslist.EpisodeItemListAdapter; +import de.danoeh.antennapod.ui.screen.subscriptions.HorizontalFeedListAdapter; +import de.danoeh.antennapod.ui.episodeslist.HorizontalItemListAdapter; +import de.danoeh.antennapod.databinding.HomeSectionBinding; +import de.danoeh.antennapod.ui.episodeslist.FeedItemMenuHandler; +import de.danoeh.antennapod.ui.screen.subscriptions.FeedMenuHandler; +import de.danoeh.antennapod.model.feed.Feed; +import de.danoeh.antennapod.model.feed.FeedItem; +import org.greenrobot.eventbus.EventBus; + +import java.util.Locale; + +/** + * Section on the HomeFragment + */ +public abstract class HomeSection extends Fragment implements View.OnCreateContextMenuListener { + public static final String TAG = "HomeSection"; + protected HomeSectionBinding viewBinding; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, + @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + viewBinding = HomeSectionBinding.inflate(inflater); + viewBinding.titleLabel.setText(getSectionTitle()); + if (TextUtils.getLayoutDirectionFromLocale(Locale.getDefault()) == View.LAYOUT_DIRECTION_LTR) { + viewBinding.moreButton.setText(getMoreLinkTitle() + "\u00A0»"); + } else { + viewBinding.moreButton.setText("«\u00A0" + getMoreLinkTitle()); + } + viewBinding.moreButton.setOnClickListener((view) -> handleMoreClick()); + if (TextUtils.isEmpty(getMoreLinkTitle())) { + viewBinding.moreButton.setVisibility(View.INVISIBLE); + } + // Dummies are necessary to ensure height, but do not animate them + viewBinding.recyclerView.setItemAnimator(null); + viewBinding.recyclerView.postDelayed( + () -> viewBinding.recyclerView.setItemAnimator(new DefaultItemAnimator()), 500); + return viewBinding.getRoot(); + } + + @Override + public boolean onContextItemSelected(@NonNull MenuItem item) { + if (!getUserVisibleHint() || !isVisible() || !isMenuVisible()) { + // The method is called on all fragments in a ViewPager, so this needs to be ignored in invisible ones. + // Apparently, none of the visibility check method works reliably on its own, so we just use all. + return false; + } + if (viewBinding.recyclerView.getAdapter() instanceof HorizontalFeedListAdapter) { + HorizontalFeedListAdapter adapter = (HorizontalFeedListAdapter) viewBinding.recyclerView.getAdapter(); + Feed selectedFeed = adapter.getLongPressedItem(); + return selectedFeed != null + && FeedMenuHandler.onMenuItemClicked(this, item.getItemId(), selectedFeed, () -> { }); + } + FeedItem longPressedItem; + if (viewBinding.recyclerView.getAdapter() instanceof EpisodeItemListAdapter) { + EpisodeItemListAdapter adapter = (EpisodeItemListAdapter) viewBinding.recyclerView.getAdapter(); + longPressedItem = adapter.getLongPressedItem(); + } else if (viewBinding.recyclerView.getAdapter() instanceof HorizontalItemListAdapter) { + HorizontalItemListAdapter adapter = (HorizontalItemListAdapter) viewBinding.recyclerView.getAdapter(); + longPressedItem = adapter.getLongPressedItem(); + } else { + return false; + } + + if (longPressedItem == null) { + Log.i(TAG, "Selected item or listAdapter was null, ignoring selection"); + return super.onContextItemSelected(item); + } + return FeedItemMenuHandler.onMenuItemClicked(this, item.getItemId(), longPressedItem); + } + + @Override + public void onStart() { + super.onStart(); + EventBus.getDefault().register(this); + registerForContextMenu(viewBinding.recyclerView); + } + + @Override + public void onStop() { + super.onStop(); + EventBus.getDefault().unregister(this); + unregisterForContextMenu(viewBinding.recyclerView); + } + + protected abstract String getSectionTitle(); + + protected abstract String getMoreLinkTitle(); + + protected abstract void handleMoreClick(); +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/screen/home/HomeSectionsSettingsDialog.java b/app/src/main/java/de/danoeh/antennapod/ui/screen/home/HomeSectionsSettingsDialog.java new file mode 100644 index 000000000..3238c299c --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/screen/home/HomeSectionsSettingsDialog.java @@ -0,0 +1,42 @@ +package de.danoeh.antennapod.ui.screen.home; + +import android.content.Context; +import android.content.DialogInterface; +import android.content.SharedPreferences; +import android.text.TextUtils; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import de.danoeh.antennapod.R; + +import java.util.List; + +public class HomeSectionsSettingsDialog { + public static void open(Context context, DialogInterface.OnClickListener onSettingsChanged) { + final List hiddenSections = HomeFragment.getHiddenSections(context); + String[] sectionLabels = context.getResources().getStringArray(R.array.home_section_titles); + String[] sectionTags = context.getResources().getStringArray(R.array.home_section_tags); + final boolean[] checked = new boolean[sectionLabels.length]; + for (int i = 0; i < sectionLabels.length; i++) { + String tag = sectionTags[i]; + if (!hiddenSections.contains(tag)) { + checked[i] = true; + } + } + + MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(context); + builder.setTitle(R.string.configure_home); + builder.setMultiChoiceItems(sectionLabels, checked, (dialog, which, isChecked) -> { + if (isChecked) { + hiddenSections.remove(sectionTags[which]); + } else { + hiddenSections.add(sectionTags[which]); + } + }); + builder.setPositiveButton(R.string.confirm_label, (dialog, which) -> { + SharedPreferences prefs = context.getSharedPreferences(HomeFragment.PREF_NAME, Context.MODE_PRIVATE); + prefs.edit().putString(HomeFragment.PREF_HIDDEN_SECTIONS, TextUtils.join(",", hiddenSections)).apply(); + onSettingsChanged.onClick(dialog, which); + }); + builder.setNegativeButton(R.string.cancel_label, null); + builder.create().show(); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/screen/home/sections/AllowNotificationsSection.java b/app/src/main/java/de/danoeh/antennapod/ui/screen/home/sections/AllowNotificationsSection.java new file mode 100644 index 000000000..0b26865c8 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/screen/home/sections/AllowNotificationsSection.java @@ -0,0 +1,68 @@ +package de.danoeh.antennapod.ui.screen.home.sections; + +import android.Manifest; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.provider.Settings; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Toast; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.databinding.HomeSectionNotificationBinding; +import de.danoeh.antennapod.ui.screen.home.HomeFragment; + +public class AllowNotificationsSection extends Fragment { + HomeSectionNotificationBinding viewBinding; + + private final ActivityResultLauncher requestPermissionLauncher = + registerForActivityResult(new ActivityResultContracts.RequestPermission(), isGranted -> { + if (isGranted) { + ((MainActivity) getActivity()).loadFragment(HomeFragment.TAG, null); + } else { + viewBinding.openSettingsButton.setVisibility(View.VISIBLE); + viewBinding.allowButton.setVisibility(View.GONE); + Toast.makeText(getContext(), R.string.notification_permission_denied, Toast.LENGTH_LONG).show(); + } + }); + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, + @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + viewBinding = HomeSectionNotificationBinding.inflate(inflater); + viewBinding.allowButton.setOnClickListener(v -> { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS); + } + }); + viewBinding.openSettingsButton.setOnClickListener(view -> { + Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); + Uri uri = Uri.fromParts("package", getContext().getPackageName(), null); + intent.setData(uri); + startActivity(intent); + }); + viewBinding.denyButton.setOnClickListener(v -> { + MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(getContext()); + builder.setMessage(R.string.notification_permission_deny_warning); + builder.setPositiveButton(R.string.deny_label, (dialog, which) -> { + getContext().getSharedPreferences(HomeFragment.PREF_NAME, Context.MODE_PRIVATE) + .edit().putBoolean(HomeFragment.PREF_DISABLE_NOTIFICATION_PERMISSION_NAG, true).apply(); + ((MainActivity) getActivity()).loadFragment(HomeFragment.TAG, null); + }); + builder.setNegativeButton(R.string.cancel_label, null); + builder.show(); + }); + return viewBinding.getRoot(); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/screen/home/sections/DownloadsSection.java b/app/src/main/java/de/danoeh/antennapod/ui/screen/home/sections/DownloadsSection.java new file mode 100644 index 000000000..4dc34a80e --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/screen/home/sections/DownloadsSection.java @@ -0,0 +1,140 @@ +package de.danoeh.antennapod.ui.screen.home.sections; + +import android.os.Bundle; +import android.util.Log; +import android.view.ContextMenu; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.ui.episodeslist.EpisodeItemListAdapter; +import de.danoeh.antennapod.event.DownloadLogEvent; +import de.danoeh.antennapod.ui.MenuItemUtils; +import de.danoeh.antennapod.storage.database.DBReader; +import de.danoeh.antennapod.event.FeedItemEvent; +import de.danoeh.antennapod.event.PlayerStatusEvent; +import de.danoeh.antennapod.event.playback.PlaybackPositionEvent; +import de.danoeh.antennapod.ui.screen.download.CompletedDownloadsFragment; +import de.danoeh.antennapod.ui.swipeactions.SwipeActions; +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedItemFilter; +import de.danoeh.antennapod.model.feed.SortOrder; +import de.danoeh.antennapod.storage.preferences.UserPreferences; +import de.danoeh.antennapod.ui.screen.home.HomeSection; +import de.danoeh.antennapod.ui.episodeslist.EpisodeItemViewHolder; +import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; +import io.reactivex.schedulers.Schedulers; +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; + +import java.util.List; + +public class DownloadsSection extends HomeSection { + public static final String TAG = "DownloadsSection"; + private static final int NUM_EPISODES = 2; + private EpisodeItemListAdapter adapter; + private List items; + private Disposable disposable; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, + @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + final View view = super.onCreateView(inflater, container, savedInstanceState); + viewBinding.recyclerView.setPadding(0, 0, 0, 0); + viewBinding.recyclerView.setOverScrollMode(RecyclerView.OVER_SCROLL_NEVER); + viewBinding.recyclerView.setLayoutManager(new LinearLayoutManager(getContext(), RecyclerView.VERTICAL, false)); + viewBinding.recyclerView.setRecycledViewPool(((MainActivity) requireActivity()).getRecycledViewPool()); + adapter = new EpisodeItemListAdapter((MainActivity) requireActivity()) { + @Override + public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { + super.onCreateContextMenu(menu, v, menuInfo); + MenuItemUtils.setOnClickListeners(menu, DownloadsSection.this::onContextItemSelected); + } + }; + adapter.setDummyViews(NUM_EPISODES); + viewBinding.recyclerView.setAdapter(adapter); + + SwipeActions swipeActions = new SwipeActions(this, CompletedDownloadsFragment.TAG); + swipeActions.attachTo(viewBinding.recyclerView); + swipeActions.setFilter(new FeedItemFilter(FeedItemFilter.DOWNLOADED)); + return view; + } + + @Override + public void onStart() { + super.onStart(); + loadItems(); + } + + @Override + protected void handleMoreClick() { + ((MainActivity) requireActivity()).loadChildFragment(new CompletedDownloadsFragment()); + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onEventMainThread(FeedItemEvent event) { + loadItems(); + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onEventMainThread(PlaybackPositionEvent event) { + if (adapter == null) { + return; + } + for (int i = 0; i < adapter.getItemCount(); i++) { + EpisodeItemViewHolder holder = (EpisodeItemViewHolder) + viewBinding.recyclerView.findViewHolderForAdapterPosition(i); + if (holder != null && holder.isCurrentlyPlayingItem()) { + holder.notifyPlaybackPositionUpdated(event); + break; + } + } + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onDownloadLogChanged(DownloadLogEvent event) { + loadItems(); + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onPlayerStatusChanged(PlayerStatusEvent event) { + loadItems(); + } + + @Override + protected String getSectionTitle() { + return getString(R.string.home_downloads_title); + } + + @Override + protected String getMoreLinkTitle() { + return getString(R.string.downloads_label); + } + + private void loadItems() { + if (disposable != null) { + disposable.dispose(); + } + SortOrder sortOrder = UserPreferences.getDownloadsSortedOrder(); + disposable = Observable.fromCallable(() -> DBReader.getEpisodes(0, Integer.MAX_VALUE, + new FeedItemFilter(FeedItemFilter.DOWNLOADED), sortOrder)) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(downloads -> { + if (downloads.size() > NUM_EPISODES) { + downloads = downloads.subList(0, NUM_EPISODES); + } + items = downloads; + adapter.setDummyViews(0); + adapter.updateItems(items); + }, error -> Log.e(TAG, Log.getStackTraceString(error))); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/screen/home/sections/EchoSection.java b/app/src/main/java/de/danoeh/antennapod/ui/screen/home/sections/EchoSection.java new file mode 100644 index 000000000..d8df470f0 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/screen/home/sections/EchoSection.java @@ -0,0 +1,71 @@ +package de.danoeh.antennapod.ui.screen.home.sections; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.databinding.HomeSectionEchoBinding; +import de.danoeh.antennapod.storage.database.DBReader; +import de.danoeh.antennapod.storage.database.StatisticsItem; +import de.danoeh.antennapod.ui.echo.EchoActivity; +import de.danoeh.antennapod.ui.echo.EchoConfig; +import de.danoeh.antennapod.ui.screen.home.HomeFragment; +import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; +import io.reactivex.schedulers.Schedulers; + +public class EchoSection extends Fragment { + private HomeSectionEchoBinding viewBinding; + private Disposable disposable; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, + @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + viewBinding = HomeSectionEchoBinding.inflate(inflater); + viewBinding.titleLabel.setText(getString(R.string.antennapod_echo_year, EchoConfig.RELEASE_YEAR)); + viewBinding.echoButton.setOnClickListener(v -> startActivity(new Intent(getContext(), EchoActivity.class))); + viewBinding.closeButton.setOnClickListener(v -> hideThisYear()); + updateVisibility(); + return viewBinding.getRoot(); + } + + private void updateVisibility() { + if (disposable != null) { + disposable.dispose(); + } + disposable = Observable.fromCallable( + () -> { + DBReader.StatisticsResult statisticsResult = DBReader.getStatistics( + false, EchoConfig.jan1(), Long.MAX_VALUE); + long totalTime = 0; + for (StatisticsItem feedTime : statisticsResult.feedTime) { + totalTime += feedTime.timePlayed; + } + return totalTime; + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(totalTime -> { + boolean shouldShow = (totalTime >= 3600 * 10); + viewBinding.getRoot().setVisibility(shouldShow ? View.VISIBLE : View.GONE); + if (!shouldShow) { + hideThisYear(); + } + }, Throwable::printStackTrace); + } + + void hideThisYear() { + getContext().getSharedPreferences(HomeFragment.PREF_NAME, Context.MODE_PRIVATE) + .edit().putInt(HomeFragment.PREF_HIDE_ECHO, EchoConfig.RELEASE_YEAR).apply(); + ((MainActivity) getActivity()).loadFragment(HomeFragment.TAG, null); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/screen/home/sections/EpisodesSurpriseSection.java b/app/src/main/java/de/danoeh/antennapod/ui/screen/home/sections/EpisodesSurpriseSection.java new file mode 100644 index 000000000..451a0aedc --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/screen/home/sections/EpisodesSurpriseSection.java @@ -0,0 +1,155 @@ +package de.danoeh.antennapod.ui.screen.home.sections; + +import android.os.Bundle; +import android.util.Log; +import android.view.ContextMenu; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.ui.episodeslist.HorizontalItemListAdapter; +import de.danoeh.antennapod.ui.MenuItemUtils; +import de.danoeh.antennapod.storage.database.DBReader; +import de.danoeh.antennapod.core.util.FeedItemUtil; +import de.danoeh.antennapod.event.EpisodeDownloadEvent; +import de.danoeh.antennapod.event.FeedItemEvent; +import de.danoeh.antennapod.event.PlayerStatusEvent; +import de.danoeh.antennapod.event.playback.PlaybackPositionEvent; +import de.danoeh.antennapod.ui.screen.AllEpisodesFragment; +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.ui.screen.home.HomeSection; +import de.danoeh.antennapod.ui.episodeslist.HorizontalItemViewHolder; +import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; +import io.reactivex.schedulers.Schedulers; +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +public class EpisodesSurpriseSection extends HomeSection { + public static final String TAG = "EpisodesSurpriseSection"; + private static final int NUM_EPISODES = 8; + private static int seed = 0; + private HorizontalItemListAdapter listAdapter; + private Disposable disposable; + private List episodes = new ArrayList<>(); + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, + @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + final View view = super.onCreateView(inflater, container, savedInstanceState); + viewBinding.shuffleButton.setVisibility(View.VISIBLE); + viewBinding.shuffleButton.setOnClickListener(v -> { + seed = new Random().nextInt(); + viewBinding.recyclerView.scrollToPosition(0); + loadItems(); + }); + listAdapter = new HorizontalItemListAdapter((MainActivity) getActivity()) { + @Override + public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { + super.onCreateContextMenu(menu, v, menuInfo); + MenuItemUtils.setOnClickListeners(menu, EpisodesSurpriseSection.this::onContextItemSelected); + } + }; + listAdapter.setDummyViews(NUM_EPISODES); + viewBinding.recyclerView.setLayoutManager( + new LinearLayoutManager(getContext(), RecyclerView.HORIZONTAL, false)); + viewBinding.recyclerView.setAdapter(listAdapter); + int paddingHorizontal = (int) (12 * getResources().getDisplayMetrics().density); + viewBinding.recyclerView.setPadding(paddingHorizontal, 0, paddingHorizontal, 0); + if (seed == 0) { + seed = new Random().nextInt(); + } + return view; + } + + @Override + public void onStart() { + super.onStart(); + loadItems(); + } + + @Override + protected void handleMoreClick() { + ((MainActivity) requireActivity()).loadChildFragment(new AllEpisodesFragment()); + } + + @Override + protected String getSectionTitle() { + return getString(R.string.home_surprise_title); + } + + @Override + protected String getMoreLinkTitle() { + return getString(R.string.episodes_label); + } + + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onPlayerStatusChanged(PlayerStatusEvent event) { + loadItems(); + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onEventMainThread(FeedItemEvent event) { + Log.d(TAG, "onEventMainThread() called with: " + "event = [" + event + "]"); + for (int i = 0, size = event.items.size(); i < size; i++) { + FeedItem item = event.items.get(i); + int pos = FeedItemUtil.indexOfItemWithId(episodes, item.getId()); + if (pos >= 0) { + episodes.remove(pos); + episodes.add(pos, item); + listAdapter.notifyItemChangedCompat(pos); + } + } + } + + @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) + public void onEventMainThread(EpisodeDownloadEvent event) { + for (String downloadUrl : event.getUrls()) { + int pos = FeedItemUtil.indexOfItemWithDownloadUrl(episodes, downloadUrl); + if (pos >= 0) { + listAdapter.notifyItemChangedCompat(pos); + } + } + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onEventMainThread(PlaybackPositionEvent event) { + if (listAdapter == null) { + return; + } + for (int i = 0; i < listAdapter.getItemCount(); i++) { + HorizontalItemViewHolder holder = (HorizontalItemViewHolder) + viewBinding.recyclerView.findViewHolderForAdapterPosition(i); + if (holder != null && holder.isCurrentlyPlayingItem()) { + holder.notifyPlaybackPositionUpdated(event); + break; + } + } + } + + private void loadItems() { + if (disposable != null) { + disposable.dispose(); + } + disposable = Observable.fromCallable(() -> DBReader.getRandomEpisodes(NUM_EPISODES, seed)) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(episodes -> { + this.episodes = episodes; + listAdapter.setDummyViews(0); + listAdapter.updateData(episodes); + }, error -> Log.e(TAG, Log.getStackTraceString(error))); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/screen/home/sections/InboxSection.java b/app/src/main/java/de/danoeh/antennapod/ui/screen/home/sections/InboxSection.java new file mode 100644 index 000000000..de6f6bef4 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/screen/home/sections/InboxSection.java @@ -0,0 +1,141 @@ +package de.danoeh.antennapod.ui.screen.home.sections; + +import android.os.Bundle; +import android.util.Log; +import android.view.ContextMenu; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.util.Pair; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.ui.episodeslist.EpisodeItemListAdapter; +import de.danoeh.antennapod.ui.MenuItemUtils; +import de.danoeh.antennapod.storage.database.DBReader; +import de.danoeh.antennapod.core.util.FeedItemUtil; +import de.danoeh.antennapod.event.EpisodeDownloadEvent; +import de.danoeh.antennapod.event.FeedItemEvent; +import de.danoeh.antennapod.event.FeedListUpdateEvent; +import de.danoeh.antennapod.event.UnreadItemsUpdateEvent; +import de.danoeh.antennapod.ui.screen.InboxFragment; +import de.danoeh.antennapod.ui.swipeactions.SwipeActions; +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedItemFilter; +import de.danoeh.antennapod.storage.preferences.UserPreferences; +import de.danoeh.antennapod.ui.screen.home.HomeSection; +import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; +import io.reactivex.schedulers.Schedulers; +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +public class InboxSection extends HomeSection { + public static final String TAG = "InboxSection"; + private static final int NUM_EPISODES = 2; + private EpisodeItemListAdapter adapter; + private List items = new ArrayList<>(); + private Disposable disposable; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, + @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + final View view = super.onCreateView(inflater, container, savedInstanceState); + viewBinding.recyclerView.setPadding(0, 0, 0, 0); + viewBinding.recyclerView.setOverScrollMode(RecyclerView.OVER_SCROLL_NEVER); + viewBinding.recyclerView.setLayoutManager(new LinearLayoutManager(getContext(), RecyclerView.VERTICAL, false)); + viewBinding.recyclerView.setRecycledViewPool(((MainActivity) requireActivity()).getRecycledViewPool()); + adapter = new EpisodeItemListAdapter((MainActivity) requireActivity()) { + @Override + public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { + super.onCreateContextMenu(menu, v, menuInfo); + MenuItemUtils.setOnClickListeners(menu, InboxSection.this::onContextItemSelected); + } + }; + adapter.setDummyViews(NUM_EPISODES); + viewBinding.recyclerView.setAdapter(adapter); + + SwipeActions swipeActions = new SwipeActions(this, InboxFragment.TAG); + swipeActions.attachTo(viewBinding.recyclerView); + swipeActions.setFilter(new FeedItemFilter(FeedItemFilter.NEW)); + return view; + } + + @Override + public void onStart() { + super.onStart(); + loadItems(); + } + + @Override + protected void handleMoreClick() { + ((MainActivity) requireActivity()).loadChildFragment(new InboxFragment()); + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onUnreadItemsChanged(UnreadItemsUpdateEvent event) { + loadItems(); + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onEventMainThread(FeedItemEvent event) { + loadItems(); + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onFeedListChanged(FeedListUpdateEvent event) { + loadItems(); + } + + @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) + public void onEventMainThread(EpisodeDownloadEvent event) { + for (String downloadUrl : event.getUrls()) { + int pos = FeedItemUtil.indexOfItemWithDownloadUrl(items, downloadUrl); + if (pos >= 0) { + adapter.notifyItemChangedCompat(pos); + } + } + } + + @Override + protected String getSectionTitle() { + return getString(R.string.home_new_title); + } + + @Override + protected String getMoreLinkTitle() { + return getString(R.string.inbox_label); + } + + private void loadItems() { + if (disposable != null) { + disposable.dispose(); + } + disposable = Observable.fromCallable(() -> + new Pair<>(DBReader.getEpisodes(0, NUM_EPISODES, + new FeedItemFilter(FeedItemFilter.NEW), UserPreferences.getInboxSortedOrder()), + DBReader.getTotalEpisodeCount(new FeedItemFilter(FeedItemFilter.NEW)))) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(data -> { + items = data.first; + adapter.setDummyViews(0); + adapter.updateItems(items); + viewBinding.numNewItemsLabel.setVisibility(View.VISIBLE); + if (data.second >= 100) { + viewBinding.numNewItemsLabel.setText(String.format(Locale.getDefault(), "%d+", 99)); + } else { + viewBinding.numNewItemsLabel.setText(String.format(Locale.getDefault(), "%d", data.second)); + } + }, error -> Log.e(TAG, Log.getStackTraceString(error))); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/screen/home/sections/QueueSection.java b/app/src/main/java/de/danoeh/antennapod/ui/screen/home/sections/QueueSection.java new file mode 100644 index 000000000..57d896ae9 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/screen/home/sections/QueueSection.java @@ -0,0 +1,163 @@ +package de.danoeh.antennapod.ui.screen.home.sections; + +import android.os.Bundle; +import android.util.Log; +import android.view.ContextMenu; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.ui.episodeslist.HorizontalItemListAdapter; +import de.danoeh.antennapod.ui.MenuItemUtils; +import de.danoeh.antennapod.storage.database.DBReader; +import de.danoeh.antennapod.core.util.FeedItemUtil; +import de.danoeh.antennapod.event.EpisodeDownloadEvent; +import de.danoeh.antennapod.event.FeedItemEvent; +import de.danoeh.antennapod.event.PlayerStatusEvent; +import de.danoeh.antennapod.event.QueueEvent; +import de.danoeh.antennapod.event.playback.PlaybackPositionEvent; +import de.danoeh.antennapod.ui.screen.queue.QueueFragment; +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.ui.screen.home.HomeSection; +import de.danoeh.antennapod.ui.episodeslist.HorizontalItemViewHolder; +import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; +import io.reactivex.schedulers.Schedulers; +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; + +import java.util.ArrayList; +import java.util.List; + +public class QueueSection extends HomeSection { + public static final String TAG = "QueueSection"; + private static final int NUM_EPISODES = 8; + private HorizontalItemListAdapter listAdapter; + private Disposable disposable; + private List queue = new ArrayList<>(); + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, + @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + final View view = super.onCreateView(inflater, container, savedInstanceState); + listAdapter = new HorizontalItemListAdapter((MainActivity) getActivity()) { + @Override + public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { + super.onCreateContextMenu(menu, v, menuInfo); + MenuItemUtils.setOnClickListeners(menu, QueueSection.this::onContextItemSelected); + } + }; + listAdapter.setDummyViews(NUM_EPISODES); + viewBinding.recyclerView.setLayoutManager( + new LinearLayoutManager(getContext(), RecyclerView.HORIZONTAL, false)); + viewBinding.recyclerView.setAdapter(listAdapter); + int paddingHorizontal = (int) (12 * getResources().getDisplayMetrics().density); + viewBinding.recyclerView.setPadding(paddingHorizontal, 0, paddingHorizontal, 0); + return view; + } + + @Override + public void onStart() { + super.onStart(); + loadItems(); + } + + @Override + protected void handleMoreClick() { + ((MainActivity) requireActivity()).loadChildFragment(new QueueFragment()); + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onQueueChanged(QueueEvent event) { + loadItems(); + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onPlayerStatusChanged(PlayerStatusEvent event) { + loadItems(); + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onEventMainThread(FeedItemEvent event) { + Log.d(TAG, "onEventMainThread() called with: " + "event = [" + event + "]"); + if (queue == null) { + return; + } + for (int i = 0, size = event.items.size(); i < size; i++) { + FeedItem item = event.items.get(i); + int pos = FeedItemUtil.indexOfItemWithId(queue, item.getId()); + if (pos >= 0) { + queue.remove(pos); + queue.add(pos, item); + listAdapter.notifyItemChangedCompat(pos); + } + } + } + + @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) + public void onEventMainThread(EpisodeDownloadEvent event) { + for (String downloadUrl : event.getUrls()) { + int pos = FeedItemUtil.indexOfItemWithDownloadUrl(queue, downloadUrl); + if (pos >= 0) { + listAdapter.notifyItemChangedCompat(pos); + } + } + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onEventMainThread(PlaybackPositionEvent event) { + if (listAdapter == null) { + return; + } + boolean foundCurrentlyPlayingItem = false; + boolean currentlyPlayingItemIsFirst = true; + for (int i = 0; i < listAdapter.getItemCount(); i++) { + HorizontalItemViewHolder holder = (HorizontalItemViewHolder) + viewBinding.recyclerView.findViewHolderForAdapterPosition(i); + if (holder == null) { + continue; + } + if (holder.isCurrentlyPlayingItem()) { + holder.notifyPlaybackPositionUpdated(event); + foundCurrentlyPlayingItem = true; + currentlyPlayingItemIsFirst = (i == 0); + break; + } + } + if (!foundCurrentlyPlayingItem || !currentlyPlayingItemIsFirst) { + loadItems(); + } + } + + @Override + protected String getSectionTitle() { + return getString(R.string.home_continue_title); + } + + @Override + protected String getMoreLinkTitle() { + return getString(R.string.queue_label); + } + + private void loadItems() { + if (disposable != null) { + disposable.dispose(); + } + disposable = Observable.fromCallable(() -> DBReader.getPausedQueue(NUM_EPISODES)) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(queue -> { + this.queue = queue; + listAdapter.setDummyViews(0); + listAdapter.updateData(queue); + }, error -> Log.e(TAG, Log.getStackTraceString(error))); + + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/screen/home/sections/SubscriptionsSection.java b/app/src/main/java/de/danoeh/antennapod/ui/screen/home/sections/SubscriptionsSection.java new file mode 100644 index 000000000..a37d1367f --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/screen/home/sections/SubscriptionsSection.java @@ -0,0 +1,111 @@ +package de.danoeh.antennapod.ui.screen.home.sections; + +import android.content.Context; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.util.Log; +import android.view.ContextMenu; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.ui.screen.subscriptions.HorizontalFeedListAdapter; +import de.danoeh.antennapod.ui.MenuItemUtils; +import de.danoeh.antennapod.storage.database.DBReader; +import de.danoeh.antennapod.event.FeedListUpdateEvent; +import de.danoeh.antennapod.ui.screen.subscriptions.SubscriptionFragment; +import de.danoeh.antennapod.model.feed.Feed; +import de.danoeh.antennapod.ui.screen.home.HomeSection; +import de.danoeh.antennapod.ui.statistics.StatisticsFragment; +import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; +import io.reactivex.schedulers.Schedulers; +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class SubscriptionsSection extends HomeSection { + public static final String TAG = "SubscriptionsSection"; + private static final int NUM_FEEDS = 8; + private HorizontalFeedListAdapter listAdapter; + private Disposable disposable; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, + @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + final View view = super.onCreateView(inflater, container, savedInstanceState); + viewBinding.recyclerView.setLayoutManager( + new LinearLayoutManager(getActivity(), RecyclerView.HORIZONTAL, false)); + listAdapter = new HorizontalFeedListAdapter((MainActivity) getActivity()) { + @Override + public void onCreateContextMenu(ContextMenu contextMenu, View view, + ContextMenu.ContextMenuInfo contextMenuInfo) { + super.onCreateContextMenu(contextMenu, view, contextMenuInfo); + MenuItemUtils.setOnClickListeners(contextMenu, SubscriptionsSection.this::onContextItemSelected); + } + }; + listAdapter.setDummyViews(NUM_FEEDS); + viewBinding.recyclerView.setAdapter(listAdapter); + int paddingHorizontal = (int) (12 * getResources().getDisplayMetrics().density); + viewBinding.recyclerView.setPadding(paddingHorizontal, 0, paddingHorizontal, 0); + return view; + } + + @Override + public void onStart() { + super.onStart(); + loadItems(); + } + + @Override + protected void handleMoreClick() { + ((MainActivity) requireActivity()).loadChildFragment(new SubscriptionFragment()); + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onFeedListChanged(FeedListUpdateEvent event) { + loadItems(); + } + + @Override + protected String getSectionTitle() { + return getString(R.string.home_classics_title); + } + + @Override + protected String getMoreLinkTitle() { + return getString(R.string.subscriptions_label); + } + + private void loadItems() { + if (disposable != null) { + disposable.dispose(); + } + SharedPreferences prefs = getContext().getSharedPreferences(StatisticsFragment.PREF_NAME, Context.MODE_PRIVATE); + boolean includeMarkedAsPlayed = prefs.getBoolean(StatisticsFragment.PREF_INCLUDE_MARKED_PLAYED, false); + disposable = Observable.fromCallable(() -> + DBReader.getStatistics(includeMarkedAsPlayed, 0, Long.MAX_VALUE).feedTime) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(statisticsData -> { + Collections.sort(statisticsData, (item1, item2) -> + Long.compare(item2.timePlayed, item1.timePlayed)); + List feeds = new ArrayList<>(); + for (int i = 0; i < statisticsData.size() && i < NUM_FEEDS; i++) { + feeds.add(statisticsData.get(i).feed); + } + listAdapter.setDummyViews(0); + listAdapter.updateData(feeds); + }, error -> Log.e(TAG, Log.getStackTraceString(error))); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/screen/onlinefeedview/FeedDiscoverer.java b/app/src/main/java/de/danoeh/antennapod/ui/screen/onlinefeedview/FeedDiscoverer.java new file mode 100644 index 000000000..1109c86d8 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/screen/onlinefeedview/FeedDiscoverer.java @@ -0,0 +1,79 @@ +package de.danoeh.antennapod.ui.screen.onlinefeedview; + +import android.net.Uri; +import androidx.collection.ArrayMap; +import android.text.TextUtils; + +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.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), 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) { + return findLinks(Jsoup.parse(in), baseUrl); + } + + private Map findLinks(Document document, String baseUrl) { + Map res = new ArrayMap<>(); + Elements links = document.head().getElementsByTag("link"); + for (Element link : links) { + String rel = link.attr("rel"); + String href = link.attr("href"); + if (!TextUtils.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, + (TextUtils.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/ui/screen/onlinefeedview/FeedItemlistDescriptionAdapter.java b/app/src/main/java/de/danoeh/antennapod/ui/screen/onlinefeedview/FeedItemlistDescriptionAdapter.java new file mode 100644 index 000000000..f46d786cf --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/screen/onlinefeedview/FeedItemlistDescriptionAdapter.java @@ -0,0 +1,112 @@ +package de.danoeh.antennapod.ui.screen.onlinefeedview; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.model.playback.MediaType; +import de.danoeh.antennapod.net.common.NetworkUtils; +import de.danoeh.antennapod.model.playback.RemoteMedia; +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.playback.service.PlaybackService; +import de.danoeh.antennapod.playback.service.PlaybackServiceStarter; +import de.danoeh.antennapod.ui.common.DateFormatter; +import de.danoeh.antennapod.model.playback.Playable; +import de.danoeh.antennapod.ui.cleaner.HtmlToPlainText; +import de.danoeh.antennapod.ui.StreamingConfirmationDialog; + +import java.util.List; + +/** + * List adapter for showing a list of FeedItems with their title and description. + */ +public class FeedItemlistDescriptionAdapter extends ArrayAdapter { + private static final int MAX_LINES_COLLAPSED = 2; + + public FeedItemlistDescriptionAdapter(Context context, int resource, List objects) { + super(context, resource, objects); + } + + @NonNull + @Override + public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { + Holder holder; + + FeedItem item = getItem(position); + + // Inflate layout + if (convertView == null) { + holder = new Holder(); + LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE); + convertView = inflater.inflate(R.layout.itemdescription_listitem, parent, false); + holder.title = convertView.findViewById(R.id.txtvTitle); + holder.pubDate = convertView.findViewById(R.id.txtvPubDate); + holder.description = convertView.findViewById(R.id.txtvDescription); + holder.preview = convertView.findViewById(R.id.butPreview); + + convertView.setTag(holder); + } else { + holder = (Holder) convertView.getTag(); + } + + holder.title.setText(item.getTitle()); + holder.pubDate.setText(DateFormatter.formatAbbrev(getContext(), item.getPubDate())); + if (item.getDescription() != null) { + String description = HtmlToPlainText.getPlainText(item.getDescription()) + .replaceAll("\n", " ") + .replaceAll("\\s+", " ") + .trim(); + holder.description.setText(description); + holder.description.setMaxLines(MAX_LINES_COLLAPSED); + } + holder.description.setTag(Boolean.FALSE); // not expanded + holder.preview.setVisibility(View.GONE); + holder.preview.setOnClickListener(v -> { + if (item.getMedia() == null) { + return; + } + Playable playable = new RemoteMedia(item); + if (!NetworkUtils.isStreamingAllowed()) { + new StreamingConfirmationDialog(getContext(), playable).show(); + return; + } + + new PlaybackServiceStarter(getContext(), playable) + .callEvenIfRunning(true) + .start(); + + if (playable.getMediaType() == MediaType.VIDEO) { + getContext().startActivity(PlaybackService.getPlayerActivityIntent(getContext(), playable)); + } + }); + convertView.setOnClickListener(v -> { + if (holder.description.getTag() == Boolean.TRUE) { + holder.description.setMaxLines(MAX_LINES_COLLAPSED); + holder.preview.setVisibility(View.GONE); + holder.description.setTag(Boolean.FALSE); + } else { + holder.description.setMaxLines(30); + holder.description.setTag(Boolean.TRUE); + + holder.preview.setVisibility(item.getMedia() != null ? View.VISIBLE : View.GONE); + holder.preview.setText(R.string.preview_episode); + } + }); + return convertView; + } + + static class Holder { + TextView title; + TextView pubDate; + TextView description; + Button preview; + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/screen/onlinefeedview/OnlineFeedViewActivity.java b/app/src/main/java/de/danoeh/antennapod/ui/screen/onlinefeedview/OnlineFeedViewActivity.java new file mode 100644 index 000000000..42050e718 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/screen/onlinefeedview/OnlineFeedViewActivity.java @@ -0,0 +1,711 @@ +package de.danoeh.antennapod.ui.screen.onlinefeedview; + +import android.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.SharedPreferences; +import android.graphics.LightingColorFilter; +import android.os.Bundle; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.TextUtils; +import android.text.style.ForegroundColorSpan; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.Toast; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.UiThread; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import androidx.appcompat.app.AppCompatActivity; +import com.bumptech.glide.Glide; +import com.bumptech.glide.request.RequestOptions; +import com.google.android.material.snackbar.Snackbar; + +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.net.download.service.feed.remote.Downloader; +import de.danoeh.antennapod.net.download.service.feed.remote.HttpDownloader; +import de.danoeh.antennapod.ui.appstartintent.MainActivityStarter; +import de.danoeh.antennapod.ui.common.ThemeSwitcher; +import de.danoeh.antennapod.net.download.serviceinterface.DownloadRequestCreator; +import de.danoeh.antennapod.net.discovery.FeedUrlNotFoundException; +import de.danoeh.antennapod.storage.database.FeedDatabaseWriter; +import de.danoeh.antennapod.playback.service.PlaybackServiceInterface; +import de.danoeh.antennapod.ui.screen.download.DownloadErrorLabel; +import de.danoeh.antennapod.databinding.EditTextDialogBinding; +import de.danoeh.antennapod.databinding.OnlinefeedviewHeaderBinding; +import de.danoeh.antennapod.event.EpisodeDownloadEvent; +import de.danoeh.antennapod.event.FeedListUpdateEvent; +import de.danoeh.antennapod.event.PlayerStatusEvent; +import de.danoeh.antennapod.storage.preferences.PlaybackPreferences; +import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterface; +import de.danoeh.antennapod.storage.preferences.UserPreferences; +import de.danoeh.antennapod.model.download.DownloadRequest; +import de.danoeh.antennapod.model.download.DownloadResult; +import de.danoeh.antennapod.storage.database.DBReader; +import de.danoeh.antennapod.storage.database.DBWriter; +import de.danoeh.antennapod.net.discovery.CombinedSearcher; +import de.danoeh.antennapod.net.discovery.PodcastSearchResult; +import de.danoeh.antennapod.net.discovery.PodcastSearcherRegistry; +import de.danoeh.antennapod.parser.feed.FeedHandler; +import de.danoeh.antennapod.parser.feed.FeedHandlerResult; +import de.danoeh.antennapod.model.download.DownloadError; +import de.danoeh.antennapod.core.util.IntentUtils; +import de.danoeh.antennapod.net.common.UrlChecker; +import de.danoeh.antennapod.ui.cleaner.HtmlToPlainText; +import de.danoeh.antennapod.databinding.OnlinefeedviewActivityBinding; +import de.danoeh.antennapod.model.feed.Feed; +import de.danoeh.antennapod.model.feed.FeedPreferences; +import de.danoeh.antennapod.model.playback.RemoteMedia; +import de.danoeh.antennapod.parser.feed.UnsupportedFeedtypeException; +import de.danoeh.antennapod.ui.common.ThemeUtils; +import de.danoeh.antennapod.ui.glide.FastBlurTransformation; +import de.danoeh.antennapod.ui.preferences.screen.synchronization.AuthenticationDialog; +import io.reactivex.Maybe; +import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; +import io.reactivex.observers.DisposableMaybeObserver; +import io.reactivex.schedulers.Schedulers; +import org.apache.commons.lang3.StringUtils; +import org.greenrobot.eventbus.EventBus; +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static de.danoeh.antennapod.ui.appstartintent.OnlineFeedviewActivityStarter.ARG_FEEDURL; +import static de.danoeh.antennapod.ui.appstartintent.OnlineFeedviewActivityStarter.ARG_STARTED_FROM_SEARCH; +import static de.danoeh.antennapod.ui.appstartintent.OnlineFeedviewActivityStarter.ARG_WAS_MANUAL_URL; + +/** + * Downloads a feed from a feed URL and parses it. Subclasses can display the + * feed object that was parsed. This activity MUST be started with a given URL + * or an Exception will be thrown. + *

+ * If the feed cannot be downloaded or parsed, an error dialog will be displayed + * and the activity will finish as soon as the error dialog is closed. + */ +public class OnlineFeedViewActivity extends AppCompatActivity { + + private static final int RESULT_ERROR = 2; + private static final String TAG = "OnlineFeedViewActivity"; + private static final String PREFS = "OnlineFeedViewActivityPreferences"; + private static final String PREF_LAST_AUTO_DOWNLOAD = "lastAutoDownload"; + private static final int DESCRIPTION_MAX_LINES_COLLAPSED = 4; + + private volatile List feeds; + private String selectedDownloadUrl; + private Downloader downloader; + private String username = null; + private String password = null; + + private boolean isPaused; + private boolean didPressSubscribe = false; + private boolean isFeedFoundBySearch = false; + + private Dialog dialog; + + private Disposable download; + private Disposable parser; + private Disposable updater; + + private OnlinefeedviewHeaderBinding headerBinding; + private OnlinefeedviewActivityBinding viewBinding; + + @Override + protected void onCreate(Bundle savedInstanceState) { + setTheme(ThemeSwitcher.getTranslucentTheme(this)); + super.onCreate(savedInstanceState); + + viewBinding = OnlinefeedviewActivityBinding.inflate(getLayoutInflater()); + setContentView(viewBinding.getRoot()); + + viewBinding.transparentBackground.setOnClickListener(v -> finish()); + viewBinding.closeButton.setOnClickListener(view -> finish()); + viewBinding.card.setOnClickListener(null); + viewBinding.card.setCardBackgroundColor(ThemeUtils.getColorFromAttr(this, R.attr.colorSurface)); + headerBinding = OnlinefeedviewHeaderBinding.inflate(getLayoutInflater()); + + String feedUrl = null; + if (getIntent().hasExtra(ARG_FEEDURL)) { + feedUrl = getIntent().getStringExtra(ARG_FEEDURL); + } else if (TextUtils.equals(getIntent().getAction(), Intent.ACTION_SEND)) { + feedUrl = getIntent().getStringExtra(Intent.EXTRA_TEXT); + } else if (TextUtils.equals(getIntent().getAction(), Intent.ACTION_VIEW)) { + feedUrl = getIntent().getDataString(); + } + + if (feedUrl == null) { + Log.e(TAG, "feedUrl is null."); + showNoPodcastFoundError(); + } else { + Log.d(TAG, "Activity was started with url " + feedUrl); + setLoadingLayout(); + // Remove subscribeonandroid.com from feed URL in order to subscribe to the actual feed URL + if (feedUrl.contains("subscribeonandroid.com")) { + feedUrl = feedUrl.replaceFirst("((www.)?(subscribeonandroid.com/))", ""); + } + if (savedInstanceState != null) { + username = savedInstanceState.getString("username"); + password = savedInstanceState.getString("password"); + } + lookupUrlAndDownload(feedUrl); + } + } + + private void showNoPodcastFoundError() { + runOnUiThread(() -> new MaterialAlertDialogBuilder(OnlineFeedViewActivity.this) + .setNeutralButton(android.R.string.ok, (dialog, which) -> finish()) + .setTitle(R.string.error_label) + .setMessage(R.string.null_value_podcast_error) + .setOnDismissListener(dialog1 -> { + setResult(RESULT_ERROR); + finish(); + }) + .show()); + } + + /** + * Displays a progress indicator. + */ + private void setLoadingLayout() { + viewBinding.progressBar.setVisibility(View.VISIBLE); + viewBinding.feedDisplayContainer.setVisibility(View.GONE); + } + + @Override + protected void onStart() { + super.onStart(); + isPaused = false; + EventBus.getDefault().register(this); + } + + @Override + protected void onStop() { + super.onStop(); + isPaused = true; + EventBus.getDefault().unregister(this); + if (downloader != null && !downloader.isFinished()) { + downloader.cancel(); + } + if(dialog != null && dialog.isShowing()) { + dialog.dismiss(); + } + } + + @Override + public void onDestroy() { + super.onDestroy(); + if(updater != null) { + updater.dispose(); + } + if(download != null) { + download.dispose(); + } + if(parser != null) { + parser.dispose(); + } + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putString("username", username); + outState.putString("password", password); + } + + private void resetIntent(String url) { + Intent intent = new Intent(); + intent.putExtra(ARG_FEEDURL, url); + setIntent(intent); + } + + @Override + public void finish() { + super.finish(); + overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out); + } + + private void lookupUrlAndDownload(String url) { + download = PodcastSearcherRegistry.lookupUrl(url) + .subscribeOn(Schedulers.io()) + .observeOn(Schedulers.io()) + .subscribe(this::startFeedDownload, + error -> { + if (error instanceof FeedUrlNotFoundException) { + tryToRetrieveFeedUrlBySearch((FeedUrlNotFoundException) error); + } else { + showNoPodcastFoundError(); + Log.e(TAG, Log.getStackTraceString(error)); + } + }); + } + + private void tryToRetrieveFeedUrlBySearch(FeedUrlNotFoundException error) { + Log.d(TAG, "Unable to retrieve feed url, trying to retrieve feed url from search"); + String url = searchFeedUrlByTrackName(error.getTrackName(), error.getArtistName()); + if (url != null) { + Log.d(TAG, "Successfully retrieve feed url"); + isFeedFoundBySearch = true; + startFeedDownload(url); + } else { + showNoPodcastFoundError(); + Log.d(TAG, "Failed to retrieve feed url"); + } + } + + private String searchFeedUrlByTrackName(String trackName, String artistName) { + CombinedSearcher searcher = new CombinedSearcher(); + String query = trackName + " " + artistName; + List results = searcher.search(query).blockingGet(); + for (PodcastSearchResult result : results) { + if (result.feedUrl != null && result.author != null + && result.author.equalsIgnoreCase(artistName) && result.title.equalsIgnoreCase(trackName)) { + return result.feedUrl; + + } + } + return null; + } + + private void startFeedDownload(String url) { + Log.d(TAG, "Starting feed download"); + selectedDownloadUrl = UrlChecker.prepareUrl(url); + DownloadRequest request = DownloadRequestCreator.create(new Feed(selectedDownloadUrl, null)) + .withAuthentication(username, password) + .withInitiatedByUser(true) + .build(); + + download = Observable.fromCallable(() -> { + feeds = DBReader.getFeedList(); + downloader = new HttpDownloader(request); + downloader.call(); + return downloader.getResult(); + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(status -> checkDownloadResult(status, request.getDestination()), + error -> Log.e(TAG, Log.getStackTraceString(error))); + } + + private void checkDownloadResult(@NonNull DownloadResult status, String destination) { + if (status.isSuccessful()) { + parseFeed(destination); + } else if (status.getReason() == DownloadError.ERROR_UNAUTHORIZED) { + if (!isFinishing() && !isPaused) { + if (username != null && password != null) { + Toast.makeText(this, R.string.download_error_unauthorized, Toast.LENGTH_LONG).show(); + } + dialog = new FeedViewAuthenticationDialog(OnlineFeedViewActivity.this, + R.string.authentication_notification_title, + downloader.getDownloadRequest().getSource()).create(); + dialog.show(); + } + } else { + showErrorDialog(getString(DownloadErrorLabel.from(status.getReason())), status.getReasonDetailed()); + } + } + + @Subscribe + public void onFeedListChanged(FeedListUpdateEvent event) { + updater = Observable.fromCallable(DBReader::getFeedList) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + feeds -> { + OnlineFeedViewActivity.this.feeds = feeds; + handleUpdatedFeedStatus(); + }, error -> Log.e(TAG, Log.getStackTraceString(error)) + ); + } + + @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) + public void onEventMainThread(EpisodeDownloadEvent event) { + handleUpdatedFeedStatus(); + } + + private void parseFeed(String destination) { + Log.d(TAG, "Parsing feed"); + parser = Maybe.fromCallable(() -> doParseFeed(destination)) + .subscribeOn(Schedulers.computation()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeWith(new DisposableMaybeObserver() { + @Override + public void onSuccess(@NonNull FeedHandlerResult result) { + showFeedInformation(result.feed, result.alternateFeedUrls); + } + + @Override + public void onComplete() { + // Ignore null result: We showed the discovery dialog. + } + + @Override + public void onError(@NonNull Throwable error) { + showErrorDialog(error.getMessage(), ""); + Log.d(TAG, "Feed parser exception: " + Log.getStackTraceString(error)); + } + }); + } + + /** + * Try to parse the feed. + * @return The FeedHandlerResult if successful. + * Null if unsuccessful but we started another attempt. + * @throws Exception If unsuccessful but we do not know a resolution. + */ + @Nullable + private FeedHandlerResult doParseFeed(String destination) throws Exception { + FeedHandler handler = new FeedHandler(); + Feed feed = new Feed(selectedDownloadUrl, null); + feed.setLocalFileUrl(destination); + File destinationFile = new File(destination); + try { + return handler.parseFeed(feed); + } catch (UnsupportedFeedtypeException e) { + Log.d(TAG, "Unsupported feed type detected"); + if ("html".equalsIgnoreCase(e.getRootElement())) { + boolean dialogShown = showFeedDiscoveryDialog(destinationFile, selectedDownloadUrl); + if (dialogShown) { + return null; // Should not display an error message + } else { + throw new UnsupportedFeedtypeException(getString(R.string.download_error_unsupported_type_html)); + } + } else { + throw e; + } + } catch (Exception e) { + Log.e(TAG, Log.getStackTraceString(e)); + throw e; + } finally { + boolean rc = destinationFile.delete(); + Log.d(TAG, "Deleted feed source file. Result: " + rc); + } + } + + /** + * Called when feed parsed successfully. + * This method is executed on the GUI thread. + */ + private void showFeedInformation(final Feed feed, Map alternateFeedUrls) { + viewBinding.progressBar.setVisibility(View.GONE); + viewBinding.feedDisplayContainer.setVisibility(View.VISIBLE); + if (isFeedFoundBySearch) { + int resId = R.string.no_feed_url_podcast_found_by_search; + Snackbar.make(findViewById(android.R.id.content), resId, Snackbar.LENGTH_LONG).show(); + } + + viewBinding.backgroundImage.setColorFilter(new LightingColorFilter(0xff828282, 0x000000)); + + viewBinding.listView.addHeaderView(headerBinding.getRoot()); + viewBinding.listView.setSelector(android.R.color.transparent); + viewBinding.listView.setAdapter(new FeedItemlistDescriptionAdapter(this, 0, feed.getItems())); + + if (StringUtils.isNotBlank(feed.getImageUrl())) { + Glide.with(this) + .load(feed.getImageUrl()) + .apply(new RequestOptions() + .placeholder(R.color.light_gray) + .error(R.color.light_gray) + .fitCenter() + .dontAnimate()) + .into(viewBinding.coverImage); + Glide.with(this) + .load(feed.getImageUrl()) + .apply(new RequestOptions() + .placeholder(R.color.image_readability_tint) + .error(R.color.image_readability_tint) + .transform(new FastBlurTransformation()) + .dontAnimate()) + .into(viewBinding.backgroundImage); + } + + viewBinding.titleLabel.setText(feed.getTitle()); + viewBinding.authorLabel.setText(feed.getAuthor()); + headerBinding.txtvDescription.setText(HtmlToPlainText.getPlainText(feed.getDescription())); + + viewBinding.subscribeButton.setOnClickListener(v -> { + if (feedInFeedlist()) { + openFeed(); + } else { + FeedDatabaseWriter.updateFeed(this, feed, false); + didPressSubscribe = true; + handleUpdatedFeedStatus(); + } + }); + + viewBinding.stopPreviewButton.setOnClickListener(v -> { + PlaybackPreferences.writeNoMediaPlaying(); + IntentUtils.sendLocalBroadcast(this, PlaybackServiceInterface.ACTION_SHUTDOWN_PLAYBACK_SERVICE); + }); + + if (UserPreferences.isEnableAutodownload()) { + SharedPreferences preferences = getSharedPreferences(PREFS, MODE_PRIVATE); + viewBinding.autoDownloadCheckBox.setChecked(preferences.getBoolean(PREF_LAST_AUTO_DOWNLOAD, true)); + } + + headerBinding.txtvDescription.setMaxLines(DESCRIPTION_MAX_LINES_COLLAPSED); + headerBinding.txtvDescription.setOnClickListener(v -> { + if (headerBinding.txtvDescription.getMaxLines() > DESCRIPTION_MAX_LINES_COLLAPSED) { + headerBinding.txtvDescription.setMaxLines(DESCRIPTION_MAX_LINES_COLLAPSED); + } else { + headerBinding.txtvDescription.setMaxLines(2000); + } + }); + + if (alternateFeedUrls.isEmpty()) { + viewBinding.alternateUrlsSpinner.setVisibility(View.GONE); + } else { + viewBinding.alternateUrlsSpinner.setVisibility(View.VISIBLE); + + final List alternateUrlsList = new ArrayList<>(); + final List alternateUrlsTitleList = new ArrayList<>(); + + alternateUrlsList.add(feed.getDownloadUrl()); + alternateUrlsTitleList.add(feed.getTitle()); + + + alternateUrlsList.addAll(alternateFeedUrls.keySet()); + for (String url : alternateFeedUrls.keySet()) { + alternateUrlsTitleList.add(alternateFeedUrls.get(url)); + } + + ArrayAdapter adapter = new ArrayAdapter(this, + R.layout.alternate_urls_item, alternateUrlsTitleList) { + @Override + public View getDropDownView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { + // reusing the old view causes a visual bug on Android <= 10 + return super.getDropDownView(position, null, parent); + } + }; + + adapter.setDropDownViewResource(R.layout.alternate_urls_dropdown_item); + viewBinding.alternateUrlsSpinner.setAdapter(adapter); + viewBinding.alternateUrlsSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + selectedDownloadUrl = alternateUrlsList.get(position); + } + + @Override + public void onNothingSelected(AdapterView parent) { + + } + }); + } + handleUpdatedFeedStatus(); + } + + private void openFeed() { + // feed.getId() is always 0, we have to retrieve the id from the feed list from the database + MainActivityStarter mainActivityStarter = new MainActivityStarter(this); + mainActivityStarter.withOpenFeed(getFeedId()); + if (getIntent().getBooleanExtra(ARG_STARTED_FROM_SEARCH, false)) { + mainActivityStarter.withAddToBackStack(); + } + finish(); + startActivity(mainActivityStarter.getIntent()); + } + + private void handleUpdatedFeedStatus() { + if (DownloadServiceInterface.get().isDownloadingEpisode(selectedDownloadUrl)) { + viewBinding.subscribeButton.setEnabled(false); + viewBinding.subscribeButton.setText(R.string.subscribing_label); + } else if (feedInFeedlist()) { + viewBinding.subscribeButton.setEnabled(true); + viewBinding.subscribeButton.setText(R.string.open_podcast); + if (didPressSubscribe) { + didPressSubscribe = false; + + Feed feed1 = DBReader.getFeed(getFeedId()); + FeedPreferences feedPreferences = feed1.getPreferences(); + if (UserPreferences.isEnableAutodownload()) { + boolean autoDownload = viewBinding.autoDownloadCheckBox.isChecked(); + feedPreferences.setAutoDownload(autoDownload); + + SharedPreferences preferences = getSharedPreferences(PREFS, MODE_PRIVATE); + SharedPreferences.Editor editor = preferences.edit(); + editor.putBoolean(PREF_LAST_AUTO_DOWNLOAD, autoDownload); + editor.apply(); + } + if (username != null) { + feedPreferences.setUsername(username); + feedPreferences.setPassword(password); + } + DBWriter.setFeedPreferences(feedPreferences); + + openFeed(); + } + } else { + viewBinding.subscribeButton.setEnabled(true); + viewBinding.subscribeButton.setText(R.string.subscribe_label); + if (UserPreferences.isEnableAutodownload()) { + viewBinding.autoDownloadCheckBox.setVisibility(View.VISIBLE); + } + } + } + + private boolean feedInFeedlist() { + return getFeedId() != 0; + } + + private long getFeedId() { + if (feeds == null) { + return 0; + } + for (Feed f : feeds) { + if (f.getDownloadUrl().equals(selectedDownloadUrl)) { + return f.getId(); + } + } + return 0; + } + + @UiThread + private void showErrorDialog(String errorMsg, String details) { + if (!isFinishing() && !isPaused) { + MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this); + builder.setTitle(R.string.error_label); + if (errorMsg != null) { + String total = errorMsg + "\n\n" + details; + SpannableString errorMessage = new SpannableString(total); + errorMessage.setSpan(new ForegroundColorSpan(0x88888888), + errorMsg.length(), total.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + builder.setMessage(errorMessage); + } else { + builder.setMessage(R.string.download_error_error_unknown); + } + builder.setPositiveButton(android.R.string.ok, (dialog, which) -> dialog.cancel()); + if (getIntent().getBooleanExtra(ARG_WAS_MANUAL_URL, false)) { + builder.setNeutralButton(R.string.edit_url_menu, (dialog, which) -> editUrl()); + } + builder.setOnCancelListener(dialog -> { + setResult(RESULT_ERROR); + finish(); + }); + if (dialog != null && dialog.isShowing()) { + dialog.dismiss(); + } + dialog = builder.show(); + } + } + + private void editUrl() { + MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this); + builder.setTitle(R.string.edit_url_menu); + final EditTextDialogBinding dialogBinding = EditTextDialogBinding.inflate(getLayoutInflater()); + if (downloader != null) { + dialogBinding.urlEditText.setText(downloader.getDownloadRequest().getSource()); + } + builder.setView(dialogBinding.getRoot()); + builder.setPositiveButton(R.string.confirm_label, (dialog, which) -> { + setLoadingLayout(); + lookupUrlAndDownload(dialogBinding.urlEditText.getText().toString()); + }); + builder.setNegativeButton(R.string.cancel_label, (dialog1, which) -> dialog1.cancel()); + builder.setOnCancelListener(dialog1 -> { + setResult(RESULT_ERROR); + finish(); + }); + builder.show(); + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void playbackStateChanged(PlayerStatusEvent event) { + boolean isPlayingPreview = + PlaybackPreferences.getCurrentlyPlayingMediaType() == RemoteMedia.PLAYABLE_TYPE_REMOTE_MEDIA; + viewBinding.stopPreviewButton.setVisibility(isPlayingPreview ? View.VISIBLE : View.GONE); + } + + /** + * + * @return true if a FeedDiscoveryDialog is shown, false otherwise (e.g., due to no feed found). + */ + private boolean showFeedDiscoveryDialog(File feedFile, String baseUrl) { + FeedDiscoverer fd = new FeedDiscoverer(); + final Map urlsMap; + try { + urlsMap = fd.findLinks(feedFile, baseUrl); + if (urlsMap == null || urlsMap.isEmpty()) { + return false; + } + } catch (IOException e) { + e.printStackTrace(); + return false; + } + + if (isPaused || isFinishing()) { + return false; + } + + final List titles = new ArrayList<>(); + + final List urls = new ArrayList<>(urlsMap.keySet()); + for (String url : urls) { + titles.add(urlsMap.get(url)); + } + + if (urls.size() == 1) { + // Skip dialog and display the item directly + resetIntent(urls.get(0)); + startFeedDownload(urls.get(0)); + return true; + } + + final ArrayAdapter adapter = new ArrayAdapter<>(OnlineFeedViewActivity.this, + R.layout.ellipsize_start_listitem, R.id.txtvTitle, titles); + DialogInterface.OnClickListener onClickListener = (dialog, which) -> { + String selectedUrl = urls.get(which); + dialog.dismiss(); + resetIntent(selectedUrl); + startFeedDownload(selectedUrl); + }; + + MaterialAlertDialogBuilder ab = new MaterialAlertDialogBuilder(OnlineFeedViewActivity.this) + .setTitle(R.string.feeds_label) + .setCancelable(true) + .setOnCancelListener(dialog -> finish()) + .setAdapter(adapter, onClickListener); + + runOnUiThread(() -> { + if(dialog != null && dialog.isShowing()) { + dialog.dismiss(); + } + dialog = ab.show(); + }); + return true; + } + + private class FeedViewAuthenticationDialog extends AuthenticationDialog { + + private final String feedUrl; + + FeedViewAuthenticationDialog(Context context, int titleRes, String feedUrl) { + super(context, titleRes, true, username, password); + this.feedUrl = feedUrl; + } + + @Override + protected void onCancelled() { + super.onCancelled(); + finish(); + } + + @Override + protected void onConfirmed(String username, String password) { + OnlineFeedViewActivity.this.username = username; + OnlineFeedViewActivity.this.password = password; + startFeedDownload(feedUrl); + } + } + +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/screen/playback/MediaPlayerErrorDialog.java b/app/src/main/java/de/danoeh/antennapod/ui/screen/playback/MediaPlayerErrorDialog.java new file mode 100644 index 000000000..62dff00fd --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/screen/playback/MediaPlayerErrorDialog.java @@ -0,0 +1,31 @@ +package de.danoeh.antennapod.ui.screen.playback; + +import android.app.Activity; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.style.ForegroundColorSpan; +import com.google.android.material.bottomsheet.BottomSheetBehavior; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.event.PlayerErrorEvent; + +public class MediaPlayerErrorDialog { + public static void show(Activity activity, PlayerErrorEvent event) { + final MaterialAlertDialogBuilder errorDialog = new MaterialAlertDialogBuilder(activity); + errorDialog.setTitle(R.string.error_label); + + String genericMessage = activity.getString(R.string.playback_error_generic); + SpannableString errorMessage = new SpannableString(genericMessage + "\n\n" + event.getMessage()); + errorMessage.setSpan(new ForegroundColorSpan(0x88888888), + genericMessage.length(), errorMessage.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + + errorDialog.setMessage(errorMessage); + errorDialog.setPositiveButton(android.R.string.ok, (dialog, which) -> { + if (activity instanceof MainActivity) { + ((MainActivity) activity).getBottomSheet().setState(BottomSheetBehavior.STATE_COLLAPSED); + } + }); + errorDialog.create().show(); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/screen/playback/PlayButton.java b/app/src/main/java/de/danoeh/antennapod/ui/screen/playback/PlayButton.java new file mode 100644 index 000000000..fbd791e71 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/screen/playback/PlayButton.java @@ -0,0 +1,52 @@ +package de.danoeh.antennapod.ui.screen.playback; + +import android.content.Context; +import android.util.AttributeSet; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.widget.AppCompatImageButton; +import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat; +import de.danoeh.antennapod.R; + +public class PlayButton extends AppCompatImageButton { + private boolean isShowPlay = true; + private boolean isVideoScreen = false; + + public PlayButton(@NonNull Context context) { + super(context); + } + + public PlayButton(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + } + + public PlayButton(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public void setIsVideoScreen(boolean isVideoScreen) { + this.isVideoScreen = isVideoScreen; + } + + public void setIsShowPlay(boolean showPlay) { + if (this.isShowPlay != showPlay) { + this.isShowPlay = showPlay; + setContentDescription(getContext().getString(showPlay ? R.string.play_label : R.string.pause_label)); + if (isVideoScreen) { + setImageResource(showPlay ? R.drawable.ic_play_video_white : R.drawable.ic_pause_video_white); + } else if (!isShown()) { + setImageResource(showPlay ? R.drawable.ic_play_48dp : R.drawable.ic_pause); + } else if (showPlay) { + AnimatedVectorDrawableCompat drawable = AnimatedVectorDrawableCompat.create( + getContext(), R.drawable.ic_animate_pause_play); + setImageDrawable(drawable); + drawable.start(); + } else { + AnimatedVectorDrawableCompat drawable = AnimatedVectorDrawableCompat.create( + getContext(), R.drawable.ic_animate_play_pause); + setImageDrawable(drawable); + drawable.start(); + } + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/screen/playback/PlaybackControlsDialog.java b/app/src/main/java/de/danoeh/antennapod/ui/screen/playback/PlaybackControlsDialog.java new file mode 100644 index 000000000..b46c5e80c --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/screen/playback/PlaybackControlsDialog.java @@ -0,0 +1,78 @@ +package de.danoeh.antennapod.ui.screen.playback; + +import android.app.Dialog; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.view.View; +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import androidx.fragment.app.DialogFragment; +import android.widget.Button; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.playback.service.PlaybackController; + +import java.util.List; + +public class PlaybackControlsDialog extends DialogFragment { + private PlaybackController controller; + private AlertDialog dialog; + + public static PlaybackControlsDialog newInstance() { + Bundle arguments = new Bundle(); + PlaybackControlsDialog dialog = new PlaybackControlsDialog(); + dialog.setArguments(arguments); + return dialog; + } + + public PlaybackControlsDialog() { + // Empty constructor required for DialogFragment + } + + @Override + public void onStart() { + super.onStart(); + controller = new PlaybackController(getActivity()) { + @Override + public void loadMediaInfo() { + setupAudioTracks(); + } + }; + controller.init(); + } + + @Override + public void onStop() { + super.onStop(); + controller.release(); + controller = null; + } + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + dialog = new MaterialAlertDialogBuilder(getContext()) + .setTitle(R.string.audio_controls) + .setView(R.layout.audio_controls) + .setPositiveButton(R.string.close_label, null).create(); + return dialog; + } + + private void setupAudioTracks() { + List audioTracks = controller.getAudioTracks(); + int selectedAudioTrack = controller.getSelectedAudioTrack(); + final Button butAudioTracks = dialog.findViewById(R.id.audio_tracks); + if (audioTracks.size() < 2 || selectedAudioTrack < 0) { + butAudioTracks.setVisibility(View.GONE); + return; + } + + butAudioTracks.setVisibility(View.VISIBLE); + butAudioTracks.setText(audioTracks.get(selectedAudioTrack)); + butAudioTracks.setOnClickListener(v -> { + controller.setAudioTrack((selectedAudioTrack + 1) % audioTracks.size()); + new Handler(Looper.getMainLooper()).postDelayed(this::setupAudioTracks, 500); + }); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/screen/playback/PlaybackSpeedDialogActivity.java b/app/src/main/java/de/danoeh/antennapod/ui/screen/playback/PlaybackSpeedDialogActivity.java new file mode 100644 index 000000000..9d73e81a7 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/screen/playback/PlaybackSpeedDialogActivity.java @@ -0,0 +1,29 @@ +package de.danoeh.antennapod.ui.screen.playback; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AppCompatActivity; + +import android.content.DialogInterface; +import android.os.Bundle; + +import de.danoeh.antennapod.ui.common.ThemeSwitcher; +import de.danoeh.antennapod.ui.screen.playback.VariableSpeedDialog; + +public class PlaybackSpeedDialogActivity extends AppCompatActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + setTheme(ThemeSwitcher.getTranslucentTheme(this)); + super.onCreate(savedInstanceState); + VariableSpeedDialog speedDialog = new InnerVariableSpeedDialog(); + speedDialog.show(getSupportFragmentManager(), null); + } + + public static class InnerVariableSpeedDialog extends VariableSpeedDialog { + @Override + public void onDismiss(@NonNull DialogInterface dialog) { + super.onDismiss(dialog); + getActivity().finish(); + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/screen/playback/PlaybackSpeedSeekBar.java b/app/src/main/java/de/danoeh/antennapod/ui/screen/playback/PlaybackSpeedSeekBar.java new file mode 100644 index 000000000..c65d3656c --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/screen/playback/PlaybackSpeedSeekBar.java @@ -0,0 +1,76 @@ +package de.danoeh.antennapod.ui.screen.playback; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.SeekBar; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.util.Consumer; +import de.danoeh.antennapod.R; + +public class PlaybackSpeedSeekBar extends FrameLayout { + private SeekBar seekBar; + private Consumer progressChangedListener; + + public PlaybackSpeedSeekBar(@NonNull Context context) { + super(context); + setup(); + } + + public PlaybackSpeedSeekBar(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + setup(); + } + + public PlaybackSpeedSeekBar(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + setup(); + } + + private void setup() { + View.inflate(getContext(), R.layout.playback_speed_seek_bar, this); + seekBar = findViewById(R.id.playback_speed); + findViewById(R.id.butDecSpeed).setOnClickListener(v -> seekBar.setProgress(seekBar.getProgress() - 2)); + findViewById(R.id.butIncSpeed).setOnClickListener(v -> seekBar.setProgress(seekBar.getProgress() + 2)); + + seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + float playbackSpeed = (progress + 10) / 20.0f; + if (progressChangedListener != null) { + progressChangedListener.accept(playbackSpeed); + } + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + } + }); + } + + public void updateSpeed(float speedMultiplier) { + seekBar.setProgress(Math.round((20 * speedMultiplier) - 10)); + } + + public void setProgressChangedListener(Consumer progressChangedListener) { + this.progressChangedListener = progressChangedListener; + } + + public float getCurrentSpeed() { + return (seekBar.getProgress() + 10) / 20.0f; + } + + @Override + public void setEnabled(boolean enabled) { + super.setEnabled(enabled); + seekBar.setEnabled(enabled); + findViewById(R.id.butDecSpeed).setEnabled(enabled); + findViewById(R.id.butIncSpeed).setEnabled(enabled); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/screen/playback/SleepTimerDialog.java b/app/src/main/java/de/danoeh/antennapod/ui/screen/playback/SleepTimerDialog.java new file mode 100644 index 000000000..911460d58 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/screen/playback/SleepTimerDialog.java @@ -0,0 +1,214 @@ +package de.danoeh.antennapod.ui.screen.playback; + +import android.app.Activity; +import android.app.Dialog; +import android.content.Context; +import android.os.Bundle; +import android.text.format.DateFormat; +import android.view.View; +import android.view.inputmethod.InputMethodManager; +import android.widget.Button; +import android.widget.CheckBox; +import android.widget.EditText; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.fragment.app.DialogFragment; + +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import com.google.android.material.snackbar.Snackbar; + +import de.danoeh.antennapod.playback.service.PlaybackController; +import de.danoeh.antennapod.playback.service.PlaybackService; +import org.greenrobot.eventbus.EventBus; +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; + +import java.util.Locale; + +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.storage.preferences.SleepTimerPreferences; +import de.danoeh.antennapod.ui.common.Converter; +import de.danoeh.antennapod.event.playback.SleepTimerUpdatedEvent; + +public class SleepTimerDialog extends DialogFragment { + private PlaybackController controller; + private EditText etxtTime; + private LinearLayout timeSetup; + private LinearLayout timeDisplay; + private TextView time; + private CheckBox chAutoEnable; + + public SleepTimerDialog() { + + } + + @Override + public void onStart() { + super.onStart(); + controller = new PlaybackController(getActivity()) { + @Override + public void loadMediaInfo() { + } + }; + controller.init(); + EventBus.getDefault().register(this); + } + + @Override + public void onStop() { + super.onStop(); + if (controller != null) { + controller.release(); + } + EventBus.getDefault().unregister(this); + } + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + View content = View.inflate(getContext(), R.layout.time_dialog, null); + MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(getContext()); + builder.setTitle(R.string.sleep_timer_label); + builder.setView(content); + builder.setPositiveButton(R.string.close_label, null); + + etxtTime = content.findViewById(R.id.etxtTime); + timeSetup = content.findViewById(R.id.timeSetup); + timeDisplay = content.findViewById(R.id.timeDisplay); + timeDisplay.setVisibility(View.GONE); + time = content.findViewById(R.id.time); + Button extendSleepFiveMinutesButton = content.findViewById(R.id.extendSleepFiveMinutesButton); + extendSleepFiveMinutesButton.setText(getString(R.string.extend_sleep_timer_label, 5)); + Button extendSleepTenMinutesButton = content.findViewById(R.id.extendSleepTenMinutesButton); + extendSleepTenMinutesButton.setText(getString(R.string.extend_sleep_timer_label, 10)); + Button extendSleepTwentyMinutesButton = content.findViewById(R.id.extendSleepTwentyMinutesButton); + extendSleepTwentyMinutesButton.setText(getString(R.string.extend_sleep_timer_label, 20)); + extendSleepFiveMinutesButton.setOnClickListener(v -> { + if (controller != null) { + controller.extendSleepTimer(5 * 1000 * 60); + } + }); + extendSleepTenMinutesButton.setOnClickListener(v -> { + if (controller != null) { + controller.extendSleepTimer(10 * 1000 * 60); + } + }); + extendSleepTwentyMinutesButton.setOnClickListener(v -> { + if (controller != null) { + controller.extendSleepTimer(20 * 1000 * 60); + } + }); + + etxtTime.setText(SleepTimerPreferences.lastTimerValue()); + etxtTime.postDelayed(() -> { + InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + imm.showSoftInput(etxtTime, InputMethodManager.SHOW_IMPLICIT); + }, 100); + + final CheckBox cbShakeToReset = content.findViewById(R.id.cbShakeToReset); + final CheckBox cbVibrate = content.findViewById(R.id.cbVibrate); + chAutoEnable = content.findViewById(R.id.chAutoEnable); + final ImageView changeTimesButton = content.findViewById(R.id.changeTimesButton); + + cbShakeToReset.setChecked(SleepTimerPreferences.shakeToReset()); + cbVibrate.setChecked(SleepTimerPreferences.vibrate()); + chAutoEnable.setChecked(SleepTimerPreferences.autoEnable()); + changeTimesButton.setEnabled(chAutoEnable.isChecked()); + changeTimesButton.setAlpha(chAutoEnable.isChecked() ? 1.0f : 0.5f); + + cbShakeToReset.setOnCheckedChangeListener((buttonView, isChecked) + -> SleepTimerPreferences.setShakeToReset(isChecked)); + cbVibrate.setOnCheckedChangeListener((buttonView, isChecked) + -> SleepTimerPreferences.setVibrate(isChecked)); + chAutoEnable.setOnCheckedChangeListener((compoundButton, isChecked) + -> { + SleepTimerPreferences.setAutoEnable(isChecked); + changeTimesButton.setEnabled(isChecked); + changeTimesButton.setAlpha(isChecked ? 1.0f : 0.5f); + }); + updateAutoEnableText(); + + changeTimesButton.setOnClickListener(changeTimesBtn -> { + int from = SleepTimerPreferences.autoEnableFrom(); + int to = SleepTimerPreferences.autoEnableTo(); + showTimeRangeDialog(getContext(), from, to); + }); + + Button disableButton = content.findViewById(R.id.disableSleeptimerButton); + disableButton.setOnClickListener(v -> { + if (controller != null) { + controller.disableSleepTimer(); + } + }); + Button setButton = content.findViewById(R.id.setSleeptimerButton); + setButton.setOnClickListener(v -> { + if (!PlaybackService.isRunning) { + Snackbar.make(content, R.string.no_media_playing_label, Snackbar.LENGTH_LONG).show(); + return; + } + try { + long time = Long.parseLong(etxtTime.getText().toString()); + if (time == 0) { + throw new NumberFormatException("Timer must not be zero"); + } + SleepTimerPreferences.setLastTimer(etxtTime.getText().toString()); + if (controller != null) { + controller.setSleepTimer(SleepTimerPreferences.timerMillis()); + } + closeKeyboard(content); + } catch (NumberFormatException e) { + e.printStackTrace(); + Snackbar.make(content, R.string.time_dialog_invalid_input, Snackbar.LENGTH_LONG).show(); + } + }); + return builder.create(); + } + + private void showTimeRangeDialog(Context context, int from, int to) { + TimeRangeDialog dialog = new TimeRangeDialog(context, from, to); + dialog.setOnDismissListener(v -> { + SleepTimerPreferences.setAutoEnableFrom(dialog.getFrom()); + SleepTimerPreferences.setAutoEnableTo(dialog.getTo()); + updateAutoEnableText(); + }); + dialog.show(); + } + + private void updateAutoEnableText() { + String text; + int from = SleepTimerPreferences.autoEnableFrom(); + int to = SleepTimerPreferences.autoEnableTo(); + + if (from == to) { + text = getString(R.string.auto_enable_label); + } else if (DateFormat.is24HourFormat(getContext())) { + String formattedFrom = String.format(Locale.getDefault(), "%02d:00", from); + String formattedTo = String.format(Locale.getDefault(), "%02d:00", to); + text = getString(R.string.auto_enable_label_with_times, formattedFrom, formattedTo); + } else { + String formattedFrom = String.format(Locale.getDefault(), "%02d:00 %s", + from % 12, from >= 12 ? "PM" : "AM"); + String formattedTo = String.format(Locale.getDefault(), "%02d:00 %s", + to % 12, to >= 12 ? "PM" : "AM"); + text = getString(R.string.auto_enable_label_with_times, formattedFrom, formattedTo); + + } + chAutoEnable.setText(text); + } + + @Subscribe(threadMode = ThreadMode.MAIN) + @SuppressWarnings("unused") + public void timerUpdated(SleepTimerUpdatedEvent event) { + timeDisplay.setVisibility(event.isOver() || event.isCancelled() ? View.GONE : View.VISIBLE); + timeSetup.setVisibility(event.isOver() || event.isCancelled() ? View.VISIBLE : View.GONE); + time.setText(Converter.getDurationStringLong((int) event.getTimeLeft())); + } + + private void closeKeyboard(View content) { + InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Activity.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(content.getWindowToken(), 0); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/screen/playback/TimeRangeDialog.java b/app/src/main/java/de/danoeh/antennapod/ui/screen/playback/TimeRangeDialog.java new file mode 100644 index 000000000..683c162fc --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/screen/playback/TimeRangeDialog.java @@ -0,0 +1,187 @@ +package de.danoeh.antennapod.ui.screen.playback; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Point; +import android.graphics.RectF; +import android.text.format.DateFormat; +import android.view.MotionEvent; +import android.view.View; +import androidx.annotation.NonNull; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.ui.common.ThemeUtils; + +import java.util.Locale; + +public class TimeRangeDialog extends MaterialAlertDialogBuilder { + private final TimeRangeView view; + + public TimeRangeDialog(@NonNull Context context, int from, int to) { + super(context); + view = new TimeRangeView(context, from, to); + setView(view); + setPositiveButton(android.R.string.ok, null); + } + + public int getFrom() { + return view.from; + } + + public int getTo() { + return view.to; + } + + static class TimeRangeView extends View { + private static final int DIAL_ALPHA = 120; + private final Paint paintDial = new Paint(); + private final Paint paintSelected = new Paint(); + private final Paint paintText = new Paint(); + private int from; + private int to; + private final RectF bounds = new RectF(); + int touching = 0; + + public TimeRangeView(Context context) { // Used by Android tools + this(context, 0, 0); + } + + public TimeRangeView(Context context, int from, int to) { + super(context); + this.from = from; + this.to = to; + setup(); + } + + private void setup() { + paintDial.setAntiAlias(true); + paintDial.setStyle(Paint.Style.STROKE); + paintDial.setStrokeCap(Paint.Cap.ROUND); + paintDial.setColor(ThemeUtils.getColorFromAttr(getContext(), android.R.attr.textColorPrimary)); + paintDial.setAlpha(DIAL_ALPHA); + + paintSelected.setAntiAlias(true); + paintSelected.setStyle(Paint.Style.STROKE); + paintSelected.setStrokeCap(Paint.Cap.ROUND); + paintSelected.setColor(ThemeUtils.getColorFromAttr(getContext(), R.attr.colorAccent)); + + paintText.setAntiAlias(true); + paintText.setStyle(Paint.Style.FILL); + paintText.setColor(ThemeUtils.getColorFromAttr(getContext(), android.R.attr.textColorPrimary)); + paintText.setTextAlign(Paint.Align.CENTER); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + if (MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY + && MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } else if (MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY) { + super.onMeasure(widthMeasureSpec, widthMeasureSpec); + } else if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY) { + super.onMeasure(heightMeasureSpec, heightMeasureSpec); + } else if (MeasureSpec.getSize(widthMeasureSpec) < MeasureSpec.getSize(heightMeasureSpec)) { + super.onMeasure(widthMeasureSpec, widthMeasureSpec); + } else { + super.onMeasure(heightMeasureSpec, heightMeasureSpec); + } + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + float size = getHeight(); // square + float padding = size * 0.1f; + paintDial.setStrokeWidth(size * 0.005f); + bounds.set(padding, padding, size - padding, size - padding); + + paintText.setAlpha(DIAL_ALPHA); + canvas.drawArc(bounds, 0, 360, false, paintDial); + for (int i = 0; i < 24; i++) { + paintDial.setStrokeWidth(size * 0.005f); + if (i % 6 == 0) { + paintDial.setStrokeWidth(size * 0.01f); + Point textPos = radToPoint(i / 24.0f * 360.f, size / 2 - 2.5f * padding); + paintText.setTextSize(0.4f * padding); + canvas.drawText(String.valueOf(i), textPos.x, + textPos.y + (-paintText.descent() - paintText.ascent()) / 2, paintText); + } + Point outer = radToPoint(i / 24.0f * 360.f, size / 2 - 1.7f * padding); + Point inner = radToPoint(i / 24.0f * 360.f, size / 2 - 1.9f * padding); + canvas.drawLine(outer.x, outer.y, inner.x, inner.y, paintDial); + } + paintText.setAlpha(255); + + float angleFrom = (float) from / 24 * 360 - 90; + float angleDistance = (float) ((to - from + 24) % 24) / 24 * 360; + paintSelected.setStrokeWidth(padding / 6); + paintSelected.setStyle(Paint.Style.STROKE); + canvas.drawArc(bounds, angleFrom, angleDistance, false, paintSelected); + paintSelected.setStyle(Paint.Style.FILL); + Point p1 = radToPoint(angleFrom + 90, size / 2 - padding); + canvas.drawCircle(p1.x, p1.y, padding / 2, paintSelected); + Point p2 = radToPoint(angleFrom + angleDistance + 90, size / 2 - padding); + canvas.drawCircle(p2.x, p2.y, padding / 2, paintSelected); + + paintText.setTextSize(0.6f * padding); + String timeRange; + if (from == to) { + timeRange = getContext().getString(R.string.sleep_timer_always); + } else if (DateFormat.is24HourFormat(getContext())) { + timeRange = String.format(Locale.getDefault(), "%02d:00 - %02d:00", from, to); + } else { + timeRange = String.format(Locale.getDefault(), "%02d:00 %s - %02d:00 %s", from % 12, + from >= 12 ? "PM" : "AM", to % 12, to >= 12 ? "PM" : "AM"); + } + canvas.drawText(timeRange, size / 2, (size - paintText.descent() - paintText.ascent()) / 2, paintText); + } + + protected Point radToPoint(float angle, float radius) { + return new Point((int) (getWidth() / 2.0 + radius * Math.sin(-angle * Math.PI / 180.0 + Math.PI)), + (int) (getHeight() / 2.0 + radius * Math.cos(-angle * Math.PI / 180.0 + Math.PI))); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + getParent().requestDisallowInterceptTouchEvent(true); + Point center = new Point(getWidth() / 2, getHeight() / 2); + double angleRad = Math.atan2(center.y - event.getY(), center.x - event.getX()); + float angle = (float) (angleRad * (180 / Math.PI)); + angle += 360 + 360 - 90; + angle %= 360; + + if (event.getAction() == MotionEvent.ACTION_DOWN) { + float fromDistance = Math.abs(angle - (float) from / 24 * 360); + float toDistance = Math.abs(angle - (float) to / 24 * 360); + if (fromDistance < 15 || fromDistance > (360 - 15)) { + touching = 1; + return true; + } else if (toDistance < 15 || toDistance > (360 - 15)) { + touching = 2; + return true; + } + } else if (event.getAction() == MotionEvent.ACTION_MOVE) { + int newTime = (int) (24 * (angle / 360.0)); + if (from == to && touching != 0) { + // Switch which handle is focussed such that selection is the smaller arc + touching = (((newTime - to + 24) % 24) < 12) ? 2 : 1; + } + if (touching == 1) { + from = newTime; + invalidate(); + return true; + } else if (touching == 2) { + to = newTime; + invalidate(); + return true; + } + } else if (touching != 0) { + touching = 0; + return true; + } + return super.onTouchEvent(event); + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/screen/playback/VariableSpeedDialog.java b/app/src/main/java/de/danoeh/antennapod/ui/screen/playback/VariableSpeedDialog.java new file mode 100644 index 000000000..8343e933a --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/screen/playback/VariableSpeedDialog.java @@ -0,0 +1,180 @@ +package de.danoeh.antennapod.ui.screen.playback; + +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CheckBox; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import com.google.android.material.bottomsheet.BottomSheetDialogFragment; +import com.google.android.material.chip.Chip; +import com.google.android.material.snackbar.Snackbar; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.event.playback.SpeedChangedEvent; +import de.danoeh.antennapod.playback.service.PlaybackController; +import de.danoeh.antennapod.storage.preferences.UserPreferences; +import de.danoeh.antennapod.ui.view.ItemOffsetDecoration; +import org.greenrobot.eventbus.EventBus; +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; + +import java.text.DecimalFormatSymbols; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Locale; + +public class VariableSpeedDialog extends BottomSheetDialogFragment { + private SpeedSelectionAdapter adapter; + private PlaybackController controller; + private final List selectedSpeeds; + private PlaybackSpeedSeekBar speedSeekBar; + private Chip addCurrentSpeedChip; + private CheckBox skipSilenceCheckbox; + + public VariableSpeedDialog() { + DecimalFormatSymbols format = new DecimalFormatSymbols(Locale.US); + format.setDecimalSeparator('.'); + selectedSpeeds = new ArrayList<>(UserPreferences.getPlaybackSpeedArray()); + } + + @Override + public void onStart() { + super.onStart(); + controller = new PlaybackController(getActivity()) { + @Override + public void loadMediaInfo() { + updateSpeed(new SpeedChangedEvent(controller.getCurrentPlaybackSpeedMultiplier())); + updateSkipSilence(controller.getCurrentPlaybackSkipSilence()); + } + }; + controller.init(); + EventBus.getDefault().register(this); + updateSpeed(new SpeedChangedEvent(controller.getCurrentPlaybackSpeedMultiplier())); + updateSkipSilence(controller.getCurrentPlaybackSkipSilence()); + } + + @Override + public void onStop() { + super.onStop(); + controller.release(); + controller = null; + EventBus.getDefault().unregister(this); + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void updateSpeed(SpeedChangedEvent event) { + speedSeekBar.updateSpeed(event.getNewSpeed()); + addCurrentSpeedChip.setText(String.format(Locale.getDefault(), "%1$.2f", event.getNewSpeed())); + } + + public void updateSkipSilence(boolean skipSilence) { + skipSilenceCheckbox.setChecked(skipSilence); + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + View root = View.inflate(getContext(), R.layout.speed_select_dialog, null); + speedSeekBar = root.findViewById(R.id.speed_seek_bar); + speedSeekBar.setProgressChangedListener(multiplier -> { + UserPreferences.setPlaybackSpeed(multiplier); + if (controller != null) { + controller.setPlaybackSpeed(multiplier); + } + }); + RecyclerView selectedSpeedsGrid = root.findViewById(R.id.selected_speeds_grid); + selectedSpeedsGrid.setLayoutManager(new GridLayoutManager(getContext(), 3)); + selectedSpeedsGrid.addItemDecoration(new ItemOffsetDecoration(getContext(), 4)); + adapter = new SpeedSelectionAdapter(); + adapter.setHasStableIds(true); + selectedSpeedsGrid.setAdapter(adapter); + + addCurrentSpeedChip = root.findViewById(R.id.add_current_speed_chip); + addCurrentSpeedChip.setCloseIconVisible(true); + addCurrentSpeedChip.setCloseIconResource(R.drawable.ic_add); + addCurrentSpeedChip.setOnCloseIconClickListener(v -> addCurrentSpeed()); + addCurrentSpeedChip.setCloseIconContentDescription(getString(R.string.add_preset)); + addCurrentSpeedChip.setOnClickListener(v -> addCurrentSpeed()); + + skipSilenceCheckbox = root.findViewById(R.id.skipSilence); + skipSilenceCheckbox.setChecked(UserPreferences.isSkipSilence()); + skipSilenceCheckbox.setOnCheckedChangeListener((buttonView, isChecked) -> { + UserPreferences.setSkipSilence(isChecked); + controller.setSkipSilence(isChecked); + }); + return root; + } + + private void addCurrentSpeed() { + float newSpeed = speedSeekBar.getCurrentSpeed(); + if (selectedSpeeds.contains(newSpeed)) { + Snackbar.make(addCurrentSpeedChip, + getString(R.string.preset_already_exists, newSpeed), Snackbar.LENGTH_LONG).show(); + } else { + selectedSpeeds.add(newSpeed); + Collections.sort(selectedSpeeds); + UserPreferences.setPlaybackSpeedArray(selectedSpeeds); + adapter.notifyDataSetChanged(); + } + } + + public class SpeedSelectionAdapter extends RecyclerView.Adapter { + + @Override + @NonNull + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + Chip chip = new Chip(getContext()); + chip.setTextAlignment(View.TEXT_ALIGNMENT_CENTER); + return new ViewHolder(chip); + } + + @Override + public void onBindViewHolder(@NonNull ViewHolder holder, int position) { + float speed = selectedSpeeds.get(position); + + holder.chip.setText(String.format(Locale.getDefault(), "%1$.2f", speed)); + holder.chip.setOnLongClickListener(v -> { + selectedSpeeds.remove(speed); + UserPreferences.setPlaybackSpeedArray(selectedSpeeds); + notifyDataSetChanged(); + return true; + }); + holder.chip.setOnClickListener(v -> { + UserPreferences.setPlaybackSpeed(speed); + new Handler(Looper.getMainLooper()).postDelayed(() -> { + if (controller != null) { + controller.setPlaybackSpeed(speed); + dismiss(); + } + }, 200); + }); + } + + @Override + public int getItemCount() { + return selectedSpeeds.size(); + } + + @Override + public long getItemId(int position) { + return selectedSpeeds.get(position).hashCode(); + } + + public class ViewHolder extends RecyclerView.ViewHolder { + Chip chip; + + ViewHolder(Chip itemView) { + super(itemView); + chip = itemView; + } + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/screen/playback/audio/AudioPlayerFragment.java b/app/src/main/java/de/danoeh/antennapod/ui/screen/playback/audio/AudioPlayerFragment.java new file mode 100644 index 000000000..5e1f5c818 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/screen/playback/audio/AudioPlayerFragment.java @@ -0,0 +1,561 @@ +package de.danoeh.antennapod.ui.screen.playback.audio; + +import android.content.Intent; +import android.os.Bundle; +import android.util.Log; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageButton; +import android.widget.ProgressBar; +import android.widget.SeekBar; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.cardview.widget.CardView; +import androidx.fragment.app.Fragment; +import androidx.interpolator.view.animation.FastOutSlowInInterpolator; +import androidx.viewpager2.adapter.FragmentStateAdapter; +import androidx.viewpager2.widget.ViewPager2; + +import com.google.android.material.appbar.MaterialToolbar; +import com.google.android.material.bottomsheet.BottomSheetBehavior; +import com.google.android.material.elevation.SurfaceColors; + +import de.danoeh.antennapod.playback.service.PlaybackController; +import de.danoeh.antennapod.ui.appstartintent.MediaButtonStarter; +import de.danoeh.antennapod.ui.episodes.PlaybackSpeedUtils; +import de.danoeh.antennapod.ui.episodes.TimeSpeedConverter; +import de.danoeh.antennapod.ui.screen.playback.MediaPlayerErrorDialog; +import de.danoeh.antennapod.ui.screen.playback.PlayButton; +import de.danoeh.antennapod.ui.screen.playback.SleepTimerDialog; +import de.danoeh.antennapod.ui.screen.playback.VariableSpeedDialog; +import org.greenrobot.eventbus.EventBus; +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; + +import java.text.DecimalFormat; +import java.text.NumberFormat; +import java.util.List; + +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.core.util.ChapterUtils; +import de.danoeh.antennapod.ui.common.Converter; +import de.danoeh.antennapod.ui.screen.feed.preferences.SkipPreferenceDialog; +import de.danoeh.antennapod.event.FavoritesEvent; +import de.danoeh.antennapod.event.PlayerErrorEvent; +import de.danoeh.antennapod.event.UnreadItemsUpdateEvent; +import de.danoeh.antennapod.event.playback.BufferUpdateEvent; +import de.danoeh.antennapod.event.playback.PlaybackPositionEvent; +import de.danoeh.antennapod.event.playback.PlaybackServiceEvent; +import de.danoeh.antennapod.event.playback.SleepTimerUpdatedEvent; +import de.danoeh.antennapod.event.playback.SpeedChangedEvent; +import de.danoeh.antennapod.ui.episodeslist.FeedItemMenuHandler; +import de.danoeh.antennapod.model.feed.Chapter; +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedMedia; +import de.danoeh.antennapod.model.playback.Playable; +import de.danoeh.antennapod.playback.cast.CastEnabledActivity; +import de.danoeh.antennapod.storage.preferences.UserPreferences; +import de.danoeh.antennapod.ui.common.PlaybackSpeedIndicatorView; +import io.reactivex.Maybe; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; +import io.reactivex.schedulers.Schedulers; + +/** + * Shows the audio player. + */ +public class AudioPlayerFragment extends Fragment implements + ChapterSeekBar.OnSeekBarChangeListener, MaterialToolbar.OnMenuItemClickListener { + public static final String TAG = "AudioPlayerFragment"; + public static final int POS_COVER = 0; + public static final int POS_DESCRIPTION = 1; + private static final int NUM_CONTENT_FRAGMENTS = 2; + + PlaybackSpeedIndicatorView butPlaybackSpeed; + TextView txtvPlaybackSpeed; + private ViewPager2 pager; + private TextView txtvPosition; + private TextView txtvLength; + private ChapterSeekBar sbPosition; + private ImageButton butRev; + private TextView txtvRev; + private PlayButton butPlay; + private ImageButton butFF; + private TextView txtvFF; + private ImageButton butSkip; + private MaterialToolbar toolbar; + private ProgressBar progressIndicator; + private CardView cardViewSeek; + private TextView txtvSeek; + + private PlaybackController controller; + private Disposable disposable; + private boolean showTimeLeft; + private boolean seekedToChapterStart = false; + private int currentChapterIndex = -1; + private int duration; + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, + @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + super.onCreateView(inflater, container, savedInstanceState); + View root = inflater.inflate(R.layout.audioplayer_fragment, container, false); + root.setOnTouchListener((v, event) -> true); // Avoid clicks going through player to fragments below + toolbar = root.findViewById(R.id.toolbar); + toolbar.setTitle(""); + toolbar.setNavigationOnClickListener(v -> + ((MainActivity) getActivity()).getBottomSheet().setState(BottomSheetBehavior.STATE_COLLAPSED)); + toolbar.setOnMenuItemClickListener(this); + + ExternalPlayerFragment externalPlayerFragment = new ExternalPlayerFragment(); + getChildFragmentManager().beginTransaction() + .replace(R.id.playerFragment, externalPlayerFragment, ExternalPlayerFragment.TAG) + .commit(); + root.findViewById(R.id.playerFragment).setBackgroundColor( + SurfaceColors.getColorForElevation(getContext(), 8 * getResources().getDisplayMetrics().density)); + + butPlaybackSpeed = root.findViewById(R.id.butPlaybackSpeed); + txtvPlaybackSpeed = root.findViewById(R.id.txtvPlaybackSpeed); + sbPosition = root.findViewById(R.id.sbPosition); + txtvPosition = root.findViewById(R.id.txtvPosition); + txtvLength = root.findViewById(R.id.txtvLength); + butRev = root.findViewById(R.id.butRev); + txtvRev = root.findViewById(R.id.txtvRev); + butPlay = root.findViewById(R.id.butPlay); + butFF = root.findViewById(R.id.butFF); + txtvFF = root.findViewById(R.id.txtvFF); + butSkip = root.findViewById(R.id.butSkip); + progressIndicator = root.findViewById(R.id.progLoading); + cardViewSeek = root.findViewById(R.id.cardViewSeek); + txtvSeek = root.findViewById(R.id.txtvSeek); + + setupLengthTextView(); + setupControlButtons(); + butPlaybackSpeed.setOnClickListener(v -> new VariableSpeedDialog().show(getChildFragmentManager(), null)); + sbPosition.setOnSeekBarChangeListener(this); + + pager = root.findViewById(R.id.pager); + pager.setAdapter(new AudioPlayerPagerAdapter(this)); + // Required for getChildAt(int) in ViewPagerBottomSheetBehavior to return the correct page + pager.setOffscreenPageLimit((int) NUM_CONTENT_FRAGMENTS); + pager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() { + @Override + public void onPageSelected(int position) { + pager.post(() -> { + if (getActivity() != null) { + // By the time this is posted, the activity might be closed again. + ((MainActivity) getActivity()).getBottomSheet().updateScrollingChild(); + } + }); + } + }); + + return root; + } + + private void setChapterDividers(Playable media) { + + if (media == null) { + return; + } + + float[] dividerPos = null; + + if (media.getChapters() != null && !media.getChapters().isEmpty()) { + List chapters = media.getChapters(); + dividerPos = new float[chapters.size()]; + + for (int i = 0; i < chapters.size(); i++) { + dividerPos[i] = chapters.get(i).getStart() / (float) duration; + } + } + + sbPosition.setDividerPos(dividerPos); + } + + private void setupControlButtons() { + butRev.setOnClickListener(v -> { + if (controller != null) { + int curr = controller.getPosition(); + controller.seekTo(curr - UserPreferences.getRewindSecs() * 1000); + } + }); + butRev.setOnLongClickListener(v -> { + SkipPreferenceDialog.showSkipPreference(getContext(), + SkipPreferenceDialog.SkipDirection.SKIP_REWIND, txtvRev); + return true; + }); + butPlay.setOnClickListener(v -> { + if (controller != null) { + controller.init(); + controller.playPause(); + } + }); + butFF.setOnClickListener(v -> { + if (controller != null) { + int curr = controller.getPosition(); + controller.seekTo(curr + UserPreferences.getFastForwardSecs() * 1000); + } + }); + butFF.setOnLongClickListener(v -> { + SkipPreferenceDialog.showSkipPreference(getContext(), + SkipPreferenceDialog.SkipDirection.SKIP_FORWARD, txtvFF); + return false; + }); + butSkip.setOnClickListener(v -> getActivity().sendBroadcast( + MediaButtonStarter.createIntent(getContext(), KeyEvent.KEYCODE_MEDIA_NEXT))); + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onUnreadItemsUpdate(UnreadItemsUpdateEvent event) { + if (controller == null) { + return; + } + updatePosition(new PlaybackPositionEvent(controller.getPosition(), + controller.getDuration())); + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onPlaybackServiceChanged(PlaybackServiceEvent event) { + if (event.action == PlaybackServiceEvent.Action.SERVICE_SHUT_DOWN) { + ((MainActivity) getActivity()).getBottomSheet().setState(BottomSheetBehavior.STATE_COLLAPSED); + } + } + + private void setupLengthTextView() { + showTimeLeft = UserPreferences.shouldShowRemainingTime(); + txtvLength.setOnClickListener(v -> { + if (controller == null) { + return; + } + showTimeLeft = !showTimeLeft; + UserPreferences.setShowRemainTimeSetting(showTimeLeft); + updatePosition(new PlaybackPositionEvent(controller.getPosition(), + controller.getDuration())); + }); + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void updatePlaybackSpeedButton(SpeedChangedEvent event) { + String speedStr = new DecimalFormat("0.00").format(event.getNewSpeed()); + txtvPlaybackSpeed.setText(speedStr); + butPlaybackSpeed.setSpeed(event.getNewSpeed()); + } + + private void loadMediaInfo(boolean includingChapters) { + if (disposable != null) { + disposable.dispose(); + } + disposable = Maybe.create(emitter -> { + Playable media = controller.getMedia(); + if (media != null) { + if (includingChapters) { + ChapterUtils.loadChapters(media, getContext(), false); + } + emitter.onSuccess(media); + } else { + emitter.onComplete(); + } + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(media -> { + updateUi(media); + if (media.getChapters() == null && !includingChapters) { + loadMediaInfo(true); + } + }, error -> Log.e(TAG, Log.getStackTraceString(error)), + () -> updateUi(null)); + } + + private PlaybackController newPlaybackController() { + return new PlaybackController(getActivity()) { + @Override + protected void updatePlayButtonShowsPlay(boolean showPlay) { + butPlay.setIsShowPlay(showPlay); + } + + @Override + public void loadMediaInfo() { + AudioPlayerFragment.this.loadMediaInfo(false); + } + + @Override + public void onPlaybackEnd() { + ((MainActivity) getActivity()).getBottomSheet().setState(BottomSheetBehavior.STATE_COLLAPSED); + } + }; + } + + private void updateUi(Playable media) { + if (controller == null || media == null) { + return; + } + duration = controller.getDuration(); + updatePosition(new PlaybackPositionEvent(media.getPosition(), media.getDuration())); + updatePlaybackSpeedButton(new SpeedChangedEvent(PlaybackSpeedUtils.getCurrentPlaybackSpeed(media))); + setChapterDividers(media); + setupOptionsMenu(media); + } + + @Subscribe(threadMode = ThreadMode.MAIN) + @SuppressWarnings("unused") + public void sleepTimerUpdate(SleepTimerUpdatedEvent event) { + if (event.isCancelled() || event.wasJustEnabled()) { + AudioPlayerFragment.this.loadMediaInfo(false); + } + } + + @Override + public void onStart() { + super.onStart(); + controller = newPlaybackController(); + controller.init(); + loadMediaInfo(false); + EventBus.getDefault().register(this); + txtvRev.setText(NumberFormat.getInstance().format(UserPreferences.getRewindSecs())); + txtvFF.setText(NumberFormat.getInstance().format(UserPreferences.getFastForwardSecs())); + } + + @Override + public void onStop() { + super.onStop(); + controller.release(); + controller = null; + progressIndicator.setVisibility(View.GONE); // Controller released; we will not receive buffering updates + EventBus.getDefault().unregister(this); + if (disposable != null) { + disposable.dispose(); + } + } + + @Subscribe(threadMode = ThreadMode.MAIN) + @SuppressWarnings("unused") + public void bufferUpdate(BufferUpdateEvent event) { + if (event.hasStarted()) { + progressIndicator.setVisibility(View.VISIBLE); + } else if (event.hasEnded()) { + progressIndicator.setVisibility(View.GONE); + } else if (controller != null && controller.isStreaming()) { + sbPosition.setSecondaryProgress((int) (event.getProgress() * sbPosition.getMax())); + } else { + sbPosition.setSecondaryProgress(0); + } + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void updatePosition(PlaybackPositionEvent event) { + if (controller == null || txtvPosition == null || txtvLength == null || sbPosition == null) { + return; + } + + TimeSpeedConverter converter = new TimeSpeedConverter(controller.getCurrentPlaybackSpeedMultiplier()); + int currentPosition = converter.convert(event.getPosition()); + int duration = converter.convert(event.getDuration()); + int remainingTime = converter.convert(Math.max(event.getDuration() - event.getPosition(), 0)); + currentChapterIndex = ChapterUtils.getCurrentChapterIndex(controller.getMedia(), currentPosition); + Log.d(TAG, "currentPosition " + Converter.getDurationStringLong(currentPosition)); + if (currentPosition == Playable.INVALID_TIME || duration == Playable.INVALID_TIME) { + Log.w(TAG, "Could not react to position observer update because of invalid time"); + return; + } + txtvPosition.setText(Converter.getDurationStringLong(currentPosition)); + txtvPosition.setContentDescription(getString(R.string.position, + Converter.getDurationStringLocalized(getContext(), currentPosition))); + showTimeLeft = UserPreferences.shouldShowRemainingTime(); + if (showTimeLeft) { + txtvLength.setContentDescription(getString(R.string.remaining_time, + Converter.getDurationStringLocalized(getContext(), remainingTime))); + txtvLength.setText(((remainingTime > 0) ? "-" : "") + Converter.getDurationStringLong(remainingTime)); + } else { + txtvLength.setContentDescription(getString(R.string.chapter_duration, + Converter.getDurationStringLocalized(getContext(), duration))); + txtvLength.setText(Converter.getDurationStringLong(duration)); + } + + if (!sbPosition.isPressed()) { + float progress = ((float) event.getPosition()) / event.getDuration(); + sbPosition.setProgress((int) (progress * sbPosition.getMax())); + } + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void favoritesChanged(FavoritesEvent event) { + AudioPlayerFragment.this.loadMediaInfo(false); + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void mediaPlayerError(PlayerErrorEvent event) { + MediaPlayerErrorDialog.show(getActivity(), event); + } + + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + if (controller == null || txtvLength == null) { + return; + } + + if (fromUser) { + float prog = progress / ((float) seekBar.getMax()); + TimeSpeedConverter converter = new TimeSpeedConverter(controller.getCurrentPlaybackSpeedMultiplier()); + int position = converter.convert((int) (prog * controller.getDuration())); + int newChapterIndex = ChapterUtils.getCurrentChapterIndex(controller.getMedia(), position); + if (newChapterIndex > -1) { + if (!sbPosition.isPressed() && currentChapterIndex != newChapterIndex) { + currentChapterIndex = newChapterIndex; + position = (int) controller.getMedia().getChapters().get(currentChapterIndex).getStart(); + seekedToChapterStart = true; + controller.seekTo(position); + updateUi(controller.getMedia()); + sbPosition.highlightCurrentChapter(); + } + txtvSeek.setText(controller.getMedia().getChapters().get(newChapterIndex).getTitle() + + "\n" + Converter.getDurationStringLong(position)); + } else { + txtvSeek.setText(Converter.getDurationStringLong(position)); + } + } else if (duration != controller.getDuration()) { + updateUi(controller.getMedia()); + } + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + // interrupt position Observer, restart later + cardViewSeek.setScaleX(.8f); + cardViewSeek.setScaleY(.8f); + cardViewSeek.animate() + .setInterpolator(new FastOutSlowInInterpolator()) + .alpha(1f).scaleX(1f).scaleY(1f) + .setDuration(200) + .start(); + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + if (controller != null) { + if (seekedToChapterStart) { + seekedToChapterStart = false; + } else { + float prog = seekBar.getProgress() / ((float) seekBar.getMax()); + controller.seekTo((int) (prog * controller.getDuration())); + } + } + cardViewSeek.setScaleX(1f); + cardViewSeek.setScaleY(1f); + cardViewSeek.animate() + .setInterpolator(new FastOutSlowInInterpolator()) + .alpha(0f).scaleX(.8f).scaleY(.8f) + .setDuration(200) + .start(); + } + + public void setupOptionsMenu(Playable media) { + if (toolbar.getMenu().size() == 0) { + toolbar.inflateMenu(R.menu.mediaplayer); + } + if (controller == null) { + return; + } + boolean isFeedMedia = media instanceof FeedMedia; + toolbar.getMenu().findItem(R.id.open_feed_item).setVisible(isFeedMedia); + if (isFeedMedia) { + FeedItemMenuHandler.onPrepareMenu(toolbar.getMenu(), ((FeedMedia) media).getItem()); + } + + toolbar.getMenu().findItem(R.id.set_sleeptimer_item).setVisible(!controller.sleepTimerActive()); + toolbar.getMenu().findItem(R.id.disable_sleeptimer_item).setVisible(controller.sleepTimerActive()); + + ((CastEnabledActivity) getActivity()).requestCastButton(toolbar.getMenu()); + } + + @Override + public boolean onMenuItemClick(MenuItem item) { + if (controller == null) { + return false; + } + Playable media = controller.getMedia(); + if (media == null) { + return false; + } + + final @Nullable FeedItem feedItem = (media instanceof FeedMedia) ? ((FeedMedia) media).getItem() : null; + if (feedItem != null && FeedItemMenuHandler.onMenuItemClicked(this, item.getItemId(), feedItem)) { + return true; + } + + final int itemId = item.getItemId(); + if (itemId == R.id.disable_sleeptimer_item || itemId == R.id.set_sleeptimer_item) { + new SleepTimerDialog().show(getChildFragmentManager(), "SleepTimerDialog"); + return true; + } else if (itemId == R.id.open_feed_item) { + if (feedItem != null) { + Intent intent = MainActivity.getIntentToOpenFeed(getContext(), feedItem.getFeedId()); + startActivity(intent); + } + return true; + } + return false; + } + + public void fadePlayerToToolbar(float slideOffset) { + float playerFadeProgress = Math.max(0.0f, Math.min(0.2f, slideOffset - 0.2f)) / 0.2f; + View player = getView().findViewById(R.id.playerFragment); + player.setAlpha(1 - playerFadeProgress); + player.setVisibility(playerFadeProgress > 0.99f ? View.INVISIBLE : View.VISIBLE); + float toolbarFadeProgress = Math.max(0.0f, Math.min(0.2f, slideOffset - 0.6f)) / 0.2f; + toolbar.setAlpha(toolbarFadeProgress); + toolbar.setVisibility(toolbarFadeProgress < 0.01f ? View.INVISIBLE : View.VISIBLE); + } + + private static class AudioPlayerPagerAdapter extends FragmentStateAdapter { + private static final String TAG = "AudioPlayerPagerAdapter"; + + public AudioPlayerPagerAdapter(@NonNull Fragment fragment) { + super(fragment); + } + + @NonNull + @Override + public Fragment createFragment(int position) { + Log.d(TAG, "getItem(" + position + ")"); + + switch (position) { + case POS_COVER: + return new CoverFragment(); + default: + case POS_DESCRIPTION: + return new ItemDescriptionFragment(); + } + } + + @Override + public int getItemCount() { + return NUM_CONTENT_FRAGMENTS; + } + } + + public void scrollToPage(int page, boolean smoothScroll) { + if (pager == null) { + return; + } + + pager.setCurrentItem(page, smoothScroll); + + Fragment visibleChild = getChildFragmentManager().findFragmentByTag("f" + POS_DESCRIPTION); + if (visibleChild instanceof ItemDescriptionFragment) { + ((ItemDescriptionFragment) visibleChild).scrollToTop(); + } + } + + public void scrollToPage(int page) { + scrollToPage(page, false); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/screen/playback/audio/ChapterSeekBar.java b/app/src/main/java/de/danoeh/antennapod/ui/screen/playback/audio/ChapterSeekBar.java new file mode 100644 index 000000000..0ce284f97 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/screen/playback/audio/ChapterSeekBar.java @@ -0,0 +1,148 @@ +package de.danoeh.antennapod.ui.screen.playback.audio; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.os.Handler; +import android.os.Looper; +import android.util.AttributeSet; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.ui.common.ThemeUtils; + +public class ChapterSeekBar extends androidx.appcompat.widget.AppCompatSeekBar { + + private float top; + private float width; + private float center; + private float bottom; + private float density; + private float progressPrimary; + private float progressSecondary; + private float[] dividerPos; + private boolean isHighlighted = false; + private final Paint paintBackground = new Paint(); + private final Paint paintProgressPrimary = new Paint(); + + public ChapterSeekBar(Context context) { + super(context); + init(context); + } + + public ChapterSeekBar(Context context, AttributeSet attrs) { + super(context, attrs); + init(context); + } + + public ChapterSeekBar(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(context); + } + + private void init(Context context) { + setBackground(null); // Removes the thumb shadow + dividerPos = null; + density = context.getResources().getDisplayMetrics().density; + + paintBackground.setColor(ThemeUtils.getColorFromAttr(getContext(), R.attr.colorSurfaceVariant)); + paintBackground.setAlpha(128); + paintProgressPrimary.setColor(ThemeUtils.getColorFromAttr(getContext(), R.attr.colorPrimary)); + } + + /** + * Sets the relative positions of the chapter dividers. + * @param dividerPos of the chapter dividers relative to the duration of the media. + */ + public void setDividerPos(final float[] dividerPos) { + if (dividerPos != null) { + this.dividerPos = new float[dividerPos.length + 2]; + this.dividerPos[0] = 0; + System.arraycopy(dividerPos, 0, this.dividerPos, 1, dividerPos.length); + this.dividerPos[this.dividerPos.length - 1] = 1; + } else { + this.dividerPos = null; + } + invalidate(); + } + + public void highlightCurrentChapter() { + isHighlighted = true; + new Handler(Looper.getMainLooper()).postDelayed(new Runnable() { + @Override + public void run() { + isHighlighted = false; + invalidate(); + } + }, 1000); + } + + @Override + protected synchronized void onDraw(Canvas canvas) { + center = (getBottom() - getPaddingBottom() - getTop() - getPaddingTop()) / 2.0f; + top = center - density * 1.5f; + bottom = center + density * 1.5f; + width = (float) (getRight() - getPaddingRight() - getLeft() - getPaddingLeft()); + progressSecondary = getSecondaryProgress() / (float) getMax() * width; + progressPrimary = getProgress() / (float) getMax() * width; + + if (dividerPos == null) { + drawProgress(canvas); + } else { + drawProgressChapters(canvas); + } + drawThumb(canvas); + } + + private void drawProgress(Canvas canvas) { + final int saveCount = canvas.save(); + canvas.translate(getPaddingLeft(), getPaddingTop()); + canvas.drawRect(0, top, width, bottom, paintBackground); + canvas.drawRect(0, top, progressSecondary, bottom, paintBackground); + canvas.drawRect(0, top, progressPrimary, bottom, paintProgressPrimary); + canvas.restoreToCount(saveCount); + } + + private void drawProgressChapters(Canvas canvas) { + final int saveCount = canvas.save(); + int currChapter = 1; + float chapterMargin = density * 1.2f; + float topExpanded = center - density * 2.0f; + float bottomExpanded = center + density * 2.0f; + + canvas.translate(getPaddingLeft(), getPaddingTop()); + + for (int i = 1; i < dividerPos.length; i++) { + float right = dividerPos[i] * width - chapterMargin; + float left = dividerPos[i - 1] * width; + float rightCurr = dividerPos[currChapter] * width - chapterMargin; + float leftCurr = dividerPos[currChapter - 1] * width; + + canvas.drawRect(left, top, right, bottom, paintBackground); + + if (progressSecondary > 0 && progressSecondary < width) { + if (right < progressSecondary) { + canvas.drawRect(left, top, right, bottom, paintBackground); + } else if (progressSecondary > left) { + canvas.drawRect(left, top, progressSecondary, bottom, paintBackground); + } + } + + if (right < progressPrimary) { + currChapter = i + 1; + canvas.drawRect(left, top, right, bottom, paintProgressPrimary); + } else if (isHighlighted || isPressed()) { + canvas.drawRect(leftCurr, topExpanded, rightCurr, bottomExpanded, paintBackground); + canvas.drawRect(leftCurr, topExpanded, progressPrimary, bottomExpanded, paintProgressPrimary); + } else { + canvas.drawRect(leftCurr, top, progressPrimary, bottom, paintProgressPrimary); + } + } + canvas.restoreToCount(saveCount); + } + + private void drawThumb(Canvas canvas) { + final int saveCount = canvas.save(); + canvas.translate(getPaddingLeft() - getThumbOffset(), getPaddingTop()); + getThumb().draw(canvas); + canvas.restoreToCount(saveCount); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/screen/playback/audio/CoverFragment.java b/app/src/main/java/de/danoeh/antennapod/ui/screen/playback/audio/CoverFragment.java new file mode 100644 index 000000000..05e6f3d6c --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/screen/playback/audio/CoverFragment.java @@ -0,0 +1,342 @@ +package de.danoeh.antennapod.ui.screen.playback.audio; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Intent; +import android.content.res.Configuration; +import android.graphics.ColorFilter; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.os.Bundle; +import android.text.TextUtils; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; +import androidx.core.graphics.BlendModeColorFilterCompat; +import androidx.core.graphics.BlendModeCompat; +import androidx.fragment.app.Fragment; +import com.bumptech.glide.Glide; +import com.bumptech.glide.RequestBuilder; +import com.bumptech.glide.load.resource.bitmap.FitCenter; +import com.bumptech.glide.load.resource.bitmap.RoundedCorners; +import com.bumptech.glide.request.RequestOptions; +import com.google.android.material.snackbar.Snackbar; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.core.util.ChapterUtils; +import de.danoeh.antennapod.ui.screen.chapter.ChaptersFragment; +import de.danoeh.antennapod.playback.service.PlaybackController; +import de.danoeh.antennapod.ui.common.DateFormatter; +import de.danoeh.antennapod.databinding.CoverFragmentBinding; +import de.danoeh.antennapod.event.playback.PlaybackPositionEvent; +import de.danoeh.antennapod.model.feed.Chapter; +import de.danoeh.antennapod.model.feed.EmbeddedChapterImage; +import de.danoeh.antennapod.model.feed.FeedMedia; +import de.danoeh.antennapod.model.playback.Playable; +import de.danoeh.antennapod.ui.episodes.ImageResourceUtils; +import io.reactivex.Maybe; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; +import io.reactivex.schedulers.Schedulers; +import org.apache.commons.lang3.StringUtils; +import org.greenrobot.eventbus.EventBus; +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; + +import static android.widget.LinearLayout.LayoutParams.MATCH_PARENT; +import static android.widget.LinearLayout.LayoutParams.WRAP_CONTENT; + +/** + * Displays the cover and the title of a FeedItem. + */ +public class CoverFragment extends Fragment { + private static final String TAG = "CoverFragment"; + private CoverFragmentBinding viewBinding; + private PlaybackController controller; + private Disposable disposable; + private int displayedChapterIndex = -1; + private Playable media; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + viewBinding = CoverFragmentBinding.inflate(inflater); + viewBinding.imgvCover.setOnClickListener(v -> onPlayPause()); + viewBinding.openDescription.setOnClickListener(view -> ((AudioPlayerFragment) requireParentFragment()) + .scrollToPage(AudioPlayerFragment.POS_DESCRIPTION, true)); + ColorFilter colorFilter = BlendModeColorFilterCompat.createBlendModeColorFilterCompat( + viewBinding.txtvPodcastTitle.getCurrentTextColor(), BlendModeCompat.SRC_IN); + viewBinding.butNextChapter.setColorFilter(colorFilter); + viewBinding.butPrevChapter.setColorFilter(colorFilter); + viewBinding.descriptionIcon.setColorFilter(colorFilter); + viewBinding.chapterButton.setOnClickListener(v -> + new ChaptersFragment().show(getChildFragmentManager(), ChaptersFragment.TAG)); + viewBinding.butPrevChapter.setOnClickListener(v -> seekToPrevChapter()); + viewBinding.butNextChapter.setOnClickListener(v -> seekToNextChapter()); + return viewBinding.getRoot(); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + configureForOrientation(getResources().getConfiguration()); + } + + private void loadMediaInfo(boolean includingChapters) { + if (disposable != null) { + disposable.dispose(); + } + disposable = Maybe.create(emitter -> { + Playable media = controller.getMedia(); + if (media != null) { + if (includingChapters) { + ChapterUtils.loadChapters(media, getContext(), false); + } + emitter.onSuccess(media); + } else { + emitter.onComplete(); + } + }).subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(media -> { + this.media = media; + displayMediaInfo(media); + if (media.getChapters() == null && !includingChapters) { + loadMediaInfo(true); + } + }, error -> Log.e(TAG, Log.getStackTraceString(error))); + } + + private void displayMediaInfo(@NonNull Playable media) { + String pubDateStr = DateFormatter.formatAbbrev(getActivity(), media.getPubDate()); + viewBinding.txtvPodcastTitle.setText(StringUtils.stripToEmpty(media.getFeedTitle()) + + "\u00A0" + + "・" + + "\u00A0" + + StringUtils.replace(StringUtils.stripToEmpty(pubDateStr), " ", "\u00A0")); + if (media instanceof FeedMedia) { + Intent openFeed = MainActivity.getIntentToOpenFeed(requireContext(), + ((FeedMedia) media).getItem().getFeedId()); + viewBinding.txtvPodcastTitle.setOnClickListener(v -> startActivity(openFeed)); + } else { + viewBinding.txtvPodcastTitle.setOnClickListener(null); + } + viewBinding.txtvPodcastTitle.setOnLongClickListener(v -> copyText(media.getFeedTitle())); + viewBinding.txtvEpisodeTitle.setText(media.getEpisodeTitle()); + viewBinding.txtvEpisodeTitle.setOnLongClickListener(v -> copyText(media.getEpisodeTitle())); + viewBinding.txtvEpisodeTitle.setOnClickListener(v -> { + int lines = viewBinding.txtvEpisodeTitle.getLineCount(); + int animUnit = 1500; + if (lines > viewBinding.txtvEpisodeTitle.getMaxLines()) { + int titleHeight = viewBinding.txtvEpisodeTitle.getHeight() + - viewBinding.txtvEpisodeTitle.getPaddingTop() + - viewBinding.txtvEpisodeTitle.getPaddingBottom(); + ObjectAnimator verticalMarquee = ObjectAnimator.ofInt( + viewBinding.txtvEpisodeTitle, "scrollY", 0, (lines - viewBinding.txtvEpisodeTitle.getMaxLines()) + * (titleHeight / viewBinding.txtvEpisodeTitle.getMaxLines())) + .setDuration(lines * animUnit); + ObjectAnimator fadeOut = ObjectAnimator.ofFloat( + viewBinding.txtvEpisodeTitle, "alpha", 0); + fadeOut.setStartDelay(animUnit); + fadeOut.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + viewBinding.txtvEpisodeTitle.scrollTo(0, 0); + } + }); + ObjectAnimator fadeBackIn = ObjectAnimator.ofFloat( + viewBinding.txtvEpisodeTitle, "alpha", 1); + AnimatorSet set = new AnimatorSet(); + set.playSequentially(verticalMarquee, fadeOut, fadeBackIn); + set.start(); + } + }); + + displayedChapterIndex = -1; + refreshChapterData(ChapterUtils.getCurrentChapterIndex(media, media.getPosition())); //calls displayCoverImage + updateChapterControlVisibility(); + } + + private void updateChapterControlVisibility() { + boolean chapterControlVisible = false; + if (media.getChapters() != null) { + chapterControlVisible = media.getChapters().size() > 0; + } else if (media instanceof FeedMedia) { + FeedMedia fm = ((FeedMedia) media); + // If an item has chapters but they are not loaded yet, still display the button. + chapterControlVisible = fm.getItem() != null && fm.getItem().hasChapters(); + } + int newVisibility = chapterControlVisible ? View.VISIBLE : View.GONE; + if (viewBinding.chapterButton.getVisibility() != newVisibility) { + viewBinding.chapterButton.setVisibility(newVisibility); + ObjectAnimator.ofFloat(viewBinding.chapterButton, + "alpha", + chapterControlVisible ? 0 : 1, + chapterControlVisible ? 1 : 0) + .start(); + } + } + + private void refreshChapterData(int chapterIndex) { + if (chapterIndex > -1) { + if (media.getPosition() > media.getDuration() || chapterIndex >= media.getChapters().size() - 1) { + displayedChapterIndex = media.getChapters().size() - 1; + viewBinding.butNextChapter.setVisibility(View.INVISIBLE); + } else { + displayedChapterIndex = chapterIndex; + viewBinding.butNextChapter.setVisibility(View.VISIBLE); + } + } + + displayCoverImage(); + } + + private Chapter getCurrentChapter() { + if (media == null || media.getChapters() == null || displayedChapterIndex == -1) { + return null; + } + return media.getChapters().get(displayedChapterIndex); + } + + private void seekToPrevChapter() { + Chapter curr = getCurrentChapter(); + + if (controller == null || curr == null || displayedChapterIndex == -1) { + return; + } + + if (displayedChapterIndex < 1) { + controller.seekTo(0); + } else if ((controller.getPosition() - 10000 * controller.getCurrentPlaybackSpeedMultiplier()) + < curr.getStart()) { + refreshChapterData(displayedChapterIndex - 1); + controller.seekTo((int) media.getChapters().get(displayedChapterIndex).getStart()); + } else { + controller.seekTo((int) curr.getStart()); + } + } + + private void seekToNextChapter() { + if (controller == null || media == null || media.getChapters() == null + || displayedChapterIndex == -1 || displayedChapterIndex + 1 >= media.getChapters().size()) { + return; + } + + refreshChapterData(displayedChapterIndex + 1); + controller.seekTo((int) media.getChapters().get(displayedChapterIndex).getStart()); + } + + @Override + public void onStart() { + super.onStart(); + controller = new PlaybackController(getActivity()) { + @Override + public void loadMediaInfo() { + CoverFragment.this.loadMediaInfo(false); + } + }; + controller.init(); + loadMediaInfo(false); + EventBus.getDefault().register(this); + } + + @Override + public void onStop() { + super.onStop(); + + if (disposable != null) { + disposable.dispose(); + } + controller.release(); + controller = null; + EventBus.getDefault().unregister(this); + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onEventMainThread(PlaybackPositionEvent event) { + int newChapterIndex = ChapterUtils.getCurrentChapterIndex(media, event.getPosition()); + if (newChapterIndex > -1 && newChapterIndex != displayedChapterIndex) { + refreshChapterData(newChapterIndex); + } + } + + private void displayCoverImage() { + RequestOptions options = new RequestOptions() + .dontAnimate() + .transform(new FitCenter(), + new RoundedCorners((int) (16 * getResources().getDisplayMetrics().density))); + + RequestBuilder cover = Glide.with(this) + .load(media.getImageLocation()) + .error(Glide.with(this) + .load(ImageResourceUtils.getFallbackImageLocation(media)) + .apply(options)) + .apply(options); + + if (displayedChapterIndex == -1 || media == null || media.getChapters() == null + || TextUtils.isEmpty(media.getChapters().get(displayedChapterIndex).getImageUrl())) { + cover.into(viewBinding.imgvCover); + } else { + Glide.with(this) + .load(EmbeddedChapterImage.getModelFor(media, displayedChapterIndex)) + .apply(options) + .thumbnail(cover) + .error(cover) + .into(viewBinding.imgvCover); + } + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + configureForOrientation(newConfig); + } + + private void configureForOrientation(Configuration newConfig) { + boolean isPortrait = newConfig.orientation == Configuration.ORIENTATION_PORTRAIT; + + viewBinding.coverFragment.setOrientation(isPortrait ? LinearLayout.VERTICAL : LinearLayout.HORIZONTAL); + + if (isPortrait) { + viewBinding.coverHolder.setLayoutParams(new LinearLayout.LayoutParams(MATCH_PARENT, 0, 1)); + viewBinding.coverFragmentTextContainer.setLayoutParams( + new LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT)); + } else { + viewBinding.coverHolder.setLayoutParams(new LinearLayout.LayoutParams(0, MATCH_PARENT, 1)); + viewBinding.coverFragmentTextContainer.setLayoutParams(new LinearLayout.LayoutParams(0, MATCH_PARENT, 1)); + } + + ((ViewGroup) viewBinding.episodeDetails.getParent()).removeView(viewBinding.episodeDetails); + if (isPortrait) { + viewBinding.coverFragment.addView(viewBinding.episodeDetails); + } else { + viewBinding.coverFragmentTextContainer.addView(viewBinding.episodeDetails); + } + } + + void onPlayPause() { + if (controller == null) { + return; + } + controller.playPause(); + } + + private boolean copyText(String text) { + ClipboardManager clipboardManager = ContextCompat.getSystemService(requireContext(), ClipboardManager.class); + if (clipboardManager != null) { + clipboardManager.setPrimaryClip(ClipData.newPlainText("AntennaPod", text)); + } + if (Build.VERSION.SDK_INT <= 32) { + ((MainActivity) requireActivity()).showSnackbarAbovePlayer( + getResources().getString(R.string.copied_to_clipboard), Snackbar.LENGTH_SHORT); + } + return true; + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/screen/playback/audio/ExternalPlayerFragment.java b/app/src/main/java/de/danoeh/antennapod/ui/screen/playback/audio/ExternalPlayerFragment.java new file mode 100644 index 000000000..67514697f --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/screen/playback/audio/ExternalPlayerFragment.java @@ -0,0 +1,222 @@ +package de.danoeh.antennapod.ui.screen.playback.audio; + +import android.content.Intent; +import android.os.Bundle; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.ProgressBar; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.fragment.app.Fragment; +import com.bumptech.glide.Glide; +import com.bumptech.glide.request.RequestOptions; +import com.google.android.material.bottomsheet.BottomSheetBehavior; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.event.playback.PlaybackPositionEvent; +import de.danoeh.antennapod.event.playback.PlaybackServiceEvent; +import de.danoeh.antennapod.model.playback.MediaType; +import de.danoeh.antennapod.model.playback.Playable; +import de.danoeh.antennapod.playback.base.PlayerStatus; +import de.danoeh.antennapod.playback.service.PlaybackController; +import de.danoeh.antennapod.playback.service.PlaybackService; +import de.danoeh.antennapod.ui.episodes.ImageResourceUtils; +import de.danoeh.antennapod.ui.screen.playback.PlayButton; +import io.reactivex.Maybe; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; +import io.reactivex.schedulers.Schedulers; +import org.greenrobot.eventbus.EventBus; +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; + +/** + * Fragment which is supposed to be displayed outside of the MediaplayerActivity. + */ +public class ExternalPlayerFragment extends Fragment { + public static final String TAG = "ExternalPlayerFragment"; + + private ImageView imgvCover; + private TextView txtvTitle; + private PlayButton butPlay; + private TextView feedName; + private ProgressBar progressBar; + private PlaybackController controller; + private Disposable disposable; + + public ExternalPlayerFragment() { + super(); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View root = inflater.inflate(R.layout.external_player_fragment, container, false); + imgvCover = root.findViewById(R.id.imgvCover); + txtvTitle = root.findViewById(R.id.txtvTitle); + butPlay = root.findViewById(R.id.butPlay); + feedName = root.findViewById(R.id.txtvAuthor); + progressBar = root.findViewById(R.id.episodeProgress); + + root.findViewById(R.id.fragmentLayout).setOnClickListener(v -> { + Log.d(TAG, "layoutInfo was clicked"); + + if (controller != null && controller.getMedia() != null) { + if (controller.getMedia().getMediaType() == MediaType.AUDIO) { + ((MainActivity) getActivity()).getBottomSheet().setState(BottomSheetBehavior.STATE_EXPANDED); + } else { + Intent intent = PlaybackService.getPlayerActivityIntent(getActivity(), controller.getMedia()); + startActivity(intent); + } + } + }); + return root; + } + + @Override + public void onViewCreated(@NonNull View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + butPlay.setOnClickListener(v -> { + if (controller == null) { + return; + } + if (controller.getMedia() != null && controller.getMedia().getMediaType() == MediaType.VIDEO + && controller.getStatus() != PlayerStatus.PLAYING) { + controller.playPause(); + getContext().startActivity(PlaybackService + .getPlayerActivityIntent(getContext(), controller.getMedia())); + } else { + controller.playPause(); + } + }); + loadMediaInfo(); + } + + private PlaybackController setupPlaybackController() { + return new PlaybackController(getActivity()) { + @Override + protected void updatePlayButtonShowsPlay(boolean showPlay) { + butPlay.setIsShowPlay(showPlay); + } + + @Override + public void loadMediaInfo() { + ExternalPlayerFragment.this.loadMediaInfo(); + } + + @Override + public void onPlaybackEnd() { + ((MainActivity) getActivity()).setPlayerVisible(false); + } + }; + } + + @Override + public void onStart() { + super.onStart(); + controller = setupPlaybackController(); + controller.init(); + loadMediaInfo(); + EventBus.getDefault().register(this); + } + + @Override + public void onStop() { + super.onStop(); + if (controller != null) { + controller.release(); + controller = null; + } + EventBus.getDefault().unregister(this); + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onPositionObserverUpdate(PlaybackPositionEvent event) { + if (controller == null) { + return; + } else if (controller.getPosition() == Playable.INVALID_TIME + || controller.getDuration() == Playable.INVALID_TIME) { + return; + } + progressBar.setProgress((int) + ((double) controller.getPosition() / controller.getDuration() * 100)); + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onPlaybackServiceChanged(PlaybackServiceEvent event) { + if (event.action == PlaybackServiceEvent.Action.SERVICE_SHUT_DOWN) { + ((MainActivity) getActivity()).setPlayerVisible(false); + } + } + + @Override + public void onDestroy() { + super.onDestroy(); + Log.d(TAG, "Fragment is about to be destroyed"); + if (disposable != null) { + disposable.dispose(); + } + } + + @Override + public void onPause() { + super.onPause(); + if (controller != null) { + controller.pause(); + } + } + + private void loadMediaInfo() { + Log.d(TAG, "Loading media info"); + if (controller == null) { + Log.w(TAG, "loadMediaInfo was called while PlaybackController was null!"); + return; + } + + if (disposable != null) { + disposable.dispose(); + } + disposable = Maybe.fromCallable(() -> controller.getMedia()) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(this::updateUi, + error -> Log.e(TAG, Log.getStackTraceString(error)), + () -> ((MainActivity) getActivity()).setPlayerVisible(false)); + } + + private void updateUi(Playable media) { + if (media == null) { + return; + } + ((MainActivity) getActivity()).setPlayerVisible(true); + txtvTitle.setText(media.getEpisodeTitle()); + feedName.setText(media.getFeedTitle()); + onPositionObserverUpdate(new PlaybackPositionEvent(media.getPosition(), media.getDuration())); + + RequestOptions options = new RequestOptions() + .placeholder(R.color.light_gray) + .error(R.color.light_gray) + .fitCenter() + .dontAnimate(); + + Glide.with(this) + .load(ImageResourceUtils.getEpisodeListImageLocation(media)) + .error(Glide.with(this) + .load(ImageResourceUtils.getFallbackImageLocation(media)) + .apply(options)) + .apply(options) + .into(imgvCover); + + if (controller != null && controller.isPlayingVideoLocally()) { + ((MainActivity) getActivity()).getBottomSheet().setLocked(true); + ((MainActivity) getActivity()).getBottomSheet().setState(BottomSheetBehavior.STATE_COLLAPSED); + } else { + butPlay.setVisibility(View.VISIBLE); + ((MainActivity) getActivity()).getBottomSheet().setLocked(false); + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/screen/playback/audio/ItemDescriptionFragment.java b/app/src/main/java/de/danoeh/antennapod/ui/screen/playback/audio/ItemDescriptionFragment.java new file mode 100644 index 000000000..3e7366431 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/screen/playback/audio/ItemDescriptionFragment.java @@ -0,0 +1,189 @@ +package de.danoeh.antennapod.ui.screen.playback.audio; + +import android.app.Activity; +import android.content.Context; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; + +import androidx.fragment.app.Fragment; + +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.playback.service.PlaybackController; +import de.danoeh.antennapod.storage.database.DBReader; +import de.danoeh.antennapod.ui.cleaner.ShownotesCleaner; +import de.danoeh.antennapod.model.feed.FeedMedia; +import de.danoeh.antennapod.model.playback.Playable; +import de.danoeh.antennapod.ui.view.ShownotesWebView; +import io.reactivex.Maybe; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; +import io.reactivex.schedulers.Schedulers; + +/** + * Displays the description of a Playable object in a Webview. + */ +public class ItemDescriptionFragment extends Fragment { + private static final String TAG = "ItemDescriptionFragment"; + + private static final String PREF = "ItemDescriptionFragmentPrefs"; + private static final String PREF_SCROLL_Y = "prefScrollY"; + private static final String PREF_PLAYABLE_ID = "prefPlayableId"; + + private ShownotesWebView webvDescription; + private Disposable webViewLoader; + private PlaybackController controller; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + Log.d(TAG, "Creating view"); + View root = inflater.inflate(R.layout.item_description_fragment, container, false); + webvDescription = root.findViewById(R.id.webview); + webvDescription.setTimecodeSelectedListener(time -> { + if (controller != null) { + controller.seekTo(time); + } + }); + webvDescription.setPageFinishedListener(() -> { + // Restoring the scroll position might not always work + webvDescription.postDelayed(ItemDescriptionFragment.this::restoreFromPreference, 50); + }); + + root.addOnLayoutChangeListener(new View.OnLayoutChangeListener() { + @Override + public void onLayoutChange(View v, int left, int top, int right, + int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) { + if (root.getMeasuredHeight() != webvDescription.getMinimumHeight()) { + webvDescription.setMinimumHeight(root.getMeasuredHeight()); + } + root.removeOnLayoutChangeListener(this); + } + }); + registerForContextMenu(webvDescription); + return root; + } + + @Override + public void onDestroy() { + super.onDestroy(); + Log.d(TAG, "Fragment destroyed"); + if (webvDescription != null) { + webvDescription.removeAllViews(); + webvDescription.destroy(); + } + } + + @Override + public boolean onContextItemSelected(MenuItem item) { + return webvDescription.onContextItemSelected(item); + } + + private void load() { + Log.d(TAG, "load()"); + if (webViewLoader != null) { + webViewLoader.dispose(); + } + Context context = getContext(); + if (context == null) { + return; + } + webViewLoader = Maybe.create(emitter -> { + Playable media = controller.getMedia(); + if (media == null) { + emitter.onComplete(); + return; + } + if (media instanceof FeedMedia) { + FeedMedia feedMedia = ((FeedMedia) media); + if (feedMedia.getItem() == null) { + feedMedia.setItem(DBReader.getFeedItem(feedMedia.getItemId())); + } + DBReader.loadDescriptionOfFeedItem(feedMedia.getItem()); + } + ShownotesCleaner shownotesCleaner = new ShownotesCleaner( + context, media.getDescription(), media.getDuration()); + emitter.onSuccess(shownotesCleaner.processShownotes()); + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(data -> { + webvDescription.loadDataWithBaseURL("https://127.0.0.1", data, "text/html", + "utf-8", "about:blank"); + Log.d(TAG, "Webview loaded"); + }, error -> Log.e(TAG, Log.getStackTraceString(error))); + } + + @Override + public void onPause() { + super.onPause(); + savePreference(); + } + + private void savePreference() { + Log.d(TAG, "Saving preferences"); + SharedPreferences prefs = getActivity().getSharedPreferences(PREF, Activity.MODE_PRIVATE); + SharedPreferences.Editor editor = prefs.edit(); + if (controller != null && controller.getMedia() != null && webvDescription != null) { + Log.d(TAG, "Saving scroll position: " + webvDescription.getScrollY()); + editor.putInt(PREF_SCROLL_Y, webvDescription.getScrollY()); + editor.putString(PREF_PLAYABLE_ID, controller.getMedia().getIdentifier() + .toString()); + } else { + Log.d(TAG, "savePreferences was called while media or webview was null"); + editor.putInt(PREF_SCROLL_Y, -1); + editor.putString(PREF_PLAYABLE_ID, ""); + } + editor.apply(); + } + + private boolean restoreFromPreference() { + Log.d(TAG, "Restoring from preferences"); + Activity activity = getActivity(); + if (activity != null) { + SharedPreferences prefs = activity.getSharedPreferences(PREF, Activity.MODE_PRIVATE); + String id = prefs.getString(PREF_PLAYABLE_ID, ""); + int scrollY = prefs.getInt(PREF_SCROLL_Y, -1); + if (controller != null && scrollY != -1 && controller.getMedia() != null + && id.equals(controller.getMedia().getIdentifier().toString()) + && webvDescription != null) { + Log.d(TAG, "Restored scroll Position: " + scrollY); + webvDescription.scrollTo(webvDescription.getScrollX(), scrollY); + return true; + } + } + return false; + } + + public void scrollToTop() { + webvDescription.scrollTo(0, 0); + savePreference(); + } + + @Override + public void onStart() { + super.onStart(); + controller = new PlaybackController(getActivity()) { + @Override + public void loadMediaInfo() { + load(); + } + }; + controller.init(); + load(); + } + + @Override + public void onStop() { + super.onStop(); + + if (webViewLoader != null) { + webViewLoader.dispose(); + } + controller.release(); + controller = null; + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/screen/playback/audio/NoRelayoutTextView.java b/app/src/main/java/de/danoeh/antennapod/ui/screen/playback/audio/NoRelayoutTextView.java new file mode 100644 index 000000000..86f9a53e9 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/screen/playback/audio/NoRelayoutTextView.java @@ -0,0 +1,42 @@ +package de.danoeh.antennapod.ui.screen.playback.audio; + +import android.content.Context; +import android.util.AttributeSet; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.widget.AppCompatTextView; + +public class NoRelayoutTextView extends AppCompatTextView { + private boolean requestLayoutEnabled = false; + private float maxTextLength = 0; + + public NoRelayoutTextView(@NonNull Context context) { + super(context); + } + + public NoRelayoutTextView(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + } + + public NoRelayoutTextView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @Override + public void requestLayout() { + if (requestLayoutEnabled) { + super.requestLayout(); + } + requestLayoutEnabled = false; + } + + @Override + public void setText(CharSequence text, BufferType type) { + float textLength = getPaint().measureText(text.toString()); + if (textLength > maxTextLength) { + maxTextLength = textLength; + requestLayoutEnabled = true; + } + super.setText(text, type); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/screen/playback/video/AspectRatioVideoView.java b/app/src/main/java/de/danoeh/antennapod/ui/screen/playback/video/AspectRatioVideoView.java new file mode 100644 index 000000000..59fa0a07f --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/screen/playback/video/AspectRatioVideoView.java @@ -0,0 +1,115 @@ +package de.danoeh.antennapod.ui.screen.playback.video; + +/* + * Copyright (C) Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.VideoView; + +public class AspectRatioVideoView extends VideoView { + + + private int mVideoWidth; + private int mVideoHeight; + private float mAvailableWidth = -1; + private float mAvailableHeight = -1; + + public AspectRatioVideoView(Context context) { + this(context, null); + } + + public AspectRatioVideoView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public AspectRatioVideoView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + mVideoWidth = 0; + mVideoHeight = 0; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + if (mVideoWidth <= 0 || mVideoHeight <= 0) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + return; + } + + if (mAvailableWidth < 0 || mAvailableHeight < 0) { + mAvailableWidth = getWidth(); + mAvailableHeight = getHeight(); + } + + float heightRatio = (float) mVideoHeight / mAvailableHeight; + float widthRatio = (float) mVideoWidth / mAvailableWidth; + + int scaledHeight; + int scaledWidth; + + if (heightRatio > widthRatio) { + scaledHeight = (int) Math.ceil((float) mVideoHeight + / heightRatio); + scaledWidth = (int) Math.ceil((float) mVideoWidth + / heightRatio); + } else { + scaledHeight = (int) Math.ceil((float) mVideoHeight + / widthRatio); + scaledWidth = (int) Math.ceil((float) mVideoWidth + / widthRatio); + } + + setMeasuredDimension(scaledWidth, scaledHeight); + } + + /** + * Source code originally from: + * http://clseto.mysinablog.com/index.php?op=ViewArticle&articleId=2992625 + * + * @param videoWidth + * @param videoHeight + */ + public void setVideoSize(int videoWidth, int videoHeight) { + // Set the new video size + mVideoWidth = videoWidth; + mVideoHeight = videoHeight; + + /* + * If this isn't set the video is stretched across the + * SurfaceHolders display surface (i.e. the SurfaceHolder + * as the same size and the video is drawn to fit this + * display area). We want the size to be the video size + * and allow the aspectratio to handle how the surface is shown + */ + getHolder().setFixedSize(videoWidth, videoHeight); + + requestLayout(); + invalidate(); + } + + /** + * Sets the maximum size that the view might expand to + * @param width + * @param height + */ + public void setAvailableSize(float width, float height) { + mAvailableWidth = width; + mAvailableHeight = height; + requestLayout(); + } + +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/screen/playback/video/PictureInPictureUtil.java b/app/src/main/java/de/danoeh/antennapod/ui/screen/playback/video/PictureInPictureUtil.java new file mode 100644 index 000000000..7f40610cc --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/screen/playback/video/PictureInPictureUtil.java @@ -0,0 +1,27 @@ +package de.danoeh.antennapod.ui.screen.playback.video; + +import android.app.Activity; +import android.content.pm.PackageManager; +import android.os.Build; + +public class PictureInPictureUtil { + private PictureInPictureUtil() { + } + + public static boolean supportsPictureInPicture(Activity activity) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + PackageManager packageManager = activity.getPackageManager(); + return packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE); + } else { + return false; + } + } + + public static boolean isInPictureInPictureMode(Activity activity) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && supportsPictureInPicture(activity)) { + return activity.isInPictureInPictureMode(); + } else { + return false; + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/screen/playback/video/VideoplayerActivity.java b/app/src/main/java/de/danoeh/antennapod/ui/screen/playback/video/VideoplayerActivity.java new file mode 100644 index 000000000..2a0746f54 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/screen/playback/video/VideoplayerActivity.java @@ -0,0 +1,815 @@ +package de.danoeh.antennapod.ui.screen.playback.video; + +import android.content.Intent; +import android.graphics.PixelFormat; +import android.graphics.drawable.ColorDrawable; +import android.media.AudioManager; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; +import android.util.Pair; +import android.view.Gravity; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.MotionEvent; +import android.view.SurfaceHolder; +import android.view.View; +import android.view.Window; +import android.view.WindowManager; +import android.view.animation.AlphaAnimation; +import android.view.animation.Animation; +import android.view.animation.AnimationSet; +import android.view.animation.AnimationUtils; +import android.view.animation.ScaleAnimation; +import android.widget.EditText; +import android.widget.FrameLayout; +import android.widget.SeekBar; +import androidx.annotation.Nullable; +import androidx.interpolator.view.animation.FastOutSlowInInterpolator; +import com.bumptech.glide.Glide; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.event.MessageEvent; +import de.danoeh.antennapod.event.playback.BufferUpdateEvent; +import de.danoeh.antennapod.event.playback.PlaybackPositionEvent; +import de.danoeh.antennapod.event.PlayerErrorEvent; +import de.danoeh.antennapod.event.playback.PlaybackServiceEvent; +import de.danoeh.antennapod.event.playback.SleepTimerUpdatedEvent; +import de.danoeh.antennapod.ui.screen.chapter.ChaptersFragment; +import de.danoeh.antennapod.playback.service.PlaybackController; +import de.danoeh.antennapod.playback.service.PlaybackService; +import de.danoeh.antennapod.storage.preferences.UserPreferences; +import de.danoeh.antennapod.storage.database.DBReader; +import de.danoeh.antennapod.storage.database.DBWriter; +import de.danoeh.antennapod.ui.common.Converter; +import de.danoeh.antennapod.core.util.FeedItemUtil; +import de.danoeh.antennapod.core.util.IntentUtils; +import de.danoeh.antennapod.ui.share.ShareUtils; +import de.danoeh.antennapod.databinding.VideoplayerActivityBinding; +import de.danoeh.antennapod.ui.share.ShareDialog; +import de.danoeh.antennapod.ui.screen.feed.preferences.SkipPreferenceDialog; +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedMedia; +import de.danoeh.antennapod.model.playback.Playable; +import de.danoeh.antennapod.playback.base.PlayerStatus; +import de.danoeh.antennapod.playback.cast.CastEnabledActivity; +import de.danoeh.antennapod.ui.appstartintent.MainActivityStarter; +import de.danoeh.antennapod.ui.episodes.TimeSpeedConverter; +import de.danoeh.antennapod.ui.screen.playback.MediaPlayerErrorDialog; +import de.danoeh.antennapod.ui.screen.playback.PlaybackControlsDialog; +import de.danoeh.antennapod.ui.screen.playback.SleepTimerDialog; +import de.danoeh.antennapod.ui.screen.playback.VariableSpeedDialog; +import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; +import io.reactivex.schedulers.Schedulers; +import org.apache.commons.lang3.StringUtils; +import org.greenrobot.eventbus.EventBus; +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; + +/** + * Activity for playing video files. + */ +public class VideoplayerActivity extends CastEnabledActivity implements SeekBar.OnSeekBarChangeListener { + private static final String TAG = "VideoplayerActivity"; + + /** + * True if video controls are currently visible. + */ + private boolean videoControlsShowing = true; + private boolean videoSurfaceCreated = false; + private boolean destroyingDueToReload = false; + private long lastScreenTap = 0; + private final Handler videoControlsHider = new Handler(Looper.getMainLooper()); + private VideoplayerActivityBinding viewBinding; + private PlaybackController controller; + private boolean showTimeLeft = false; + private boolean isFavorite = false; + private boolean switchToAudioOnly = false; + private Disposable disposable; + private float prog; + + @Override + protected void onCreate(Bundle savedInstanceState) { + getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + getWindow().addFlags(WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN); + // has to be called before setting layout content + supportRequestWindowFeature(Window.FEATURE_ACTION_BAR_OVERLAY); + setTheme(R.style.Theme_AntennaPod_VideoPlayer); + super.onCreate(savedInstanceState); + + Log.d(TAG, "onCreate()"); + + getWindow().setFormat(PixelFormat.TRANSPARENT); + viewBinding = VideoplayerActivityBinding.inflate(LayoutInflater.from(this)); + setContentView(viewBinding.getRoot()); + setupView(); + getSupportActionBar().setBackgroundDrawable(new ColorDrawable(0x80000000)); + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + } + + @Override + protected void onResume() { + super.onResume(); + switchToAudioOnly = false; + if (PlaybackService.isCasting()) { + Intent intent = PlaybackService.getPlayerActivityIntent(this); + if (!intent.getComponent().getClassName().equals(VideoplayerActivity.class.getName())) { + destroyingDueToReload = true; + finish(); + startActivity(intent); + } + } + } + + @Override + protected void onStop() { + if (controller != null) { + controller.release(); + controller = null; // prevent leak + } + if (disposable != null) { + disposable.dispose(); + } + EventBus.getDefault().unregister(this); + super.onStop(); + if (!PictureInPictureUtil.isInPictureInPictureMode(this)) { + videoControlsHider.removeCallbacks(hideVideoControls); + } + // Controller released; we will not receive buffering updates + viewBinding.progressBar.setVisibility(View.GONE); + } + + @Override + public void onUserLeaveHint() { + if (!PictureInPictureUtil.isInPictureInPictureMode(this)) { + compatEnterPictureInPicture(); + } + } + + @Override + protected void onStart() { + super.onStart(); + controller = newPlaybackController(); + controller.init(); + loadMediaInfo(); + onPositionObserverUpdate(); + EventBus.getDefault().register(this); + } + + @Override + protected void onPause() { + if (!PictureInPictureUtil.isInPictureInPictureMode(this)) { + if (controller != null && controller.getStatus() == PlayerStatus.PLAYING) { + controller.pause(); + } + } + super.onPause(); + } + + @Override + public void onTrimMemory(int level) { + super.onTrimMemory(level); + Glide.get(this).trimMemory(level); + } + + @Override + public void onLowMemory() { + super.onLowMemory(); + Glide.get(this).clearMemory(); + } + + private PlaybackController newPlaybackController() { + return new PlaybackController(this) { + @Override + protected void updatePlayButtonShowsPlay(boolean showPlay) { + viewBinding.playButton.setIsShowPlay(showPlay); + if (showPlay) { + getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } else { + getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + setupVideoAspectRatio(); + if (videoSurfaceCreated && controller != null) { + Log.d(TAG, "Videosurface already created, setting videosurface now"); + controller.setVideoSurface(viewBinding.videoView.getHolder()); + } + } + } + + @Override + public void loadMediaInfo() { + VideoplayerActivity.this.loadMediaInfo(); + } + + @Override + public void onPlaybackEnd() { + finish(); + } + }; + } + + @Subscribe(threadMode = ThreadMode.MAIN) + @SuppressWarnings("unused") + public void bufferUpdate(BufferUpdateEvent event) { + if (event.hasStarted()) { + viewBinding.progressBar.setVisibility(View.VISIBLE); + } else if (event.hasEnded()) { + viewBinding.progressBar.setVisibility(View.INVISIBLE); + } else { + viewBinding.sbPosition.setSecondaryProgress((int) (event.getProgress() * viewBinding.sbPosition.getMax())); + } + } + + @Subscribe(threadMode = ThreadMode.MAIN) + @SuppressWarnings("unused") + public void sleepTimerUpdate(SleepTimerUpdatedEvent event) { + if (event.isCancelled() || event.wasJustEnabled()) { + supportInvalidateOptionsMenu(); + } + } + + protected void loadMediaInfo() { + Log.d(TAG, "loadMediaInfo()"); + if (controller == null || controller.getMedia() == null) { + return; + } + if (controller.getStatus() == PlayerStatus.PLAYING && !controller.isPlayingVideoLocally()) { + Log.d(TAG, "Closing, no longer video"); + destroyingDueToReload = true; + finish(); + new MainActivityStarter(this).withOpenPlayer().start(); + return; + } + showTimeLeft = UserPreferences.shouldShowRemainingTime(); + onPositionObserverUpdate(); + checkFavorite(); + Playable media = controller.getMedia(); + if (media != null) { + getSupportActionBar().setSubtitle(media.getEpisodeTitle()); + getSupportActionBar().setTitle(media.getFeedTitle()); + } + } + + protected void setupView() { + showTimeLeft = UserPreferences.shouldShowRemainingTime(); + Log.d("timeleft", showTimeLeft ? "true" : "false"); + viewBinding.durationLabel.setOnClickListener(v -> { + showTimeLeft = !showTimeLeft; + Playable media = controller.getMedia(); + if (media == null) { + return; + } + + TimeSpeedConverter converter = new TimeSpeedConverter(controller.getCurrentPlaybackSpeedMultiplier()); + String length; + if (showTimeLeft) { + int remainingTime = converter.convert(media.getDuration() - media.getPosition()); + length = "-" + Converter.getDurationStringLong(remainingTime); + } else { + int duration = converter.convert(media.getDuration()); + length = Converter.getDurationStringLong(duration); + } + viewBinding.durationLabel.setText(length); + + UserPreferences.setShowRemainTimeSetting(showTimeLeft); + Log.d("timeleft on click", showTimeLeft ? "true" : "false"); + }); + + viewBinding.sbPosition.setOnSeekBarChangeListener(this); + viewBinding.rewindButton.setOnClickListener(v -> onRewind()); + viewBinding.rewindButton.setOnLongClickListener(v -> { + SkipPreferenceDialog.showSkipPreference(VideoplayerActivity.this, + SkipPreferenceDialog.SkipDirection.SKIP_REWIND, null); + return true; + }); + viewBinding.playButton.setIsVideoScreen(true); + viewBinding.playButton.setOnClickListener(v -> onPlayPause()); + viewBinding.fastForwardButton.setOnClickListener(v -> onFastForward()); + viewBinding.fastForwardButton.setOnLongClickListener(v -> { + SkipPreferenceDialog.showSkipPreference(VideoplayerActivity.this, + SkipPreferenceDialog.SkipDirection.SKIP_FORWARD, null); + return false; + }); + // To suppress touches directly below the slider + viewBinding.bottomControlsContainer.setOnTouchListener((view, motionEvent) -> true); + viewBinding.bottomControlsContainer.setFitsSystemWindows(true); + viewBinding.videoView.getHolder().addCallback(surfaceHolderCallback); + viewBinding.videoView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION); + + setupVideoControlsToggler(); + getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, + WindowManager.LayoutParams.FLAG_FULLSCREEN); + + viewBinding.videoPlayerContainer.setOnTouchListener(onVideoviewTouched); + viewBinding.videoPlayerContainer.getViewTreeObserver().addOnGlobalLayoutListener(() -> + viewBinding.videoView.setAvailableSize( + viewBinding.videoPlayerContainer.getWidth(), viewBinding.videoPlayerContainer.getHeight())); + } + + private final Runnable hideVideoControls = () -> { + if (videoControlsShowing) { + Log.d(TAG, "Hiding video controls"); + getSupportActionBar().hide(); + hideVideoControls(true); + videoControlsShowing = false; + } + }; + + private final View.OnTouchListener onVideoviewTouched = (v, event) -> { + if (event.getAction() != MotionEvent.ACTION_DOWN) { + return false; + } + if (PictureInPictureUtil.isInPictureInPictureMode(this)) { + return true; + } + videoControlsHider.removeCallbacks(hideVideoControls); + + if (System.currentTimeMillis() - lastScreenTap < 300) { + if (event.getX() > v.getMeasuredWidth() / 2.0f) { + onFastForward(); + showSkipAnimation(true); + } else { + onRewind(); + showSkipAnimation(false); + } + if (videoControlsShowing) { + getSupportActionBar().hide(); + hideVideoControls(false); + videoControlsShowing = false; + } + return true; + } + + toggleVideoControlsVisibility(); + if (videoControlsShowing) { + setupVideoControlsToggler(); + } + + lastScreenTap = System.currentTimeMillis(); + return true; + }; + + private void showSkipAnimation(boolean isForward) { + AnimationSet skipAnimation = new AnimationSet(true); + skipAnimation.addAnimation(new ScaleAnimation(1f, 2f, 1f, 2f, + Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f)); + skipAnimation.addAnimation(new AlphaAnimation(1f, 0f)); + skipAnimation.setFillAfter(false); + skipAnimation.setDuration(800); + + FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) viewBinding.skipAnimationImage.getLayoutParams(); + if (isForward) { + viewBinding.skipAnimationImage.setImageResource(R.drawable.ic_fast_forward_video_white); + params.gravity = Gravity.RIGHT | Gravity.CENTER_VERTICAL; + } else { + viewBinding.skipAnimationImage.setImageResource(R.drawable.ic_fast_rewind_video_white); + params.gravity = Gravity.LEFT | Gravity.CENTER_VERTICAL; + } + + viewBinding.skipAnimationImage.setVisibility(View.VISIBLE); + viewBinding.skipAnimationImage.setLayoutParams(params); + viewBinding.skipAnimationImage.startAnimation(skipAnimation); + skipAnimation.setAnimationListener(new Animation.AnimationListener() { + @Override + public void onAnimationStart(Animation animation) { + } + + @Override + public void onAnimationEnd(Animation animation) { + viewBinding.skipAnimationImage.setVisibility(View.GONE); + } + + @Override + public void onAnimationRepeat(Animation animation) { + + } + }); + } + + private void setupVideoControlsToggler() { + videoControlsHider.removeCallbacks(hideVideoControls); + videoControlsHider.postDelayed(hideVideoControls, 2500); + } + + private void setupVideoAspectRatio() { + if (videoSurfaceCreated && controller != null) { + Pair videoSize = controller.getVideoSize(); + if (videoSize != null && videoSize.first > 0 && videoSize.second > 0) { + Log.d(TAG, "Width,height of video: " + videoSize.first + ", " + videoSize.second); + viewBinding.videoView.setVideoSize(videoSize.first, videoSize.second); + } else { + Log.e(TAG, "Could not determine video size"); + } + } + } + + private void toggleVideoControlsVisibility() { + if (videoControlsShowing) { + getSupportActionBar().hide(); + hideVideoControls(true); + } else { + getSupportActionBar().show(); + showVideoControls(); + } + videoControlsShowing = !videoControlsShowing; + } + + void onRewind() { + if (controller == null) { + return; + } + int curr = controller.getPosition(); + controller.seekTo(curr - UserPreferences.getRewindSecs() * 1000); + setupVideoControlsToggler(); + } + + void onPlayPause() { + if (controller == null) { + return; + } + controller.playPause(); + setupVideoControlsToggler(); + } + + void onFastForward() { + if (controller == null) { + return; + } + int curr = controller.getPosition(); + controller.seekTo(curr + UserPreferences.getFastForwardSecs() * 1000); + setupVideoControlsToggler(); + } + + private final SurfaceHolder.Callback surfaceHolderCallback = new SurfaceHolder.Callback() { + @Override + public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { + holder.setFixedSize(width, height); + } + + @Override + public void surfaceCreated(SurfaceHolder holder) { + Log.d(TAG, "Videoview holder created"); + videoSurfaceCreated = true; + if (controller != null && controller.getStatus() == PlayerStatus.PLAYING) { + controller.setVideoSurface(holder); + } + setupVideoAspectRatio(); + } + + @Override + public void surfaceDestroyed(SurfaceHolder holder) { + Log.d(TAG, "Videosurface was destroyed"); + videoSurfaceCreated = false; + if (controller != null && !destroyingDueToReload && !switchToAudioOnly) { + controller.notifyVideoSurfaceAbandoned(); + } + } + }; + + private void showVideoControls() { + viewBinding.bottomControlsContainer.setVisibility(View.VISIBLE); + viewBinding.controlsContainer.setVisibility(View.VISIBLE); + final Animation animation = AnimationUtils.loadAnimation(this, R.anim.fade_in); + if (animation != null) { + viewBinding.bottomControlsContainer.startAnimation(animation); + viewBinding.controlsContainer.startAnimation(animation); + } + viewBinding.videoView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_VISIBLE); + } + + private void hideVideoControls(boolean showAnimation) { + if (showAnimation) { + final Animation animation = AnimationUtils.loadAnimation(this, R.anim.fade_out); + if (animation != null) { + viewBinding.bottomControlsContainer.startAnimation(animation); + viewBinding.controlsContainer.startAnimation(animation); + } + } + getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE + | View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION); + viewBinding.bottomControlsContainer.setFitsSystemWindows(true); + + viewBinding.bottomControlsContainer.setVisibility(View.GONE); + viewBinding.controlsContainer.setVisibility(View.GONE); + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onEventMainThread(PlaybackPositionEvent event) { + onPositionObserverUpdate(); + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onPlaybackServiceChanged(PlaybackServiceEvent event) { + if (event.action == PlaybackServiceEvent.Action.SERVICE_SHUT_DOWN) { + finish(); + } + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onMediaPlayerError(PlayerErrorEvent event) { + MediaPlayerErrorDialog.show(this, event); + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onEventMainThread(MessageEvent event) { + Log.d(TAG, "onEvent(" + event + ")"); + final MaterialAlertDialogBuilder errorDialog = new MaterialAlertDialogBuilder(this); + errorDialog.setMessage(event.message); + if (event.action != null) { + errorDialog.setPositiveButton(event.actionText, (dialog, which) -> event.action.accept(this)); + } + errorDialog.show(); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + super.onCreateOptionsMenu(menu); + requestCastButton(menu); + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.mediaplayer, menu); + return true; + } + + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + super.onPrepareOptionsMenu(menu); + if (controller == null) { + return false; + } + Playable media = controller.getMedia(); + boolean isFeedMedia = (media instanceof FeedMedia); + + menu.findItem(R.id.open_feed_item).setVisible(isFeedMedia); // FeedMedia implies it belongs to a Feed + + boolean hasWebsiteLink = getWebsiteLinkWithFallback(media) != null; + menu.findItem(R.id.visit_website_item).setVisible(hasWebsiteLink); + + boolean isItemAndHasLink = isFeedMedia && ShareUtils.hasLinkToShare(((FeedMedia) media).getItem()); + boolean isItemHasDownloadLink = isFeedMedia && ((FeedMedia) media).getDownloadUrl() != null; + menu.findItem(R.id.share_item).setVisible(hasWebsiteLink || isItemAndHasLink || isItemHasDownloadLink); + + menu.findItem(R.id.add_to_favorites_item).setVisible(false); + menu.findItem(R.id.remove_from_favorites_item).setVisible(false); + if (isFeedMedia) { + menu.findItem(R.id.add_to_favorites_item).setVisible(!isFavorite); + menu.findItem(R.id.remove_from_favorites_item).setVisible(isFavorite); + } + + menu.findItem(R.id.set_sleeptimer_item).setVisible(!controller.sleepTimerActive()); + menu.findItem(R.id.disable_sleeptimer_item).setVisible(controller.sleepTimerActive()); + + menu.findItem(R.id.player_switch_to_audio_only).setVisible(true); + + menu.findItem(R.id.audio_controls).setVisible(controller.getAudioTracks().size() >= 2); + menu.findItem(R.id.playback_speed).setVisible(true); + menu.findItem(R.id.player_show_chapters).setVisible(true); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == R.id.player_switch_to_audio_only) { + switchToAudioOnly = true; + finish(); + return true; + } else if (item.getItemId() == android.R.id.home) { + Intent intent = new Intent(VideoplayerActivity.this, MainActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(intent); + finish(); + return true; + } else if (item.getItemId() == R.id.player_show_chapters) { + new ChaptersFragment().show(getSupportFragmentManager(), ChaptersFragment.TAG); + return true; + } + + if (controller == null) { + return false; + } + + Playable media = controller.getMedia(); + if (media == null) { + return false; + } + final @Nullable FeedItem feedItem = getFeedItem(media); // some options option requires FeedItem + if (item.getItemId() == R.id.add_to_favorites_item && feedItem != null) { + DBWriter.addFavoriteItem(feedItem); + isFavorite = true; + invalidateOptionsMenu(); + } else if (item.getItemId() == R.id.remove_from_favorites_item && feedItem != null) { + DBWriter.removeFavoriteItem(feedItem); + isFavorite = false; + invalidateOptionsMenu(); + } else if (item.getItemId() == R.id.disable_sleeptimer_item + || item.getItemId() == R.id.set_sleeptimer_item) { + new SleepTimerDialog().show(getSupportFragmentManager(), "SleepTimerDialog"); + } else if (item.getItemId() == R.id.audio_controls) { + PlaybackControlsDialog dialog = PlaybackControlsDialog.newInstance(); + dialog.show(getSupportFragmentManager(), "playback_controls"); + } else if (item.getItemId() == R.id.open_feed_item && feedItem != null) { + Intent intent = MainActivity.getIntentToOpenFeed(this, feedItem.getFeedId()); + startActivity(intent); + } else if (item.getItemId() == R.id.visit_website_item) { + IntentUtils.openInBrowser(VideoplayerActivity.this, getWebsiteLinkWithFallback(media)); + } else if (item.getItemId() == R.id.share_item && feedItem != null) { + ShareDialog shareDialog = ShareDialog.newInstance(feedItem); + shareDialog.show(getSupportFragmentManager(), "ShareEpisodeDialog"); + } else if (item.getItemId() == R.id.playback_speed) { + new VariableSpeedDialog().show(getSupportFragmentManager(), null); + } else { + return false; + } + return true; + } + + private static String getWebsiteLinkWithFallback(Playable media) { + if (media == null) { + return null; + } else if (StringUtils.isNotBlank(media.getWebsiteLink())) { + return media.getWebsiteLink(); + } else if (media instanceof FeedMedia) { + return FeedItemUtil.getLinkWithFallback(((FeedMedia) media).getItem()); + } + return null; + } + + void onPositionObserverUpdate() { + if (controller == null) { + return; + } + + TimeSpeedConverter converter = new TimeSpeedConverter(controller.getCurrentPlaybackSpeedMultiplier()); + int currentPosition = converter.convert(controller.getPosition()); + int duration = converter.convert(controller.getDuration()); + int remainingTime = converter.convert( + controller.getDuration() - controller.getPosition()); + Log.d(TAG, "currentPosition " + Converter.getDurationStringLong(currentPosition)); + if (currentPosition == Playable.INVALID_TIME + || duration == Playable.INVALID_TIME) { + Log.w(TAG, "Could not react to position observer update because of invalid time"); + return; + } + viewBinding.positionLabel.setText(Converter.getDurationStringLong(currentPosition)); + if (showTimeLeft) { + viewBinding.durationLabel.setText("-" + Converter.getDurationStringLong(remainingTime)); + } else { + viewBinding.durationLabel.setText(Converter.getDurationStringLong(duration)); + } + updateProgressbarPosition(currentPosition, duration); + } + + private void updateProgressbarPosition(int position, int duration) { + Log.d(TAG, "updateProgressbarPosition(" + position + ", " + duration + ")"); + float progress = ((float) position) / duration; + viewBinding.sbPosition.setProgress((int) (progress * viewBinding.sbPosition.getMax())); + } + + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + if (controller == null) { + return; + } + if (fromUser) { + prog = progress / ((float) seekBar.getMax()); + TimeSpeedConverter converter = new TimeSpeedConverter(controller.getCurrentPlaybackSpeedMultiplier()); + int position = converter.convert((int) (prog * controller.getDuration())); + viewBinding.seekPositionLabel.setText(Converter.getDurationStringLong(position)); + } + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + viewBinding.seekCardView.setScaleX(.8f); + viewBinding.seekCardView.setScaleY(.8f); + viewBinding.seekCardView.animate() + .setInterpolator(new FastOutSlowInInterpolator()) + .alpha(1f).scaleX(1f).scaleY(1f) + .setDuration(200) + .start(); + videoControlsHider.removeCallbacks(hideVideoControls); + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + if (controller != null) { + controller.seekTo((int) (prog * controller.getDuration())); + } + viewBinding.seekCardView.setScaleX(1f); + viewBinding.seekCardView.setScaleY(1f); + viewBinding.seekCardView.animate() + .setInterpolator(new FastOutSlowInInterpolator()) + .alpha(0f).scaleX(.8f).scaleY(.8f) + .setDuration(200) + .start(); + setupVideoControlsToggler(); + } + + private void checkFavorite() { + FeedItem feedItem = getFeedItem(controller.getMedia()); + if (feedItem == null) { + return; + } + if (disposable != null) { + disposable.dispose(); + } + disposable = Observable.fromCallable(() -> DBReader.getFeedItem(feedItem.getId())) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + item -> { + boolean isFav = item.isTagged(FeedItem.TAG_FAVORITE); + if (isFavorite != isFav) { + isFavorite = isFav; + invalidateOptionsMenu(); + } + }, error -> Log.e(TAG, Log.getStackTraceString(error))); + } + + @Nullable + private static FeedItem getFeedItem(@Nullable Playable playable) { + if (playable instanceof FeedMedia) { + return ((FeedMedia) playable).getItem(); + } else { + return null; + } + } + + private void compatEnterPictureInPicture() { + if (PictureInPictureUtil.supportsPictureInPicture(this) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + getSupportActionBar().hide(); + hideVideoControls(false); + enterPictureInPictureMode(); + } + } + + //Hardware keyboard support + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + View currentFocus = getCurrentFocus(); + if (currentFocus instanceof EditText) { + return super.onKeyUp(keyCode, event); + } + + AudioManager audioManager = (AudioManager) getSystemService(AUDIO_SERVICE); + + switch (keyCode) { + case KeyEvent.KEYCODE_P: //Fallthrough + case KeyEvent.KEYCODE_SPACE: + onPlayPause(); + toggleVideoControlsVisibility(); + return true; + case KeyEvent.KEYCODE_J: //Fallthrough + case KeyEvent.KEYCODE_A: + case KeyEvent.KEYCODE_COMMA: + onRewind(); + showSkipAnimation(false); + return true; + case KeyEvent.KEYCODE_K: //Fallthrough + case KeyEvent.KEYCODE_D: + case KeyEvent.KEYCODE_PERIOD: + onFastForward(); + showSkipAnimation(true); + return true; + case KeyEvent.KEYCODE_F: //Fallthrough + case KeyEvent.KEYCODE_ESCAPE: + //Exit fullscreen mode + onBackPressed(); + return true; + case KeyEvent.KEYCODE_I: + compatEnterPictureInPicture(); + return true; + case KeyEvent.KEYCODE_PLUS: //Fallthrough + case KeyEvent.KEYCODE_W: + audioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC, + AudioManager.ADJUST_RAISE, AudioManager.FLAG_SHOW_UI); + return true; + case KeyEvent.KEYCODE_MINUS: //Fallthrough + case KeyEvent.KEYCODE_S: + audioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC, + AudioManager.ADJUST_LOWER, AudioManager.FLAG_SHOW_UI); + return true; + case KeyEvent.KEYCODE_M: + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + audioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC, + AudioManager.ADJUST_TOGGLE_MUTE, AudioManager.FLAG_SHOW_UI); + return true; + } + break; + } + + //Go to x% of video: + if (keyCode >= KeyEvent.KEYCODE_0 && keyCode <= KeyEvent.KEYCODE_9) { + controller.seekTo((int) (0.1f * (keyCode - KeyEvent.KEYCODE_0) * controller.getDuration())); + return true; + } + return super.onKeyUp(keyCode, event); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/screen/preferences/BugReportActivity.java b/app/src/main/java/de/danoeh/antennapod/ui/screen/preferences/BugReportActivity.java new file mode 100644 index 000000000..b4a5218ee --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/screen/preferences/BugReportActivity.java @@ -0,0 +1,128 @@ +package de.danoeh.antennapod.ui.screen.preferences; + +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.util.Log; +import com.google.android.material.snackbar.Snackbar; + +import androidx.annotation.NonNull; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.app.ShareCompat; +import androidx.core.content.FileProvider; + + +import android.view.Menu; +import android.view.MenuItem; +import android.widget.TextView; + + +import de.danoeh.antennapod.ui.common.ThemeSwitcher; +import de.danoeh.antennapod.CrashReportWriter; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.storage.preferences.UserPreferences; +import de.danoeh.antennapod.core.util.IntentUtils; +import org.apache.commons.io.IOUtils; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.nio.charset.Charset; + +/** + * Displays the 'crash report' screen + */ +public class BugReportActivity extends AppCompatActivity { + private static final String TAG = "BugReportActivity"; + + @Override + protected void onCreate(Bundle savedInstanceState) { + setTheme(ThemeSwitcher.getTheme(this)); + super.onCreate(savedInstanceState); + getSupportActionBar().setDisplayShowHomeEnabled(true); + setContentView(R.layout.bug_report); + + String stacktrace = "No crash report recorded"; + try { + File crashFile = CrashReportWriter.getFile(); + if (crashFile.exists()) { + stacktrace = IOUtils.toString(new FileInputStream(crashFile), Charset.forName("UTF-8")); + } else { + Log.d(TAG, stacktrace); + } + } catch (IOException e) { + e.printStackTrace(); + } + + TextView crashDetailsTextView = findViewById(R.id.crash_report_logs); + crashDetailsTextView.setText(CrashReportWriter.getSystemInfo() + "\n\n" + stacktrace); + + findViewById(R.id.btn_open_bug_tracker).setOnClickListener(v -> IntentUtils.openInBrowser( + BugReportActivity.this, "https://github.com/AntennaPod/AntennaPod/issues")); + + findViewById(R.id.btn_copy_log).setOnClickListener(v -> { + ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); + ClipData clip = ClipData.newPlainText(getString(R.string.bug_report_title), crashDetailsTextView.getText()); + clipboard.setPrimaryClip(clip); + if (Build.VERSION.SDK_INT < 32) { + Snackbar.make(findViewById(android.R.id.content), R.string.copied_to_clipboard, + Snackbar.LENGTH_SHORT).show(); + } + }); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.bug_report_options, menu); + return super.onCreateOptionsMenu(menu); + } + + @Override + public boolean onOptionsItemSelected(@NonNull MenuItem item) { + if (item.getItemId() == R.id.export_logcat) { + MaterialAlertDialogBuilder alertBuilder = new MaterialAlertDialogBuilder(this); + alertBuilder.setMessage(R.string.confirm_export_log_dialog_message); + alertBuilder.setPositiveButton(R.string.confirm_label, (dialog, which) -> { + exportLog(); + dialog.dismiss(); + }); + alertBuilder.setNegativeButton(R.string.cancel_label, null); + alertBuilder.show(); + return true; + } + return super.onOptionsItemSelected(item); + } + + private void exportLog() { + try { + File filename = new File(UserPreferences.getDataFolder(null), "full-logs.txt"); + String cmd = "logcat -d -f " + filename.getAbsolutePath(); + Runtime.getRuntime().exec(cmd); + //share file + try { + String authority = getString(R.string.provider_authority); + Uri fileUri = FileProvider.getUriForFile(this, authority, filename); + + new ShareCompat.IntentBuilder(this) + .setType("text/*") + .addStream(fileUri) + .setChooserTitle(R.string.share_file_label) + .startChooser(); + } catch (Exception e) { + e.printStackTrace(); + int strResId = R.string.log_file_share_exception; + Snackbar.make(findViewById(android.R.id.content), strResId, Snackbar.LENGTH_LONG) + .show(); + } + } catch (IOException e) { + e.printStackTrace(); + Snackbar.make(findViewById(android.R.id.content), e.getMessage(), Snackbar.LENGTH_LONG).show(); + } + } + + +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/screen/preferences/DownloadsPreferencesFragment.java b/app/src/main/java/de/danoeh/antennapod/ui/screen/preferences/DownloadsPreferencesFragment.java new file mode 100644 index 000000000..fad8c6986 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/screen/preferences/DownloadsPreferencesFragment.java @@ -0,0 +1,106 @@ +package de.danoeh.antennapod.ui.screen.preferences; + +import android.content.SharedPreferences; +import android.os.Bundle; + +import androidx.preference.PreferenceFragmentCompat; +import androidx.preference.PreferenceManager; +import androidx.preference.TwoStatePreference; + +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.net.download.serviceinterface.FeedUpdateManager; +import de.danoeh.antennapod.ui.preferences.screen.downloads.ChooseDataFolderDialog; +import de.danoeh.antennapod.storage.preferences.UserPreferences; + +import java.io.File; + + +public class DownloadsPreferencesFragment extends PreferenceFragmentCompat + implements SharedPreferences.OnSharedPreferenceChangeListener { + private static final String PREF_SCREEN_AUTODL = "prefAutoDownloadSettings"; + private static final String PREF_AUTO_DELETE_LOCAL = "prefAutoDeleteLocal"; + private static final String PREF_PROXY = "prefProxy"; + private static final String PREF_CHOOSE_DATA_DIR = "prefChooseDataDir"; + + private boolean blockAutoDeleteLocal = true; + + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + addPreferencesFromResource(R.xml.preferences_downloads); + setupNetworkScreen(); + } + + @Override + public void onStart() { + super.onStart(); + ((PreferenceActivity) getActivity()).getSupportActionBar().setTitle(R.string.downloads_pref); + PreferenceManager.getDefaultSharedPreferences(getContext()).registerOnSharedPreferenceChangeListener(this); + } + + @Override + public void onStop() { + super.onStop(); + PreferenceManager.getDefaultSharedPreferences(getContext()).unregisterOnSharedPreferenceChangeListener(this); + } + + @Override + public void onResume() { + super.onResume(); + setDataFolderText(); + } + + private void setupNetworkScreen() { + findPreference(PREF_SCREEN_AUTODL).setOnPreferenceClickListener(preference -> { + ((PreferenceActivity) getActivity()).openScreen(R.xml.preferences_autodownload); + return true; + }); + // validate and set correct value: number of downloads between 1 and 50 (inclusive) + findPreference(PREF_PROXY).setOnPreferenceClickListener(preference -> { + ProxyDialog dialog = new ProxyDialog(getActivity()); + dialog.show(); + return true; + }); + findPreference(PREF_CHOOSE_DATA_DIR).setOnPreferenceClickListener(preference -> { + ChooseDataFolderDialog.showDialog(getContext(), path -> { + UserPreferences.setDataFolder(path); + setDataFolderText(); + }); + return true; + }); + findPreference(PREF_AUTO_DELETE_LOCAL).setOnPreferenceChangeListener((preference, newValue) -> { + if (blockAutoDeleteLocal && newValue == Boolean.TRUE) { + showAutoDeleteEnableDialog(); + return false; + } else { + return true; + } + }); + } + + private void setDataFolderText() { + File f = UserPreferences.getDataFolder(null); + if (f != null) { + findPreference(PREF_CHOOSE_DATA_DIR).setSummary(f.getAbsolutePath()); + } + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + if (UserPreferences.PREF_UPDATE_INTERVAL.equals(key)) { + FeedUpdateManager.getInstance().restartUpdateAlarm(getContext(), true); + } + } + + private void showAutoDeleteEnableDialog() { + new MaterialAlertDialogBuilder(requireContext()) + .setMessage(R.string.pref_auto_local_delete_dialog_body) + .setPositiveButton(R.string.yes, (dialog, which) -> { + blockAutoDeleteLocal = false; + ((TwoStatePreference) findPreference(PREF_AUTO_DELETE_LOCAL)).setChecked(true); + blockAutoDeleteLocal = true; + }) + .setNegativeButton(R.string.cancel_label, null) + .show(); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/screen/preferences/ImportExportPreferencesFragment.java b/app/src/main/java/de/danoeh/antennapod/ui/screen/preferences/ImportExportPreferencesFragment.java new file mode 100644 index 000000000..5ffa7e57a --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/screen/preferences/ImportExportPreferencesFragment.java @@ -0,0 +1,412 @@ +package de.danoeh.antennapod.ui.screen.preferences; + +import android.app.Activity; +import android.app.ProgressDialog; +import android.content.ActivityNotFoundException; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.os.Bundle; +import android.util.Log; + +import androidx.activity.result.ActivityResult; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; +import androidx.activity.result.contract.ActivityResultContracts.GetContent; +import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; +import androidx.documentfile.provider.DocumentFile; +import androidx.preference.SwitchPreferenceCompat; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import androidx.core.app.ShareCompat; +import androidx.core.content.FileProvider; +import androidx.preference.PreferenceFragmentCompat; +import com.google.android.material.snackbar.Snackbar; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.OpmlImportActivity; +import de.danoeh.antennapod.storage.database.DBReader; +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedItemFilter; +import de.danoeh.antennapod.model.feed.SortOrder; +import de.danoeh.antennapod.storage.importexport.AutomaticDatabaseExportWorker; +import de.danoeh.antennapod.storage.importexport.DatabaseExporter; +import de.danoeh.antennapod.storage.importexport.FavoritesWriter; +import de.danoeh.antennapod.storage.importexport.HtmlWriter; +import de.danoeh.antennapod.storage.importexport.OpmlWriter; +import de.danoeh.antennapod.storage.preferences.UserPreferences; +import io.reactivex.Completable; +import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; +import io.reactivex.schedulers.Schedulers; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.nio.charset.Charset; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.List; +import java.util.Locale; + +public class ImportExportPreferencesFragment extends PreferenceFragmentCompat { + private static final String TAG = "ImportExPrefFragment"; + private static final String PREF_OPML_EXPORT = "prefOpmlExport"; + private static final String PREF_OPML_IMPORT = "prefOpmlImport"; + private static final String PREF_HTML_EXPORT = "prefHtmlExport"; + private static final String PREF_DATABASE_IMPORT = "prefDatabaseImport"; + private static final String PREF_DATABASE_EXPORT = "prefDatabaseExport"; + private static final String PREF_AUTOMATIC_DATABASE_EXPORT = "prefAutomaticDatabaseExport"; + private static final String PREF_FAVORITE_EXPORT = "prefFavoritesExport"; + private static final String DEFAULT_OPML_OUTPUT_NAME = "antennapod-feeds-%s.opml"; + private static final String CONTENT_TYPE_OPML = "text/x-opml"; + private static final String DEFAULT_HTML_OUTPUT_NAME = "antennapod-feeds-%s.html"; + private static final String CONTENT_TYPE_HTML = "text/html"; + private static final String DEFAULT_FAVORITES_OUTPUT_NAME = "antennapod-favorites-%s.html"; + private static final String DATABASE_EXPORT_FILENAME = "AntennaPodBackup-%s.db"; + + private final ActivityResultLauncher chooseOpmlExportPathLauncher = + registerForActivityResult(new StartActivityForResult(), + result -> exportToDocument(result, Export.OPML)); + private final ActivityResultLauncher chooseHtmlExportPathLauncher = + registerForActivityResult(new StartActivityForResult(), + result -> exportToDocument(result, Export.HTML)); + private final ActivityResultLauncher chooseFavoritesExportPathLauncher = + registerForActivityResult(new StartActivityForResult(), + result -> exportToDocument(result, Export.FAVORITES)); + private final ActivityResultLauncher restoreDatabaseLauncher = + registerForActivityResult(new StartActivityForResult(), this::restoreDatabaseResult); + private final ActivityResultLauncher backupDatabaseLauncher = + registerForActivityResult(new BackupDatabase(), this::backupDatabaseResult); + private final ActivityResultLauncher chooseOpmlImportPathLauncher = + registerForActivityResult(new GetContent(), uri -> { + if (uri != null) { + final Intent intent = new Intent(getContext(), OpmlImportActivity.class); + intent.setData(uri); + startActivity(intent); + } + }); + private final ActivityResultLauncher automaticBackupLauncher = + registerForActivityResult(new PickWritableFolder(), this::setupAutomaticBackup); + + private Disposable disposable; + private ProgressDialog progressDialog; + + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + addPreferencesFromResource(R.xml.preferences_import_export); + setupStorageScreen(); + progressDialog = new ProgressDialog(getContext()); + progressDialog.setIndeterminate(true); + progressDialog.setMessage(getContext().getString(R.string.please_wait)); + } + + @Override + public void onStart() { + super.onStart(); + ((PreferenceActivity) getActivity()).getSupportActionBar().setTitle(R.string.import_export_pref); + } + + @Override + public void onStop() { + super.onStop(); + if (disposable != null) { + disposable.dispose(); + } + } + + private void setupStorageScreen() { + findPreference(PREF_OPML_EXPORT).setOnPreferenceClickListener( + preference -> { + openExportPathPicker(Export.OPML, chooseOpmlExportPathLauncher); + return true; + } + ); + findPreference(PREF_HTML_EXPORT).setOnPreferenceClickListener( + preference -> { + openExportPathPicker(Export.HTML, chooseHtmlExportPathLauncher); + return true; + }); + findPreference(PREF_OPML_IMPORT).setOnPreferenceClickListener( + preference -> { + try { + chooseOpmlImportPathLauncher.launch("*/*"); + } catch (ActivityNotFoundException e) { + Log.e(TAG, "No activity found. Should never happen..."); + } + return true; + }); + findPreference(PREF_DATABASE_IMPORT).setOnPreferenceClickListener( + preference -> { + importDatabase(); + return true; + }); + findPreference(PREF_DATABASE_EXPORT).setOnPreferenceClickListener( + preference -> { + backupDatabaseLauncher.launch(dateStampFilename(DATABASE_EXPORT_FILENAME)); + return true; + }); + ((SwitchPreferenceCompat) findPreference(PREF_AUTOMATIC_DATABASE_EXPORT)) + .setChecked(UserPreferences.getAutomaticExportFolder() != null); + findPreference(PREF_AUTOMATIC_DATABASE_EXPORT).setOnPreferenceChangeListener( + (preference, newValue) -> { + if (Boolean.TRUE.equals(newValue)) { + try { + automaticBackupLauncher.launch(null); + } catch (ActivityNotFoundException e) { + e.printStackTrace(); + Snackbar.make(getView(), R.string.unable_to_start_system_file_manager, Snackbar.LENGTH_LONG) + .show(); + } + return false; + } else { + UserPreferences.setAutomaticExportFolder(null); + AutomaticDatabaseExportWorker.enqueueIfNeeded(getContext(), false); + } + return true; + }); + findPreference(PREF_FAVORITE_EXPORT).setOnPreferenceClickListener( + preference -> { + openExportPathPicker(Export.FAVORITES, chooseFavoritesExportPathLauncher); + return true; + }); + } + + private String dateStampFilename(String fname) { + return String.format(fname, new SimpleDateFormat("yyyy-MM-dd", Locale.US).format(new Date())); + } + + private void importDatabase() { + // setup the alert builder + MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(getActivity()); + builder.setTitle(R.string.database_import_label); + builder.setMessage(R.string.database_import_warning); + + // add a button + builder.setNegativeButton(R.string.no, null); + builder.setPositiveButton(R.string.confirm_label, (dialog, which) -> { + Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); + intent.setType("*/*"); + restoreDatabaseLauncher.launch(intent); + }); + + // create and show the alert dialog + builder.show(); + } + + private void showDatabaseImportSuccessDialog() { + MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(getContext()); + builder.setTitle(R.string.successful_import_label); + builder.setMessage(R.string.import_ok); + builder.setCancelable(false); + builder.setPositiveButton(android.R.string.ok, (dialogInterface, i) -> forceRestart()); + builder.show(); + } + + void showExportSuccessSnackbar(Uri uri, String mimeType) { + Snackbar.make(getView(), R.string.export_success_title, Snackbar.LENGTH_LONG) + .setAction(R.string.share_label, v -> + new ShareCompat.IntentBuilder(getContext()) + .setType(mimeType) + .addStream(uri) + .setChooserTitle(R.string.share_label) + .startChooser()) + .show(); + } + + private void showExportErrorDialog(final Throwable error) { + progressDialog.dismiss(); + final MaterialAlertDialogBuilder alert = new MaterialAlertDialogBuilder(getContext()); + alert.setPositiveButton(android.R.string.ok, (dialog, which) -> dialog.dismiss()); + alert.setTitle(R.string.export_error_label); + alert.setMessage(error.getMessage()); + alert.show(); + } + + private void restoreDatabaseResult(final ActivityResult result) { + if (result.getResultCode() != Activity.RESULT_OK || result.getData() == null) { + return; + } + final Uri uri = result.getData().getData(); + progressDialog.show(); + disposable = Completable.fromAction(() -> DatabaseExporter.importBackup(uri, getContext())) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(() -> { + showDatabaseImportSuccessDialog(); + progressDialog.dismiss(); + }, this::showExportErrorDialog); + } + + private void backupDatabaseResult(final Uri uri) { + if (uri == null) { + return; + } + progressDialog.show(); + disposable = Completable.fromAction(() -> DatabaseExporter.exportToDocument(uri, getContext())) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(() -> { + showExportSuccessSnackbar(uri, "application/x-sqlite3"); + progressDialog.dismiss(); + }, this::showExportErrorDialog); + } + + private void openExportPathPicker(Export exportType, ActivityResultLauncher result) { + String title = dateStampFilename(exportType.outputNameTemplate); + + Intent intentPickAction = new Intent(Intent.ACTION_CREATE_DOCUMENT) + .addCategory(Intent.CATEGORY_OPENABLE) + .setType(exportType.contentType) + .putExtra(Intent.EXTRA_TITLE, title); + + // Creates an implicit intent to launch a file manager which lets + // the user choose a specific directory to export to. + try { + result.launch(intentPickAction); + return; + } catch (ActivityNotFoundException e) { + Log.e(TAG, "No activity found. Should never happen..."); + } + + // If we are using a SDK lower than API 21 or the implicit intent failed + // fallback to the legacy export process + File output = new File(UserPreferences.getDataFolder("export/"), title); + exportToFile(exportType, output); + } + + private void exportToFile(Export exportType, File output) { + progressDialog.show(); + disposable = Observable.create( + subscriber -> { + if (output.exists()) { + boolean success = output.delete(); + Log.w(TAG, "Overwriting previously exported file: " + success); + } + try (FileOutputStream fileOutputStream = new FileOutputStream(output)) { + writeToStream(fileOutputStream, exportType); + subscriber.onNext(output); + } catch (IOException e) { + subscriber.onError(e); + } + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(outputFile -> { + progressDialog.dismiss(); + Uri fileUri = FileProvider.getUriForFile(getActivity().getApplicationContext(), + getString(R.string.provider_authority), output); + showExportSuccessSnackbar(fileUri, exportType.contentType); + }, this::showExportErrorDialog, progressDialog::dismiss); + } + + private void exportToDocument(final ActivityResult result, Export exportType) { + if (result.getResultCode() != Activity.RESULT_OK || result.getData() == null) { + return; + } + progressDialog.show(); + DocumentFile output = DocumentFile.fromSingleUri(getContext(), result.getData().getData()); + disposable = Observable.create( + subscriber -> { + try (OutputStream outputStream = getContext().getContentResolver() + .openOutputStream(output.getUri(), "wt")) { + writeToStream(outputStream, exportType); + subscriber.onNext(output); + } catch (IOException e) { + subscriber.onError(e); + } + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(ignore -> { + progressDialog.dismiss(); + showExportSuccessSnackbar(output.getUri(), exportType.contentType); + }, this::showExportErrorDialog, progressDialog::dismiss); + } + + private void writeToStream(OutputStream outputStream, Export type) throws IOException { + try (OutputStreamWriter writer = new OutputStreamWriter(outputStream, Charset.forName("UTF-8"))) { + switch (type) { + case HTML: + HtmlWriter.writeDocument(DBReader.getFeedList(), writer, getContext()); + break; + case OPML: + OpmlWriter.writeDocument(DBReader.getFeedList(), writer); + break; + case FAVORITES: + List allFavorites = DBReader.getEpisodes(0, Integer.MAX_VALUE, + new FeedItemFilter(FeedItemFilter.IS_FAVORITE), SortOrder.DATE_NEW_OLD); + FavoritesWriter.writeDocument(allFavorites, writer, getContext()); + break; + default: + showExportErrorDialog(new Exception("Invalid export type")); + break; + } + } + } + + private void setupAutomaticBackup(Uri uri) { + if (uri == null) { + return; + } + getActivity().getContentResolver().takePersistableUriPermission(uri, + Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + UserPreferences.setAutomaticExportFolder(uri.toString()); + AutomaticDatabaseExportWorker.enqueueIfNeeded(getContext(), true); + ((SwitchPreferenceCompat) findPreference(PREF_AUTOMATIC_DATABASE_EXPORT)).setChecked(true); + } + + private void forceRestart() { + PackageManager pm = getContext().getPackageManager(); + Intent intent = pm.getLaunchIntentForPackage(getContext().getPackageName()); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + getContext().getApplicationContext().startActivity(intent); + Runtime.getRuntime().exit(0); + } + + private static class BackupDatabase extends ActivityResultContracts.CreateDocument { + + BackupDatabase() { + super("application/x-sqlite3"); + } + + @NonNull + @Override + public Intent createIntent(@NonNull final Context context, @NonNull final String input) { + return super.createIntent(context, input) + .addCategory(Intent.CATEGORY_OPENABLE) + .setType("application/x-sqlite3"); + } + } + + private static class PickWritableFolder extends ActivityResultContracts.OpenDocumentTree { + @NonNull + @Override + public Intent createIntent(@NonNull final Context context, @Nullable final Uri input) { + return super.createIntent(context, input) + .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + } + } + + private enum Export { + OPML(CONTENT_TYPE_OPML, DEFAULT_OPML_OUTPUT_NAME, R.string.opml_export_label), + HTML(CONTENT_TYPE_HTML, DEFAULT_HTML_OUTPUT_NAME, R.string.html_export_label), + FAVORITES(CONTENT_TYPE_HTML, DEFAULT_FAVORITES_OUTPUT_NAME, R.string.favorites_export_label); + + final String contentType; + final String outputNameTemplate; + @StringRes + final int labelResId; + + Export(String contentType, String outputNameTemplate, int labelResId) { + this.contentType = contentType; + this.outputNameTemplate = outputNameTemplate; + this.labelResId = labelResId; + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/screen/preferences/MainPreferencesFragment.java b/app/src/main/java/de/danoeh/antennapod/ui/screen/preferences/MainPreferencesFragment.java new file mode 100644 index 000000000..40a9dd605 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/screen/preferences/MainPreferencesFragment.java @@ -0,0 +1,155 @@ +package de.danoeh.antennapod.ui.screen.preferences; + +import android.content.Intent; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffColorFilter; +import android.os.Bundle; + +import androidx.appcompat.app.AppCompatActivity; +import androidx.preference.Preference; +import androidx.preference.PreferenceFragmentCompat; + +import com.bytehamster.lib.preferencesearch.SearchConfiguration; +import com.bytehamster.lib.preferencesearch.SearchPreference; + +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.util.IntentUtils; +import de.danoeh.antennapod.ui.preferences.screen.about.AboutFragment; + +public class MainPreferencesFragment extends PreferenceFragmentCompat { + + private static final String PREF_SCREEN_USER_INTERFACE = "prefScreenInterface"; + private static final String PREF_SCREEN_PLAYBACK = "prefScreenPlayback"; + private static final String PREF_SCREEN_DOWNLOADS = "prefScreenDownloads"; + private static final String PREF_SCREEN_IMPORT_EXPORT = "prefScreenImportExport"; + private static final String PREF_SCREEN_SYNCHRONIZATION = "prefScreenSynchronization"; + private static final String PREF_DOCUMENTATION = "prefDocumentation"; + private static final String PREF_VIEW_FORUM = "prefViewForum"; + private static final String PREF_SEND_BUG_REPORT = "prefSendBugReport"; + private static final String PREF_CATEGORY_PROJECT = "project"; + private static final String PREF_ABOUT = "prefAbout"; + private static final String PREF_NOTIFICATION = "notifications"; + private static final String PREF_CONTRIBUTE = "prefContribute"; + + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + addPreferencesFromResource(R.xml.preferences); + setupMainScreen(); + setupSearch(); + + // If you are writing a spin-off, please update the details on screens like "About" and "Report bug" + // and afterwards remove the following lines. Please keep in mind that AntennaPod is licensed under the GPL. + // This means that your application needs to be open-source under the GPL, too. + // It must also include a prominent copyright notice. + int packageHash = getContext().getPackageName().hashCode(); + if (packageHash != 1790437538 && packageHash != -1190467065) { + findPreference(PREF_CATEGORY_PROJECT).setVisible(false); + Preference copyrightNotice = new Preference(getContext()); + copyrightNotice.setIcon(R.drawable.ic_info_white); + copyrightNotice.getIcon().mutate() + .setColorFilter(new PorterDuffColorFilter(0xffcc0000, PorterDuff.Mode.MULTIPLY)); + copyrightNotice.setSummary("This application is based on AntennaPod." + + " The AntennaPod team does NOT provide support for this unofficial version." + + " If you can read this message, the developers of this modification" + + " violate the GNU General Public License (GPL)."); + findPreference(PREF_CATEGORY_PROJECT).getParent().addPreference(copyrightNotice); + } else if (packageHash == -1190467065) { + Preference debugNotice = new Preference(getContext()); + debugNotice.setIcon(R.drawable.ic_info_white); + debugNotice.getIcon().mutate() + .setColorFilter(new PorterDuffColorFilter(0xffcc0000, PorterDuff.Mode.MULTIPLY)); + debugNotice.setOrder(-1); + debugNotice.setSummary("This is a development version of AntennaPod and not meant for daily use"); + findPreference(PREF_CATEGORY_PROJECT).getParent().addPreference(debugNotice); + } + } + + @Override + public void onStart() { + super.onStart(); + ((PreferenceActivity) getActivity()).getSupportActionBar().setTitle(R.string.settings_label); + } + + private void setupMainScreen() { + findPreference(PREF_SCREEN_USER_INTERFACE).setOnPreferenceClickListener(preference -> { + ((PreferenceActivity) getActivity()).openScreen(R.xml.preferences_user_interface); + return true; + }); + findPreference(PREF_SCREEN_PLAYBACK).setOnPreferenceClickListener(preference -> { + ((PreferenceActivity) getActivity()).openScreen(R.xml.preferences_playback); + return true; + }); + findPreference(PREF_SCREEN_DOWNLOADS).setOnPreferenceClickListener(preference -> { + ((PreferenceActivity) getActivity()).openScreen(R.xml.preferences_downloads); + return true; + }); + findPreference(PREF_SCREEN_SYNCHRONIZATION).setOnPreferenceClickListener(preference -> { + ((PreferenceActivity) getActivity()).openScreen(R.xml.preferences_synchronization); + return true; + }); + findPreference(PREF_SCREEN_IMPORT_EXPORT).setOnPreferenceClickListener(preference -> { + ((PreferenceActivity) getActivity()).openScreen(R.xml.preferences_import_export); + return true; + }); + findPreference(PREF_NOTIFICATION).setOnPreferenceClickListener(preference -> { + ((PreferenceActivity) getActivity()).openScreen(R.xml.preferences_notifications); + return true; + }); + findPreference(PREF_ABOUT).setOnPreferenceClickListener( + preference -> { + getParentFragmentManager().beginTransaction() + .replace(R.id.settingsContainer, new AboutFragment()) + .addToBackStack(getString(R.string.about_pref)).commit(); + return true; + } + ); + findPreference(PREF_DOCUMENTATION).setOnPreferenceClickListener(preference -> { + IntentUtils.openInBrowser(getContext(), + IntentUtils.getLocalizedWebsiteLink(getContext()) + "/documentation/"); + return true; + }); + findPreference(PREF_VIEW_FORUM).setOnPreferenceClickListener(preference -> { + IntentUtils.openInBrowser(getContext(), "https://forum.antennapod.org/"); + return true; + }); + findPreference(PREF_CONTRIBUTE).setOnPreferenceClickListener(preference -> { + IntentUtils.openInBrowser(getContext(), + IntentUtils.getLocalizedWebsiteLink(getContext()) + "/contribute/"); + return true; + }); + findPreference(PREF_SEND_BUG_REPORT).setOnPreferenceClickListener(preference -> { + startActivity(new Intent(getActivity(), BugReportActivity.class)); + return true; + }); + } + + private void setupSearch() { + SearchPreference searchPreference = findPreference("searchPreference"); + SearchConfiguration config = searchPreference.getSearchConfiguration(); + config.setActivity((AppCompatActivity) getActivity()); + config.setFragmentContainerViewId(R.id.settingsContainer); + config.setBreadcrumbsEnabled(true); + + config.index(R.xml.preferences_user_interface) + .addBreadcrumb(PreferenceActivity.getTitleOfPage(R.xml.preferences_user_interface)); + config.index(R.xml.preferences_playback) + .addBreadcrumb(PreferenceActivity.getTitleOfPage(R.xml.preferences_playback)); + config.index(R.xml.preferences_downloads) + .addBreadcrumb(PreferenceActivity.getTitleOfPage(R.xml.preferences_downloads)); + config.index(R.xml.preferences_import_export) + .addBreadcrumb(PreferenceActivity.getTitleOfPage(R.xml.preferences_import_export)); + config.index(R.xml.preferences_autodownload) + .addBreadcrumb(PreferenceActivity.getTitleOfPage(R.xml.preferences_downloads)) + .addBreadcrumb(R.string.automation) + .addBreadcrumb(PreferenceActivity.getTitleOfPage(R.xml.preferences_autodownload)); + config.index(R.xml.preferences_synchronization) + .addBreadcrumb(PreferenceActivity.getTitleOfPage(R.xml.preferences_synchronization)); + config.index(R.xml.preferences_notifications) + .addBreadcrumb(PreferenceActivity.getTitleOfPage(R.xml.preferences_notifications)); + config.index(R.xml.feed_settings) + .addBreadcrumb(PreferenceActivity.getTitleOfPage(R.xml.feed_settings)); + config.index(R.xml.preferences_swipe) + .addBreadcrumb(PreferenceActivity.getTitleOfPage(R.xml.preferences_user_interface)) + .addBreadcrumb(PreferenceActivity.getTitleOfPage(R.xml.preferences_swipe)); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/screen/preferences/PlaybackPreferencesFragment.java b/app/src/main/java/de/danoeh/antennapod/ui/screen/preferences/PlaybackPreferencesFragment.java new file mode 100644 index 000000000..70d2828bb --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/screen/preferences/PlaybackPreferencesFragment.java @@ -0,0 +1,127 @@ +package de.danoeh.antennapod.ui.screen.preferences; + +import android.app.Activity; +import android.content.res.Resources; +import android.os.Build; +import android.os.Bundle; +import androidx.annotation.NonNull; +import androidx.collection.ArrayMap; +import androidx.preference.ListPreference; +import androidx.preference.Preference; +import androidx.preference.PreferenceFragmentCompat; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.event.UnreadItemsUpdateEvent; +import de.danoeh.antennapod.storage.preferences.UsageStatistics; +import de.danoeh.antennapod.storage.preferences.UserPreferences; +import de.danoeh.antennapod.ui.screen.feed.preferences.SkipPreferenceDialog; +import de.danoeh.antennapod.ui.screen.playback.VariableSpeedDialog; +import java.util.Map; +import org.greenrobot.eventbus.EventBus; + +public class PlaybackPreferencesFragment extends PreferenceFragmentCompat { + private static final String PREF_PLAYBACK_SPEED_LAUNCHER = "prefPlaybackSpeedLauncher"; + private static final String PREF_PLAYBACK_REWIND_DELTA_LAUNCHER = "prefPlaybackRewindDeltaLauncher"; + private static final String PREF_PLAYBACK_FAST_FORWARD_DELTA_LAUNCHER = "prefPlaybackFastForwardDeltaLauncher"; + private static final String PREF_PLAYBACK_PREFER_STREAMING = "prefStreamOverDownload"; + + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + addPreferencesFromResource(R.xml.preferences_playback); + + setupPlaybackScreen(); + buildSmartMarkAsPlayedPreference(); + } + + @Override + public void onStart() { + super.onStart(); + ((PreferenceActivity) getActivity()).getSupportActionBar().setTitle(R.string.playback_pref); + } + + private void setupPlaybackScreen() { + final Activity activity = getActivity(); + + findPreference(PREF_PLAYBACK_SPEED_LAUNCHER).setOnPreferenceClickListener(preference -> { + new VariableSpeedDialog().show(getChildFragmentManager(), null); + return true; + }); + findPreference(PREF_PLAYBACK_REWIND_DELTA_LAUNCHER).setOnPreferenceClickListener(preference -> { + SkipPreferenceDialog.showSkipPreference(activity, SkipPreferenceDialog.SkipDirection.SKIP_REWIND, null); + return true; + }); + findPreference(PREF_PLAYBACK_FAST_FORWARD_DELTA_LAUNCHER).setOnPreferenceClickListener(preference -> { + SkipPreferenceDialog.showSkipPreference(activity, SkipPreferenceDialog.SkipDirection.SKIP_FORWARD, null); + return true; + }); + findPreference(PREF_PLAYBACK_PREFER_STREAMING).setOnPreferenceChangeListener((preference, newValue) -> { + // Update all visible lists to reflect new streaming action button + EventBus.getDefault().post(new UnreadItemsUpdateEvent()); + // User consciously decided whether to prefer the streaming button, disable suggestion to change that + UsageStatistics.doNotAskAgain(UsageStatistics.ACTION_STREAM); + return true; + }); + if (Build.VERSION.SDK_INT >= 31) { + findPreference(UserPreferences.PREF_UNPAUSE_ON_HEADSET_RECONNECT).setVisible(false); + findPreference(UserPreferences.PREF_UNPAUSE_ON_BLUETOOTH_RECONNECT).setVisible(false); + } + + buildEnqueueLocationPreference(); + } + + private void buildEnqueueLocationPreference() { + final Resources res = requireActivity().getResources(); + final Map options = new ArrayMap<>(); + { + String[] keys = res.getStringArray(R.array.enqueue_location_values); + String[] values = res.getStringArray(R.array.enqueue_location_options); + for (int i = 0; i < keys.length; i++) { + options.put(keys[i], values[i]); + } + } + + ListPreference pref = requirePreference(UserPreferences.PREF_ENQUEUE_LOCATION); + pref.setSummary(res.getString(R.string.pref_enqueue_location_sum, options.get(pref.getValue()))); + + pref.setOnPreferenceChangeListener((preference, newValue) -> { + if (!(newValue instanceof String)) { + return false; + } + String newValStr = (String)newValue; + pref.setSummary(res.getString(R.string.pref_enqueue_location_sum, options.get(newValStr))); + return true; + }); + } + + @NonNull + private T requirePreference(@NonNull CharSequence key) { + // Possibly put it to a common method in abstract base class + T result = findPreference(key); + if (result == null) { + throw new IllegalArgumentException("Preference with key '" + key + "' is not found"); + + } + return result; + } + + private void buildSmartMarkAsPlayedPreference() { + final Resources res = getActivity().getResources(); + + ListPreference pref = findPreference(UserPreferences.PREF_SMART_MARK_AS_PLAYED_SECS); + String[] values = res.getStringArray(R.array.smart_mark_as_played_values); + String[] entries = new String[values.length]; + for (int x = 0; x < values.length; x++) { + if(x == 0) { + entries[x] = res.getString(R.string.pref_smart_mark_as_played_disabled); + } else { + int v = Integer.parseInt(values[x]); + if(v < 60) { + entries[x] = res.getQuantityString(R.plurals.time_seconds_quantified, v, v); + } else { + v /= 60; + entries[x] = res.getQuantityString(R.plurals.time_minutes_quantified, v, v); + } + } + } + pref.setEntries(entries); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/screen/preferences/PreferenceActivity.java b/app/src/main/java/de/danoeh/antennapod/ui/screen/preferences/PreferenceActivity.java new file mode 100644 index 000000000..e82c2c084 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/screen/preferences/PreferenceActivity.java @@ -0,0 +1,188 @@ +package de.danoeh.antennapod.ui.screen.preferences; + +import android.content.Intent; +import android.os.Build; +import android.os.Bundle; +import android.provider.Settings; +import android.util.Log; +import android.view.MenuItem; + +import android.view.View; +import android.view.inputmethod.InputMethodManager; + +import androidx.appcompat.app.ActionBar; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import androidx.appcompat.app.AppCompatActivity; +import androidx.preference.PreferenceFragmentCompat; + +import com.bytehamster.lib.preferencesearch.SearchPreferenceResult; +import com.bytehamster.lib.preferencesearch.SearchPreferenceResultListener; + +import com.google.android.material.snackbar.Snackbar; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.ui.common.ThemeSwitcher; + +import de.danoeh.antennapod.event.MessageEvent; +import de.danoeh.antennapod.ui.preferences.screen.AutoDownloadPreferencesFragment; +import de.danoeh.antennapod.ui.preferences.screen.NotificationPreferencesFragment; +import de.danoeh.antennapod.ui.preferences.screen.synchronization.SynchronizationPreferencesFragment; +import de.danoeh.antennapod.ui.preferences.databinding.SettingsActivityBinding; +import org.greenrobot.eventbus.EventBus; +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; + +/** + * PreferenceActivity for API 11+. In order to change the behavior of the preference UI, see + * PreferenceController. + */ +public class PreferenceActivity extends AppCompatActivity implements SearchPreferenceResultListener { + private static final String FRAGMENT_TAG = "tag_preferences"; + public static final String OPEN_AUTO_DOWNLOAD_SETTINGS = "OpenAutoDownloadSettings"; + private SettingsActivityBinding binding; + + @Override + protected void onCreate(Bundle savedInstanceState) { + setTheme(ThemeSwitcher.getTheme(this)); + super.onCreate(savedInstanceState); + + ActionBar ab = getSupportActionBar(); + if (ab != null) { + ab.setDisplayHomeAsUpEnabled(true); + } + + binding = SettingsActivityBinding.inflate(getLayoutInflater()); + setContentView(binding.getRoot()); + + if (getSupportFragmentManager().findFragmentByTag(FRAGMENT_TAG) == null) { + getSupportFragmentManager().beginTransaction() + .replace(binding.settingsContainer.getId(), new MainPreferencesFragment(), FRAGMENT_TAG) + .commit(); + } + Intent intent = getIntent(); + if (intent.getBooleanExtra(OPEN_AUTO_DOWNLOAD_SETTINGS, false)) { + openScreen(R.xml.preferences_autodownload); + } + } + + private PreferenceFragmentCompat getPreferenceScreen(int screen) { + PreferenceFragmentCompat prefFragment = null; + + if (screen == R.xml.preferences_user_interface) { + prefFragment = new UserInterfacePreferencesFragment(); + } else if (screen == R.xml.preferences_downloads) { + prefFragment = new DownloadsPreferencesFragment(); + } else if (screen == R.xml.preferences_import_export) { + prefFragment = new ImportExportPreferencesFragment(); + } else if (screen == R.xml.preferences_autodownload) { + prefFragment = new AutoDownloadPreferencesFragment(); + } else if (screen == R.xml.preferences_synchronization) { + prefFragment = new SynchronizationPreferencesFragment(); + } else if (screen == R.xml.preferences_playback) { + prefFragment = new PlaybackPreferencesFragment(); + } else if (screen == R.xml.preferences_notifications) { + prefFragment = new NotificationPreferencesFragment(); + } else if (screen == R.xml.preferences_swipe) { + prefFragment = new SwipePreferencesFragment(); + } + return prefFragment; + } + + public static int getTitleOfPage(int preferences) { + if (preferences == R.xml.preferences_downloads) { + return R.string.downloads_pref; + } else if (preferences == R.xml.preferences_autodownload) { + return R.string.pref_automatic_download_title; + } else if (preferences == R.xml.preferences_playback) { + return R.string.playback_pref; + } else if (preferences == R.xml.preferences_import_export) { + return R.string.import_export_pref; + } else if (preferences == R.xml.preferences_user_interface) { + return R.string.user_interface_label; + } else if (preferences == R.xml.preferences_synchronization) { + return R.string.synchronization_pref; + } else if (preferences == R.xml.preferences_notifications) { + return R.string.notification_pref_fragment; + } else if (preferences == R.xml.feed_settings) { + return R.string.feed_settings_label; + } else if (preferences == R.xml.preferences_swipe) { + return R.string.swipeactions_label; + } + return R.string.settings_label; + } + + public PreferenceFragmentCompat openScreen(int screen) { + PreferenceFragmentCompat fragment = getPreferenceScreen(screen); + if (screen == R.xml.preferences_notifications && Build.VERSION.SDK_INT >= 26) { + Intent intent = new Intent(); + intent.setAction(Settings.ACTION_APP_NOTIFICATION_SETTINGS); + intent.putExtra(Settings.EXTRA_APP_PACKAGE, getPackageName()); + startActivity(intent); + } else { + getSupportFragmentManager().beginTransaction() + .replace(binding.settingsContainer.getId(), fragment) + .addToBackStack(getString(getTitleOfPage(screen))).commit(); + } + + + return fragment; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == android.R.id.home) { + if (getSupportFragmentManager().getBackStackEntryCount() == 0) { + finish(); + } else { + InputMethodManager imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE); + View view = getCurrentFocus(); + //If no view currently has focus, create a new one, just so we can grab a window token from it + if (view == null) { + view = new View(this); + } + imm.hideSoftInputFromWindow(view.getWindowToken(), 0); + getSupportFragmentManager().popBackStack(); + } + return true; + } + return false; + } + + @Override + public void onSearchResultClicked(SearchPreferenceResult result) { + int screen = result.getResourceFile(); + if (screen == R.xml.feed_settings) { + MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this); + builder.setTitle(R.string.feed_settings_label); + builder.setMessage(R.string.pref_feed_settings_dialog_msg); + builder.setPositiveButton(android.R.string.ok, null); + builder.show(); + } else if (screen == R.xml.preferences_notifications) { + openScreen(screen); + } else { + PreferenceFragmentCompat fragment = openScreen(result.getResourceFile()); + result.highlight(fragment); + } + } + + @Override + protected void onStart() { + super.onStart(); + EventBus.getDefault().register(this); + } + + @Override + protected void onStop() { + super.onStop(); + EventBus.getDefault().unregister(this); + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onEventMainThread(MessageEvent event) { + Log.d(FRAGMENT_TAG, "onEvent(" + event + ")"); + Snackbar s = Snackbar.make(binding.getRoot(), event.message, Snackbar.LENGTH_LONG); + if (event.action != null) { + s.setAction(event.actionText, v -> event.action.accept(this)); + } + s.show(); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/screen/preferences/PreferenceListDialog.java b/app/src/main/java/de/danoeh/antennapod/ui/screen/preferences/PreferenceListDialog.java new file mode 100644 index 000000000..26ed7eada --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/screen/preferences/PreferenceListDialog.java @@ -0,0 +1,49 @@ +package de.danoeh.antennapod.ui.screen.preferences; + +import android.content.Context; + +import com.google.android.material.dialog.MaterialAlertDialogBuilder; + +import de.danoeh.antennapod.R; + +public class PreferenceListDialog { + protected Context context; + private String title; + private OnPreferenceChangedListener onPreferenceChangedListener; + private int selectedPos = 0; + + public PreferenceListDialog(Context context, String title) { + this.context = context; + this.title = title; + } + + public interface OnPreferenceChangedListener { + /** + * Notified when user confirms preference + * + * @param pos The index of the item that was selected + */ + + void preferenceChanged(int pos); + } + + public void openDialog(String[] items) { + + MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(context); + builder.setTitle(title); + builder.setSingleChoiceItems(items, selectedPos, (dialog, which) -> { + selectedPos = which; + }); + builder.setPositiveButton(R.string.confirm_label, (dialog, which) -> { + if (onPreferenceChangedListener != null && selectedPos >= 0) { + onPreferenceChangedListener.preferenceChanged(selectedPos); + } + }); + builder.setNegativeButton(R.string.cancel_label, null); + builder.create().show(); + } + + public void setOnPreferenceChangedListener(OnPreferenceChangedListener onPreferenceChangedListener) { + this.onPreferenceChangedListener = onPreferenceChangedListener; + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/screen/preferences/PreferenceSwitchDialog.java b/app/src/main/java/de/danoeh/antennapod/ui/screen/preferences/PreferenceSwitchDialog.java new file mode 100644 index 000000000..217785f90 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/screen/preferences/PreferenceSwitchDialog.java @@ -0,0 +1,57 @@ +package de.danoeh.antennapod.ui.screen.preferences; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; + +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import com.google.android.material.materialswitch.MaterialSwitch; + +import de.danoeh.antennapod.R; + +public class PreferenceSwitchDialog { + protected Context context; + private String title; + private String text; + private OnPreferenceChangedListener onPreferenceChangedListener; + + public PreferenceSwitchDialog(Context context, String title, String text) { + this.context = context; + this.title = title; + this.text = text; + } + + public interface OnPreferenceChangedListener { + /** + * Notified when user confirms preference + * + * @param enabled The preference + */ + + void preferenceChanged(boolean enabled); + } + + public void openDialog() { + + MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(context); + builder.setTitle(title); + + LayoutInflater inflater = LayoutInflater.from(this.context); + View layout = inflater.inflate(R.layout.dialog_switch_preference, null, false); + MaterialSwitch switchButton = layout.findViewById(R.id.dialogSwitch); + switchButton.setText(text); + builder.setView(layout); + + builder.setPositiveButton(R.string.confirm_label, (dialog, which) -> { + if (onPreferenceChangedListener != null) { + onPreferenceChangedListener.preferenceChanged(switchButton.isChecked()); + } + }); + builder.setNegativeButton(R.string.cancel_label, null); + builder.create().show(); + } + + public void setOnPreferenceChangedListener(OnPreferenceChangedListener onPreferenceChangedListener) { + this.onPreferenceChangedListener = onPreferenceChangedListener; + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/screen/preferences/ProxyDialog.java b/app/src/main/java/de/danoeh/antennapod/ui/screen/preferences/ProxyDialog.java new file mode 100644 index 000000000..ca4b4b5e5 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/screen/preferences/ProxyDialog.java @@ -0,0 +1,316 @@ +package de.danoeh.antennapod.ui.screen.preferences; + +import android.app.Dialog; +import android.content.Context; +import android.content.res.TypedArray; +import android.os.Build; +import androidx.appcompat.app.AlertDialog; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import android.text.Editable; +import android.text.TextUtils; +import android.text.TextWatcher; +import android.util.Patterns; +import android.view.View; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.EditText; +import android.widget.Spinner; +import android.widget.TextView; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.net.SocketAddress; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.concurrent.TimeUnit; + +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.storage.preferences.UserPreferences; +import de.danoeh.antennapod.net.common.AntennapodHttpClient; +import de.danoeh.antennapod.model.download.ProxyConfig; +import de.danoeh.antennapod.ui.common.ThemeUtils; +import io.reactivex.Completable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; +import io.reactivex.schedulers.Schedulers; +import okhttp3.Credentials; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; + +public class ProxyDialog { + private final Context context; + + private AlertDialog dialog; + + private Spinner spType; + private EditText etHost; + private EditText etPort; + private EditText etUsername; + private EditText etPassword; + + private boolean testSuccessful = false; + private TextView txtvMessage; + private Disposable disposable; + + public ProxyDialog(Context context) { + this.context = context; + } + + public Dialog show() { + View content = View.inflate(context, R.layout.proxy_settings, null); + spType = content.findViewById(R.id.spType); + + dialog = new MaterialAlertDialogBuilder(context) + .setTitle(R.string.pref_proxy_title) + .setView(content) + .setNegativeButton(R.string.cancel_label, null) + .setPositiveButton(R.string.proxy_test_label, null) + .setNeutralButton(R.string.reset, null) + .show(); + // To prevent cancelling the dialog on button click + dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener((view) -> { + if (!testSuccessful) { + dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false); + test(); + return; + } + setProxyConfig(); + AntennapodHttpClient.reinit(); + dialog.dismiss(); + }); + + dialog.getButton(AlertDialog.BUTTON_NEUTRAL).setOnClickListener((view) -> { + etHost.getText().clear(); + etPort.getText().clear(); + etUsername.getText().clear(); + etPassword.getText().clear(); + setProxyConfig(); + }); + + List types = new ArrayList<>(); + types.add(Proxy.Type.DIRECT.name()); + types.add(Proxy.Type.HTTP.name()); + if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + types.add(Proxy.Type.SOCKS.name()); + } + ArrayAdapter adapter = new ArrayAdapter<>(context, + android.R.layout.simple_spinner_item, types); + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + spType.setAdapter(adapter); + ProxyConfig proxyConfig = UserPreferences.getProxyConfig(); + spType.setSelection(adapter.getPosition(proxyConfig.type.name())); + etHost = content.findViewById(R.id.etHost); + if (!TextUtils.isEmpty(proxyConfig.host)) { + etHost.setText(proxyConfig.host); + } + etHost.addTextChangedListener(requireTestOnChange); + etPort = content.findViewById(R.id.etPort); + if (proxyConfig.port > 0) { + etPort.setText(String.valueOf(proxyConfig.port)); + } + etPort.addTextChangedListener(requireTestOnChange); + etUsername = content.findViewById(R.id.etUsername); + if (!TextUtils.isEmpty(proxyConfig.username)) { + etUsername.setText(proxyConfig.username); + } + etUsername.addTextChangedListener(requireTestOnChange); + etPassword = content.findViewById(R.id.etPassword); + if (!TextUtils.isEmpty(proxyConfig.password)) { + etPassword.setText(proxyConfig.password); + } + etPassword.addTextChangedListener(requireTestOnChange); + if (proxyConfig.type == Proxy.Type.DIRECT) { + enableSettings(false); + setTestRequired(false); + } + spType.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + if (position == 0) { + dialog.getButton(AlertDialog.BUTTON_NEUTRAL).setVisibility(View.GONE); + } else { + dialog.getButton(AlertDialog.BUTTON_NEUTRAL).setVisibility(View.VISIBLE); + } + enableSettings(position > 0); + setTestRequired(position > 0); + } + + @Override + public void onNothingSelected(AdapterView parent) { + enableSettings(false); + } + }); + txtvMessage = content.findViewById(R.id.txtvMessage); + checkValidity(); + return dialog; + } + + private void setProxyConfig() { + final String type = (String) spType.getSelectedItem(); + final Proxy.Type typeEnum = Proxy.Type.valueOf(type); + final String host = etHost.getText().toString(); + final String port = etPort.getText().toString(); + + String username = etUsername.getText().toString(); + if (TextUtils.isEmpty(username)) { + username = null; + } + String password = etPassword.getText().toString(); + if (TextUtils.isEmpty(password)) { + password = null; + } + int portValue = 0; + if (!TextUtils.isEmpty(port)) { + portValue = Integer.parseInt(port); + } + ProxyConfig config = new ProxyConfig(typeEnum, host, portValue, username, password); + UserPreferences.setProxyConfig(config); + AntennapodHttpClient.setProxyConfig(config); + } + + private final TextWatcher requireTestOnChange = new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) {} + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) {} + + @Override + public void afterTextChanged(Editable s) { + setTestRequired(true); + } + }; + + private void enableSettings(boolean enable) { + etHost.setEnabled(enable); + etPort.setEnabled(enable); + etUsername.setEnabled(enable); + etPassword.setEnabled(enable); + } + + private boolean checkValidity() { + boolean valid = true; + if (spType.getSelectedItemPosition() > 0) { + valid = checkHost(); + } + valid &= checkPort(); + return valid; + } + + private boolean checkHost() { + String host = etHost.getText().toString(); + if (host.length() == 0) { + etHost.setError(context.getString(R.string.proxy_host_empty_error)); + return false; + } + if (!"localhost".equals(host) && !Patterns.DOMAIN_NAME.matcher(host).matches()) { + etHost.setError(context.getString(R.string.proxy_host_invalid_error)); + return false; + } + return true; + } + + private boolean checkPort() { + int port = getPort(); + if (port < 0 || port > 65535) { + etPort.setError(context.getString(R.string.proxy_port_invalid_error)); + return false; + } + return true; + } + + private int getPort() { + String port = etPort.getText().toString(); + if (port.length() > 0) { + try { + return Integer.parseInt(port); + } catch (NumberFormatException e) { + // ignore + } + } + return 0; + } + + private void setTestRequired(boolean required) { + if (required) { + testSuccessful = false; + dialog.getButton(AlertDialog.BUTTON_POSITIVE).setText(R.string.proxy_test_label); + } else { + testSuccessful = true; + dialog.getButton(AlertDialog.BUTTON_POSITIVE).setText(android.R.string.ok); + } + dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(true); + } + + private void test() { + if (disposable != null) { + disposable.dispose(); + } + if (!checkValidity()) { + setTestRequired(true); + return; + } + TypedArray res = context.getTheme().obtainStyledAttributes(new int[] { android.R.attr.textColorPrimary }); + int textColorPrimary = res.getColor(0, 0); + res.recycle(); + txtvMessage.setTextColor(textColorPrimary); + txtvMessage.setText(R.string.proxy_checking); + txtvMessage.setVisibility(View.VISIBLE); + disposable = Completable.create(emitter -> { + String type = (String) spType.getSelectedItem(); + String host = etHost.getText().toString(); + String port = etPort.getText().toString(); + String username = etUsername.getText().toString(); + String password = etPassword.getText().toString(); + int portValue = 8080; + if (!TextUtils.isEmpty(port)) { + portValue = Integer.parseInt(port); + } + SocketAddress address = InetSocketAddress.createUnresolved(host, portValue); + Proxy.Type proxyType = Proxy.Type.valueOf(type.toUpperCase(Locale.US)); + OkHttpClient.Builder builder = AntennapodHttpClient.newBuilder() + .connectTimeout(10, TimeUnit.SECONDS) + .proxy(new Proxy(proxyType, address)); + if (!TextUtils.isEmpty(username)) { + builder.proxyAuthenticator((route, response) -> { + String credentials = Credentials.basic(username, password); + return response.request().newBuilder() + .header("Proxy-Authorization", credentials) + .build(); + }); + } + OkHttpClient client = builder.build(); + Request request = new Request.Builder().url("https://www.example.com").head().build(); + try (Response response = client.newCall(request).execute()) { + if (response.isSuccessful()) { + emitter.onComplete(); + } else { + emitter.onError(new IOException(response.message())); + } + } catch (IOException e) { + emitter.onError(e); + } + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + () -> { + txtvMessage.setTextColor(ThemeUtils.getColorFromAttr(context, R.attr.icon_green)); + txtvMessage.setText(R.string.proxy_test_successful); + setTestRequired(false); + }, + error -> { + error.printStackTrace(); + txtvMessage.setTextColor(ThemeUtils.getColorFromAttr(context, R.attr.icon_red)); + String message = String.format("%s: %s", + context.getString(R.string.proxy_test_failed), error.getMessage()); + txtvMessage.setText(message); + setTestRequired(true); + } + ); + } + +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/screen/preferences/SwipePreferencesFragment.java b/app/src/main/java/de/danoeh/antennapod/ui/screen/preferences/SwipePreferencesFragment.java new file mode 100644 index 000000000..7522c2a1b --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/screen/preferences/SwipePreferencesFragment.java @@ -0,0 +1,58 @@ +package de.danoeh.antennapod.ui.screen.preferences; + +import android.os.Bundle; +import androidx.preference.PreferenceFragmentCompat; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.ui.swipeactions.SwipeActionsDialog; +import de.danoeh.antennapod.ui.screen.AllEpisodesFragment; +import de.danoeh.antennapod.ui.screen.download.CompletedDownloadsFragment; +import de.danoeh.antennapod.ui.screen.feed.FeedItemlistFragment; +import de.danoeh.antennapod.ui.screen.InboxFragment; +import de.danoeh.antennapod.ui.screen.PlaybackHistoryFragment; +import de.danoeh.antennapod.ui.screen.queue.QueueFragment; + +public class SwipePreferencesFragment extends PreferenceFragmentCompat { + private static final String PREF_SWIPE_QUEUE = "prefSwipeQueue"; + private static final String PREF_SWIPE_INBOX = "prefSwipeInbox"; + private static final String PREF_SWIPE_EPISODES = "prefSwipeEpisodes"; + private static final String PREF_SWIPE_DOWNLOADS = "prefSwipeDownloads"; + private static final String PREF_SWIPE_FEED = "prefSwipeFeed"; + private static final String PREF_SWIPE_HISTORY = "prefSwipeHistory"; + + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + addPreferencesFromResource(R.xml.preferences_swipe); + + findPreference(PREF_SWIPE_QUEUE).setOnPreferenceClickListener(preference -> { + new SwipeActionsDialog(requireContext(), QueueFragment.TAG).show(() -> { }); + return true; + }); + findPreference(PREF_SWIPE_INBOX).setOnPreferenceClickListener(preference -> { + new SwipeActionsDialog(requireContext(), InboxFragment.TAG).show(() -> { }); + return true; + }); + findPreference(PREF_SWIPE_EPISODES).setOnPreferenceClickListener(preference -> { + new SwipeActionsDialog(requireContext(), AllEpisodesFragment.TAG).show(() -> { }); + return true; + }); + findPreference(PREF_SWIPE_DOWNLOADS).setOnPreferenceClickListener(preference -> { + new SwipeActionsDialog(requireContext(), CompletedDownloadsFragment.TAG).show(() -> { }); + return true; + }); + findPreference(PREF_SWIPE_FEED).setOnPreferenceClickListener(preference -> { + new SwipeActionsDialog(requireContext(), FeedItemlistFragment.TAG).show(() -> { }); + return true; + }); + findPreference(PREF_SWIPE_HISTORY).setOnPreferenceClickListener(preference -> { + new SwipeActionsDialog(requireContext(), PlaybackHistoryFragment.TAG).show(() -> { }); + return true; + }); + } + + @Override + public void onStart() { + super.onStart(); + ((PreferenceActivity) getActivity()).getSupportActionBar().setTitle(R.string.swipeactions_label); + } + +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/screen/preferences/UserInterfacePreferencesFragment.java b/app/src/main/java/de/danoeh/antennapod/ui/screen/preferences/UserInterfacePreferencesFragment.java new file mode 100644 index 000000000..86840f759 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/screen/preferences/UserInterfacePreferencesFragment.java @@ -0,0 +1,170 @@ +package de.danoeh.antennapod.ui.screen.preferences; + +import android.content.Context; +import android.content.DialogInterface; +import android.os.Build; +import android.os.Bundle; +import android.widget.Button; +import android.widget.ListView; + +import androidx.appcompat.app.AlertDialog; +import androidx.core.app.ActivityCompat; +import androidx.preference.Preference; +import androidx.preference.PreferenceFragmentCompat; + +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import com.google.android.material.snackbar.Snackbar; + +import de.danoeh.antennapod.ui.screen.subscriptions.FeedSortDialog; +import org.greenrobot.eventbus.EventBus; + +import java.util.List; + +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.ui.screen.drawer.DrawerPreferencesDialog; +import de.danoeh.antennapod.ui.screen.subscriptions.SubscriptionsFilterDialog; +import de.danoeh.antennapod.event.PlayerStatusEvent; +import de.danoeh.antennapod.event.UnreadItemsUpdateEvent; +import de.danoeh.antennapod.storage.preferences.UserPreferences; + +public class UserInterfacePreferencesFragment extends PreferenceFragmentCompat { + private static final String PREF_SWIPE = "prefSwipe"; + + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + addPreferencesFromResource(R.xml.preferences_user_interface); + setupInterfaceScreen(); + } + + @Override + public void onStart() { + super.onStart(); + ((PreferenceActivity) getActivity()).getSupportActionBar().setTitle(R.string.user_interface_label); + } + + private void setupInterfaceScreen() { + Preference.OnPreferenceChangeListener restartApp = (preference, newValue) -> { + ActivityCompat.recreate(getActivity()); + return true; + }; + findPreference(UserPreferences.PREF_THEME).setOnPreferenceChangeListener(restartApp); + findPreference(UserPreferences.PREF_THEME_BLACK).setOnPreferenceChangeListener(restartApp); + findPreference(UserPreferences.PREF_TINTED_COLORS).setOnPreferenceChangeListener(restartApp); + if (Build.VERSION.SDK_INT < 31) { + findPreference(UserPreferences.PREF_TINTED_COLORS).setVisible(false); + } + + findPreference(UserPreferences.PREF_SHOW_TIME_LEFT) + .setOnPreferenceChangeListener( + (preference, newValue) -> { + UserPreferences.setShowRemainTimeSetting((Boolean) newValue); + EventBus.getDefault().post(new UnreadItemsUpdateEvent()); + EventBus.getDefault().post(new PlayerStatusEvent()); + return true; + }); + + findPreference(UserPreferences.PREF_HIDDEN_DRAWER_ITEMS) + .setOnPreferenceClickListener(preference -> { + DrawerPreferencesDialog.show(getContext(), null); + return true; + }); + + findPreference(UserPreferences.PREF_FULL_NOTIFICATION_BUTTONS) + .setOnPreferenceClickListener(preference -> { + showFullNotificationButtonsDialog(); + return true; + }); + findPreference(UserPreferences.PREF_FILTER_FEED) + .setOnPreferenceClickListener((preference -> { + new SubscriptionsFilterDialog().show(getChildFragmentManager(), "filter"); + return true; + })); + + findPreference(UserPreferences.PREF_DRAWER_FEED_ORDER) + .setOnPreferenceClickListener((preference -> { + FeedSortDialog.showDialog(requireContext()); + return true; + })); + findPreference(PREF_SWIPE) + .setOnPreferenceClickListener(preference -> { + ((PreferenceActivity) getActivity()).openScreen(R.xml.preferences_swipe); + return true; + }); + + if (Build.VERSION.SDK_INT >= 26) { + findPreference(UserPreferences.PREF_EXPANDED_NOTIFICATION).setVisible(false); + } + } + + private void showFullNotificationButtonsDialog() { + final Context context = getActivity(); + + final List preferredButtons = UserPreferences.getFullNotificationButtons(); + final String[] allButtonNames = context.getResources().getStringArray( + R.array.full_notification_buttons_options); + final int[] buttonIds = { + UserPreferences.NOTIFICATION_BUTTON_SKIP, + UserPreferences.NOTIFICATION_BUTTON_NEXT_CHAPTER, + UserPreferences.NOTIFICATION_BUTTON_PLAYBACK_SPEED, + UserPreferences.NOTIFICATION_BUTTON_SLEEP_TIMER, + }; + final DialogInterface.OnClickListener completeListener = (dialog, which) -> + UserPreferences.setFullNotificationButtons(preferredButtons); + final String title = context.getResources().getString(R.string.pref_full_notification_buttons_title); + + boolean[] checked = new boolean[allButtonNames.length]; // booleans default to false in java + + // Clear buttons that are not part of the setting anymore + for (int i = preferredButtons.size() - 1; i >= 0; i--) { + boolean isValid = false; + for (int j = 0; j < checked.length; j++) { + if (buttonIds[j] == preferredButtons.get(i)) { + isValid = true; + break; + } + } + + if (!isValid) { + preferredButtons.remove(i); + } + } + + for (int i = 0; i < checked.length; i++) { + if (preferredButtons.contains(buttonIds[i])) { + checked[i] = true; + } + } + + MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(context); + builder.setTitle(title); + builder.setMultiChoiceItems(allButtonNames, checked, (dialog, which, isChecked) -> { + checked[which] = isChecked; + if (isChecked) { + preferredButtons.add(buttonIds[which]); + } else { + preferredButtons.remove((Integer) buttonIds[which]); + } + }); + builder.setPositiveButton(R.string.confirm_label, null); + builder.setNegativeButton(R.string.cancel_label, null); + final AlertDialog dialog = builder.create(); + + dialog.show(); + + Button positiveButton = dialog.getButton(AlertDialog.BUTTON_POSITIVE); + + positiveButton.setOnClickListener(v -> { + if (preferredButtons.size() != 2) { + ListView selectionView = dialog.getListView(); + Snackbar.make( + selectionView, + context.getResources().getString(R.string.pref_compact_notification_buttons_dialog_error_exact), + Snackbar.LENGTH_SHORT).show(); + + } else { + completeListener.onClick(dialog, AlertDialog.BUTTON_POSITIVE); + dialog.cancel(); + } + }); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/screen/queue/QueueFragment.java b/app/src/main/java/de/danoeh/antennapod/ui/screen/queue/QueueFragment.java new file mode 100644 index 000000000..31fedda1d --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/screen/queue/QueueFragment.java @@ -0,0 +1,639 @@ +package de.danoeh.antennapod.ui.screen.queue; + +import android.content.Context; +import android.content.DialogInterface; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.util.Log; +import android.view.ContextMenu; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CheckBox; +import android.widget.ProgressBar; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.ItemTouchHelper; +import androidx.recyclerview.widget.RecyclerView; +import androidx.recyclerview.widget.SimpleItemAnimator; +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; + +import com.google.android.material.appbar.MaterialToolbar; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import com.google.android.material.snackbar.Snackbar; +import com.leinardi.android.speeddial.SpeedDialView; + +import de.danoeh.antennapod.ui.screen.SearchFragment; +import de.danoeh.antennapod.net.download.serviceinterface.FeedUpdateManager; +import de.danoeh.antennapod.ui.episodes.PlaybackSpeedUtils; +import org.greenrobot.eventbus.EventBus; +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; + +import java.util.List; + +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.ui.episodeslist.EpisodeItemListAdapter; +import de.danoeh.antennapod.core.util.ConfirmationDialog; +import de.danoeh.antennapod.ui.MenuItemUtils; +import de.danoeh.antennapod.storage.database.DBReader; +import de.danoeh.antennapod.storage.database.DBWriter; +import de.danoeh.antennapod.ui.common.Converter; +import de.danoeh.antennapod.core.util.FeedItemUtil; +import de.danoeh.antennapod.ui.screen.feed.ItemSortDialog; +import de.danoeh.antennapod.event.EpisodeDownloadEvent; +import de.danoeh.antennapod.event.FeedItemEvent; +import de.danoeh.antennapod.event.FeedUpdateRunningEvent; +import de.danoeh.antennapod.event.PlayerStatusEvent; +import de.danoeh.antennapod.event.QueueEvent; +import de.danoeh.antennapod.event.UnreadItemsUpdateEvent; +import de.danoeh.antennapod.event.playback.PlaybackPositionEvent; +import de.danoeh.antennapod.ui.episodeslist.EpisodeMultiSelectActionHandler; +import de.danoeh.antennapod.ui.swipeactions.SwipeActions; +import de.danoeh.antennapod.ui.episodeslist.FeedItemMenuHandler; +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedItemFilter; +import de.danoeh.antennapod.model.feed.SortOrder; +import de.danoeh.antennapod.storage.preferences.UserPreferences; +import de.danoeh.antennapod.ui.view.EmptyViewHandler; +import de.danoeh.antennapod.ui.episodeslist.EpisodeItemListRecyclerView; +import de.danoeh.antennapod.ui.view.LiftOnScrollListener; +import de.danoeh.antennapod.ui.episodeslist.EpisodeItemViewHolder; +import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; +import io.reactivex.schedulers.Schedulers; + +/** + * Shows all items in the queue. + */ +public class QueueFragment extends Fragment implements MaterialToolbar.OnMenuItemClickListener, + EpisodeItemListAdapter.OnSelectModeListener { + public static final String TAG = "QueueFragment"; + private static final String KEY_UP_ARROW = "up_arrow"; + + private TextView infoBar; + private EpisodeItemListRecyclerView recyclerView; + private QueueRecyclerAdapter recyclerAdapter; + private EmptyViewHandler emptyView; + private MaterialToolbar toolbar; + private SwipeRefreshLayout swipeRefreshLayout; + private boolean displayUpArrow; + + private List queue; + + private static final String PREFS = "QueueFragment"; + private static final String PREF_SHOW_LOCK_WARNING = "show_lock_warning"; + + private Disposable disposable; + private SwipeActions swipeActions; + private SharedPreferences prefs; + + private SpeedDialView speedDialView; + private ProgressBar progressBar; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + prefs = getActivity().getSharedPreferences(PREFS, Context.MODE_PRIVATE); + } + + @Override + public void onStart() { + super.onStart(); + if (queue != null) { + recyclerView.restoreScrollPosition(QueueFragment.TAG); + } + loadItems(true); + EventBus.getDefault().register(this); + } + + @Override + public void onPause() { + super.onPause(); + recyclerView.saveScrollPosition(QueueFragment.TAG); + } + + @Override + public void onStop() { + super.onStop(); + EventBus.getDefault().unregister(this); + if (disposable != null) { + disposable.dispose(); + } + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onEventMainThread(QueueEvent event) { + Log.d(TAG, "onEventMainThread() called with: " + "event = [" + event + "]"); + if (queue == null) { + return; + } else if (recyclerAdapter == null) { + loadItems(true); + return; + } + switch(event.action) { + case ADDED: + queue.add(event.position, event.item); + recyclerAdapter.notifyItemInserted(event.position); + break; + case SET_QUEUE: + case SORTED: //Deliberate fall-through + queue = event.items; + recyclerAdapter.updateItems(event.items); + break; + case REMOVED: + case IRREVERSIBLE_REMOVED: + int position = FeedItemUtil.indexOfItemWithId(queue, event.item.getId()); + queue.remove(position); + recyclerAdapter.notifyItemRemoved(position); + break; + case CLEARED: + queue.clear(); + recyclerAdapter.updateItems(queue); + break; + case MOVED: + return; + } + recyclerAdapter.updateDragDropEnabled(); + refreshToolbarState(); + recyclerView.saveScrollPosition(QueueFragment.TAG); + refreshInfoBar(); + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onEventMainThread(FeedItemEvent event) { + Log.d(TAG, "onEventMainThread() called with: " + "event = [" + event + "]"); + if (queue == null) { + return; + } else if (recyclerAdapter == null) { + loadItems(true); + return; + } + for (int i = 0, size = event.items.size(); i < size; i++) { + FeedItem item = event.items.get(i); + int pos = FeedItemUtil.indexOfItemWithId(queue, item.getId()); + if (pos >= 0) { + queue.remove(pos); + queue.add(pos, item); + recyclerAdapter.notifyItemChangedCompat(pos); + refreshInfoBar(); + } + } + } + + @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) + public void onEventMainThread(EpisodeDownloadEvent event) { + if (queue == null) { + return; + } + for (String downloadUrl : event.getUrls()) { + int pos = FeedItemUtil.indexOfItemWithDownloadUrl(queue, downloadUrl); + if (pos >= 0) { + recyclerAdapter.notifyItemChangedCompat(pos); + } + } + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onEventMainThread(PlaybackPositionEvent event) { + if (recyclerAdapter != null) { + for (int i = 0; i < recyclerAdapter.getItemCount(); i++) { + EpisodeItemViewHolder holder = (EpisodeItemViewHolder) + recyclerView.findViewHolderForAdapterPosition(i); + if (holder != null && holder.isCurrentlyPlayingItem()) { + holder.notifyPlaybackPositionUpdated(event); + break; + } + } + } + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onPlayerStatusChanged(PlayerStatusEvent event) { + loadItems(false); + refreshToolbarState(); + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onUnreadItemsChanged(UnreadItemsUpdateEvent event) { + // Sent when playback position is reset + loadItems(false); + refreshToolbarState(); + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onKeyUp(KeyEvent event) { + if (!isAdded() || !isVisible() || !isMenuVisible()) { + return; + } + switch (event.getKeyCode()) { + case KeyEvent.KEYCODE_T: + recyclerView.smoothScrollToPosition(0); + break; + case KeyEvent.KEYCODE_B: + recyclerView.smoothScrollToPosition(recyclerAdapter.getItemCount() - 1); + break; + default: + break; + } + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + if (recyclerAdapter != null) { + recyclerAdapter.endSelectMode(); + } + recyclerAdapter = null; + if (toolbar != null) { + toolbar.setOnMenuItemClickListener(null); + toolbar.setOnLongClickListener(null); + } + } + + private void refreshToolbarState() { + boolean keepSorted = UserPreferences.isQueueKeepSorted(); + toolbar.getMenu().findItem(R.id.queue_lock).setChecked(UserPreferences.isQueueLocked()); + toolbar.getMenu().findItem(R.id.queue_lock).setVisible(!keepSorted); + } + + @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) + public void onEventMainThread(FeedUpdateRunningEvent event) { + swipeRefreshLayout.setRefreshing(event.isFeedUpdateRunning); + } + + @Override + public boolean onMenuItemClick(MenuItem item) { + final int itemId = item.getItemId(); + if (itemId == R.id.queue_lock) { + toggleQueueLock(); + return true; + } else if (itemId == R.id.queue_sort) { + new QueueSortDialog().show(getChildFragmentManager().beginTransaction(), "SortDialog"); + return true; + } else if (itemId == R.id.refresh_item) { + FeedUpdateManager.getInstance().runOnceOrAsk(requireContext()); + return true; + } else if (itemId == R.id.clear_queue) { + // make sure the user really wants to clear the queue + ConfirmationDialog conDialog = new ConfirmationDialog(getActivity(), + R.string.clear_queue_label, + R.string.clear_queue_confirmation_msg) { + + @Override + public void onConfirmButtonPressed( + DialogInterface dialog) { + dialog.dismiss(); + DBWriter.clearQueue(); + } + }; + conDialog.createNewDialog().show(); + return true; + } else if (itemId == R.id.action_search) { + ((MainActivity) getActivity()).loadChildFragment(SearchFragment.newInstance()); + return true; + } + return false; + } + + private void toggleQueueLock() { + boolean isLocked = UserPreferences.isQueueLocked(); + if (isLocked) { + setQueueLocked(false); + } else { + boolean shouldShowLockWarning = prefs.getBoolean(PREF_SHOW_LOCK_WARNING, true); + if (!shouldShowLockWarning) { + setQueueLocked(true); + } else { + MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(getContext()); + builder.setTitle(R.string.lock_queue); + builder.setMessage(R.string.queue_lock_warning); + + View view = View.inflate(getContext(), R.layout.checkbox_do_not_show_again, null); + CheckBox checkDoNotShowAgain = view.findViewById(R.id.checkbox_do_not_show_again); + builder.setView(view); + + builder.setPositiveButton(R.string.lock_queue, (dialog, which) -> { + prefs.edit().putBoolean(PREF_SHOW_LOCK_WARNING, !checkDoNotShowAgain.isChecked()).apply(); + setQueueLocked(true); + }); + builder.setNegativeButton(R.string.cancel_label, null); + builder.show(); + } + } + } + + private void setQueueLocked(boolean locked) { + UserPreferences.setQueueLocked(locked); + refreshToolbarState(); + if (recyclerAdapter != null) { + recyclerAdapter.updateDragDropEnabled(); + } + if (queue.size() == 0) { + if (locked) { + ((MainActivity) getActivity()).showSnackbarAbovePlayer(R.string.queue_locked, Snackbar.LENGTH_SHORT); + } else { + ((MainActivity) getActivity()).showSnackbarAbovePlayer(R.string.queue_unlocked, Snackbar.LENGTH_SHORT); + } + } + } + + @Override + public boolean onContextItemSelected(MenuItem item) { + Log.d(TAG, "onContextItemSelected() called with: " + "item = [" + item + "]"); + if (!isVisible() || recyclerAdapter == null) { + return false; + } + FeedItem selectedItem = recyclerAdapter.getLongPressedItem(); + if (selectedItem == null) { + Log.i(TAG, "Selected item was null, ignoring selection"); + return super.onContextItemSelected(item); + } + + int position = FeedItemUtil.indexOfItemWithId(queue, selectedItem.getId()); + if (position < 0) { + Log.i(TAG, "Selected item no longer exist, ignoring selection"); + return super.onContextItemSelected(item); + } + if (recyclerAdapter.onContextItemSelected(item)) { + return true; + } + + final int itemId = item.getItemId(); + if (itemId == R.id.move_to_top_item) { + queue.add(0, queue.remove(position)); + recyclerAdapter.notifyItemMoved(position, 0); + DBWriter.moveQueueItemToTop(selectedItem.getId(), true); + return true; + } else if (itemId == R.id.move_to_bottom_item) { + queue.add(queue.size() - 1, queue.remove(position)); + recyclerAdapter.notifyItemMoved(position, queue.size() - 1); + DBWriter.moveQueueItemToBottom(selectedItem.getId(), true); + return true; + } + return FeedItemMenuHandler.onMenuItemClicked(this, item.getItemId(), selectedItem); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + super.onCreateView(inflater, container, savedInstanceState); + View root = inflater.inflate(R.layout.queue_fragment, container, false); + toolbar = root.findViewById(R.id.toolbar); + toolbar.setOnMenuItemClickListener(this); + toolbar.setOnLongClickListener(v -> { + recyclerView.scrollToPosition(5); + recyclerView.post(() -> recyclerView.smoothScrollToPosition(0)); + return false; + }); + displayUpArrow = getParentFragmentManager().getBackStackEntryCount() != 0; + if (savedInstanceState != null) { + displayUpArrow = savedInstanceState.getBoolean(KEY_UP_ARROW); + } + ((MainActivity) getActivity()).setupToolbarToggle(toolbar, displayUpArrow); + toolbar.inflateMenu(R.menu.queue); + refreshToolbarState(); + progressBar = root.findViewById(R.id.progressBar); + progressBar.setVisibility(View.VISIBLE); + + infoBar = root.findViewById(R.id.info_bar); + recyclerView = root.findViewById(R.id.recyclerView); + RecyclerView.ItemAnimator animator = recyclerView.getItemAnimator(); + if (animator instanceof SimpleItemAnimator) { + ((SimpleItemAnimator) animator).setSupportsChangeAnimations(false); + } + recyclerView.setRecycledViewPool(((MainActivity) getActivity()).getRecycledViewPool()); + registerForContextMenu(recyclerView); + recyclerView.addOnScrollListener(new LiftOnScrollListener(root.findViewById(R.id.appbar))); + + swipeActions = new QueueSwipeActions(); + swipeActions.setFilter(new FeedItemFilter(FeedItemFilter.QUEUED)); + swipeActions.attachTo(recyclerView); + + recyclerAdapter = new QueueRecyclerAdapter((MainActivity) getActivity(), swipeActions) { + @Override + public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { + super.onCreateContextMenu(menu, v, menuInfo); + MenuItemUtils.setOnClickListeners(menu, QueueFragment.this::onContextItemSelected); + } + }; + recyclerAdapter.setOnSelectModeListener(this); + recyclerView.setAdapter(recyclerAdapter); + + swipeRefreshLayout = root.findViewById(R.id.swipeRefresh); + swipeRefreshLayout.setDistanceToTriggerSync(getResources().getInteger(R.integer.swipe_refresh_distance)); + swipeRefreshLayout.setOnRefreshListener(() -> FeedUpdateManager.getInstance().runOnceOrAsk(requireContext())); + + emptyView = new EmptyViewHandler(getContext()); + emptyView.attachToRecyclerView(recyclerView); + emptyView.setIcon(R.drawable.ic_playlist_play); + emptyView.setTitle(R.string.no_items_header_label); + emptyView.setMessage(R.string.no_items_label); + emptyView.updateAdapter(recyclerAdapter); + + speedDialView = root.findViewById(R.id.fabSD); + speedDialView.setOverlayLayout(root.findViewById(R.id.fabSDOverlay)); + speedDialView.inflate(R.menu.episodes_apply_action_speeddial); + speedDialView.removeActionItemById(R.id.mark_read_batch); + speedDialView.removeActionItemById(R.id.mark_unread_batch); + speedDialView.removeActionItemById(R.id.add_to_queue_batch); + speedDialView.removeActionItemById(R.id.remove_all_inbox_item); + speedDialView.setOnChangeListener(new SpeedDialView.OnChangeListener() { + @Override + public boolean onMainActionSelected() { + return false; + } + + @Override + public void onToggleChanged(boolean open) { + if (open && recyclerAdapter.getSelectedCount() == 0) { + ((MainActivity) getActivity()).showSnackbarAbovePlayer(R.string.no_items_selected, + Snackbar.LENGTH_SHORT); + speedDialView.close(); + } + } + }); + speedDialView.setOnActionSelectedListener(actionItem -> { + new EpisodeMultiSelectActionHandler(((MainActivity) getActivity()), actionItem.getId()) + .handleAction(recyclerAdapter.getSelectedItems()); + recyclerAdapter.endSelectMode(); + return true; + }); + return root; + } + + @Override + public void onSaveInstanceState(@NonNull Bundle outState) { + outState.putBoolean(KEY_UP_ARROW, displayUpArrow); + super.onSaveInstanceState(outState); + } + + private void refreshInfoBar() { + String info = getResources().getQuantityString(R.plurals.num_episodes, queue.size(), queue.size()); + if (!queue.isEmpty()) { + long timeLeft = 0; + for (FeedItem item : queue) { + float playbackSpeed = 1; + if (UserPreferences.timeRespectsSpeed()) { + playbackSpeed = PlaybackSpeedUtils.getCurrentPlaybackSpeed(item.getMedia()); + } + if (item.getMedia() != null) { + long itemTimeLeft = item.getMedia().getDuration() - item.getMedia().getPosition(); + timeLeft += itemTimeLeft / playbackSpeed; + } + } + info += " • "; + info += getString(R.string.time_left_label); + info += Converter.getDurationStringLocalized(getResources(), timeLeft, false); + } + infoBar.setText(info); + } + + private void loadItems(final boolean restoreScrollPosition) { + Log.d(TAG, "loadItems()"); + if (disposable != null) { + disposable.dispose(); + } + if (queue == null) { + emptyView.hide(); + } + disposable = Observable.fromCallable(DBReader::getQueue) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(items -> { + queue = items; + progressBar.setVisibility(View.GONE); + recyclerAdapter.setDummyViews(0); + recyclerAdapter.updateItems(queue); + if (restoreScrollPosition) { + recyclerView.restoreScrollPosition(QueueFragment.TAG); + } + refreshInfoBar(); + }, error -> Log.e(TAG, Log.getStackTraceString(error))); + } + + @Override + public void onStartSelectMode() { + swipeActions.detach(); + speedDialView.setVisibility(View.VISIBLE); + refreshToolbarState(); + infoBar.setVisibility(View.GONE); + } + + @Override + public void onEndSelectMode() { + speedDialView.close(); + speedDialView.setVisibility(View.GONE); + infoBar.setVisibility(View.VISIBLE); + swipeActions.attachTo(recyclerView); + } + + public static class QueueSortDialog extends ItemSortDialog { + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, + @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + if (UserPreferences.isQueueKeepSorted()) { + sortOrder = UserPreferences.getQueueKeepSortedOrder(); + } + final View view = super.onCreateView(inflater, container, savedInstanceState); + viewBinding.keepSortedCheckbox.setVisibility(View.VISIBLE); + viewBinding.keepSortedCheckbox.setChecked(UserPreferences.isQueueKeepSorted()); + // Disable until something gets selected + viewBinding.keepSortedCheckbox.setEnabled(UserPreferences.isQueueKeepSorted()); + return view; + } + + @Override + protected void onAddItem(int title, SortOrder ascending, SortOrder descending, boolean ascendingIsDefault) { + if (ascending != SortOrder.EPISODE_FILENAME_A_Z && ascending != SortOrder.SIZE_SMALL_LARGE) { + super.onAddItem(title, ascending, descending, ascendingIsDefault); + } + } + + @Override + protected void onSelectionChanged() { + super.onSelectionChanged(); + viewBinding.keepSortedCheckbox.setEnabled(sortOrder != SortOrder.RANDOM); + if (sortOrder == SortOrder.RANDOM) { + viewBinding.keepSortedCheckbox.setChecked(false); + } + UserPreferences.setQueueKeepSorted(viewBinding.keepSortedCheckbox.isChecked()); + UserPreferences.setQueueKeepSortedOrder(sortOrder); + DBWriter.reorderQueue(sortOrder, true); + } + } + + private class QueueSwipeActions extends SwipeActions { + + // Position tracking whilst dragging + int dragFrom = -1; + int dragTo = -1; + + public QueueSwipeActions() { + super(ItemTouchHelper.UP | ItemTouchHelper.DOWN, QueueFragment.this, TAG); + } + + @Override + public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, + @NonNull RecyclerView.ViewHolder target) { + int fromPosition = viewHolder.getBindingAdapterPosition(); + int toPosition = target.getBindingAdapterPosition(); + + // Update tracked position + if (dragFrom == -1) { + dragFrom = fromPosition; + } + dragTo = toPosition; + + int from = viewHolder.getBindingAdapterPosition(); + int to = target.getBindingAdapterPosition(); + Log.d(TAG, "move(" + from + ", " + to + ") in memory"); + if (queue == null || from >= queue.size() || to >= queue.size() || from < 0 || to < 0) { + return false; + } + queue.add(to, queue.remove(from)); + recyclerAdapter.notifyItemMoved(from, to); + return true; + } + + @Override + public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) { + if (disposable != null) { + disposable.dispose(); + } + + //SwipeActions + super.onSwiped(viewHolder, direction); + } + + @Override + public boolean isLongPressDragEnabled() { + return false; + } + + @Override + public void clearView(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) { + super.clearView(recyclerView, viewHolder); + // Check if drag finished + if (dragFrom != -1 && dragTo != -1 && dragFrom != dragTo) { + reallyMoved(dragFrom, dragTo); + } + + dragFrom = dragTo = -1; + } + + private void reallyMoved(int from, int to) { + // Write drag operation to database + Log.d(TAG, "Write to database move(" + from + ", " + to + ")"); + DBWriter.moveQueueItem(from, to, true); + } + + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/screen/queue/QueueRecyclerAdapter.java b/app/src/main/java/de/danoeh/antennapod/ui/screen/queue/QueueRecyclerAdapter.java new file mode 100644 index 000000000..371118166 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/screen/queue/QueueRecyclerAdapter.java @@ -0,0 +1,96 @@ +package de.danoeh.antennapod.ui.screen.queue; + +import android.annotation.SuppressLint; +import android.util.Log; +import android.view.ContextMenu; +import android.view.MenuInflater; +import android.view.MotionEvent; +import android.view.View; + +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.ui.episodeslist.EpisodeItemListAdapter; +import de.danoeh.antennapod.storage.preferences.UserPreferences; +import de.danoeh.antennapod.ui.swipeactions.SwipeActions; +import de.danoeh.antennapod.ui.episodeslist.EpisodeItemViewHolder; + +/** + * List adapter for the queue. + */ +public class QueueRecyclerAdapter extends EpisodeItemListAdapter { + private static final String TAG = "QueueRecyclerAdapter"; + + private final SwipeActions swipeActions; + private boolean dragDropEnabled; + + + public QueueRecyclerAdapter(MainActivity mainActivity, SwipeActions swipeActions) { + super(mainActivity); + this.swipeActions = swipeActions; + dragDropEnabled = ! (UserPreferences.isQueueKeepSorted() || UserPreferences.isQueueLocked()); + } + + public void updateDragDropEnabled() { + dragDropEnabled = ! (UserPreferences.isQueueKeepSorted() || UserPreferences.isQueueLocked()); + notifyDataSetChanged(); + } + + @Override + @SuppressLint("ClickableViewAccessibility") + protected void afterBindViewHolder(EpisodeItemViewHolder holder, int pos) { + if (!dragDropEnabled) { + holder.dragHandle.setVisibility(View.GONE); + holder.dragHandle.setOnTouchListener(null); + holder.coverHolder.setOnTouchListener(null); + } else { + holder.dragHandle.setVisibility(View.VISIBLE); + holder.dragHandle.setOnTouchListener((v1, event) -> { + if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { + Log.d(TAG, "startDrag()"); + swipeActions.startDrag(holder); + } + return false; + }); + holder.coverHolder.setOnTouchListener((v1, event) -> { + if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { + boolean isLtr = holder.itemView.getLayoutDirection() == View.LAYOUT_DIRECTION_LTR; + float factor = isLtr ? 1 : -1; + if (factor * event.getX() < factor * 0.5 * v1.getWidth()) { + Log.d(TAG, "startDrag()"); + swipeActions.startDrag(holder); + } else { + Log.d(TAG, "Ignoring drag in right half of the image"); + } + } + return false; + }); + } + if (inActionMode()) { + holder.dragHandle.setOnTouchListener(null); + holder.coverHolder.setOnTouchListener(null); + } + + holder.isInQueue.setVisibility(View.GONE); + } + + @Override + public void onCreateContextMenu(final ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { + MenuInflater inflater = getActivity().getMenuInflater(); + inflater.inflate(R.menu.queue_context, menu); + super.onCreateContextMenu(menu, v, menuInfo); + + if (!inActionMode()) { + menu.findItem(R.id.multi_select).setVisible(true); + final boolean keepSorted = UserPreferences.isQueueKeepSorted(); + if (getItem(0).getId() == getLongPressedItem().getId() || keepSorted) { + menu.findItem(R.id.move_to_top_item).setVisible(false); + } + if (getItem(getItemCount() - 1).getId() == getLongPressedItem().getId() || keepSorted) { + menu.findItem(R.id.move_to_bottom_item).setVisible(false); + } + } else { + menu.findItem(R.id.move_to_top_item).setVisible(false); + menu.findItem(R.id.move_to_bottom_item).setVisible(false); + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/screen/rating/RatingDialogFragment.java b/app/src/main/java/de/danoeh/antennapod/ui/screen/rating/RatingDialogFragment.java new file mode 100644 index 000000000..9adf885b1 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/screen/rating/RatingDialogFragment.java @@ -0,0 +1,79 @@ +package de.danoeh.antennapod.ui.screen.rating; + +import android.app.Dialog; +import android.content.DialogInterface; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.text.HtmlCompat; +import androidx.fragment.app.DialogFragment; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.util.IntentUtils; +import de.danoeh.antennapod.databinding.RatingDialogBinding; +import de.danoeh.antennapod.ui.common.DateFormatter; + +import java.util.Date; + +public class RatingDialogFragment extends DialogFragment { + private static final String EXTRA_TOTAL_TIME = "totalTime"; + private static final String EXTRA_OLDEST_DATE = "oldestDate"; + + public static RatingDialogFragment newInstance(long totalTime, long oldestDate) { + RatingDialogFragment fragment = new RatingDialogFragment(); + Bundle arguments = new Bundle(); + arguments.putLong(EXTRA_TOTAL_TIME, totalTime); + arguments.putLong(EXTRA_OLDEST_DATE, oldestDate); + fragment.setArguments(arguments); + return fragment; + } + + @NonNull + @Override + public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { + return new MaterialAlertDialogBuilder(getContext()) + .setView(onCreateView(getLayoutInflater(), null, savedInstanceState)) + .create(); + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, + @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + RatingDialogBinding viewBinding = RatingDialogBinding.inflate(inflater); + long totalTime = getArguments().getLong(EXTRA_TOTAL_TIME, 0); + long oldestDate = getArguments().getLong(EXTRA_OLDEST_DATE, 0); + + viewBinding.headerLabel.setText(HtmlCompat.fromHtml(getString(R.string.rating_tagline, + DateFormatter.formatAbbrev(getContext(), new Date(oldestDate)), + "
", totalTime / 3600L, + "
"), HtmlCompat.FROM_HTML_MODE_LEGACY)); + viewBinding.neverAgainButton.setOnClickListener(v -> { + new RatingDialogManager(getActivity()).saveRated(); + dismiss(); + }); + viewBinding.showLaterButton.setOnClickListener(v -> { + new RatingDialogManager(getActivity()).resetStartDate(); + dismiss(); + }); + viewBinding.rateButton.setOnClickListener(v -> { + IntentUtils.openInBrowser(getContext(), + "https://play.google.com/store/apps/details?id=de.danoeh.antennapod"); + new RatingDialogManager(getActivity()).saveRated(); + }); + viewBinding.contibuteButton.setOnClickListener(v -> { + IntentUtils.openInBrowser(getContext(), IntentUtils.getLocalizedWebsiteLink(getContext()) + "/contribute/"); + new RatingDialogManager(getActivity()).saveRated(); + }); + return viewBinding.getRoot(); + } + + @Override + public void onCancel(@NonNull DialogInterface dialog) { + super.onCancel(dialog); + new RatingDialogManager(getActivity()).resetStartDate(); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/screen/rating/RatingDialogManager.java b/app/src/main/java/de/danoeh/antennapod/ui/screen/rating/RatingDialogManager.java new file mode 100644 index 000000000..edba0ec83 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/screen/rating/RatingDialogManager.java @@ -0,0 +1,94 @@ +package de.danoeh.antennapod.ui.screen.rating; + +import android.content.Context; +import android.content.SharedPreferences; + +import java.util.concurrent.TimeUnit; + +import android.util.Log; +import androidx.fragment.app.FragmentActivity; +import de.danoeh.antennapod.core.BuildConfig; +import de.danoeh.antennapod.storage.database.DBReader; +import de.danoeh.antennapod.storage.database.StatisticsItem; +import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; +import io.reactivex.schedulers.Schedulers; +import kotlin.Pair; + +public class RatingDialogManager { + private static final int AFTER_DAYS = 20; + private static final String TAG = "RatingDialog"; + private static final String PREFS_NAME = "RatingPrefs"; + private static final String KEY_RATED = "KEY_WAS_RATED"; + private static final String KEY_FIRST_START_DATE = "KEY_FIRST_HIT_DATE"; + + private final SharedPreferences preferences; + private final FragmentActivity fragmentActivity; + private Disposable disposable; + + public RatingDialogManager(FragmentActivity activity) { + this.fragmentActivity = activity; + preferences = activity.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); + } + + public void showIfNeeded() { + //noinspection ConstantConditions + if (isRated() || BuildConfig.DEBUG || "free".equals(BuildConfig.FLAVOR)) { + return; + } else if (!enoughTimeSinceInstall()) { + return; + } + + if (disposable != null) { + disposable.dispose(); + } + disposable = Observable.fromCallable( + () -> { + DBReader.StatisticsResult statisticsData = DBReader.getStatistics(false, 0, Long.MAX_VALUE); + long totalTime = 0; + for (StatisticsItem item : statisticsData.feedTime) { + totalTime += item.timePlayed; + } + return new Pair<>(totalTime, statisticsData.oldestDate); + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(result -> { + long totalTime = result.getFirst(); + long oldestDate = result.getSecond(); + if (totalTime < TimeUnit.SECONDS.convert(15, TimeUnit.HOURS)) { + return; + } else if (oldestDate > System.currentTimeMillis() + - TimeUnit.MILLISECONDS.convert(AFTER_DAYS, TimeUnit.DAYS)) { + return; // In case the app was opened but nothing was played + } + RatingDialogFragment.newInstance(result.getFirst(), result.getSecond()) + .show(fragmentActivity.getSupportFragmentManager(), TAG); + }, error -> Log.e(TAG, Log.getStackTraceString(error))); + } + + private boolean isRated() { + return preferences.getBoolean(KEY_RATED, false); + } + + public void saveRated() { + preferences.edit().putBoolean(KEY_RATED, true).apply(); + } + + public void resetStartDate() { + preferences.edit().putLong(KEY_FIRST_START_DATE, System.currentTimeMillis()).apply(); + } + + private boolean enoughTimeSinceInstall() { + if (preferences.getLong(KEY_FIRST_START_DATE, 0) == 0) { + resetStartDate(); + return false; + } + long now = System.currentTimeMillis(); + long firstDate = preferences.getLong(KEY_FIRST_START_DATE, now); + long diff = now - firstDate; + long diffDays = TimeUnit.DAYS.convert(diff, TimeUnit.MILLISECONDS); + return diffDays >= AFTER_DAYS; + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/screen/subscriptions/FeedMenuHandler.java b/app/src/main/java/de/danoeh/antennapod/ui/screen/subscriptions/FeedMenuHandler.java new file mode 100644 index 000000000..e1e9f2287 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/screen/subscriptions/FeedMenuHandler.java @@ -0,0 +1,63 @@ +package de.danoeh.antennapod.ui.screen.subscriptions; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.DialogInterface; +import android.util.Log; +import androidx.annotation.NonNull; +import androidx.fragment.app.Fragment; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.util.ConfirmationDialog; +import de.danoeh.antennapod.storage.database.DBWriter; +import de.danoeh.antennapod.ui.screen.feed.RemoveFeedDialog; +import de.danoeh.antennapod.ui.screen.feed.RenameFeedDialog; +import de.danoeh.antennapod.ui.screen.feed.preferences.TagSettingsDialog; +import de.danoeh.antennapod.model.feed.Feed; +import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.schedulers.Schedulers; + +import java.util.Collections; +import java.util.concurrent.Callable; +import java.util.concurrent.Future; + +/** + * Handles interactions with the FeedItemMenu. + */ +public abstract class FeedMenuHandler { + private static final String TAG = "FeedMenuHandler"; + + public static boolean onMenuItemClicked(@NonNull Fragment fragment, int menuItemId, + @NonNull Feed selectedFeed, Runnable callback) { + @NonNull Context context = fragment.requireContext(); + if (menuItemId == R.id.rename_folder_item) { + new RenameFeedDialog(fragment.getActivity(), selectedFeed).show(); + } else if (menuItemId == R.id.remove_all_inbox_item) { + ConfirmationDialog dialog = new ConfirmationDialog(fragment.getActivity(), + R.string.remove_all_inbox_label, R.string.remove_all_inbox_confirmation_msg) { + @Override + @SuppressLint("CheckResult") + public void onConfirmButtonPressed(DialogInterface clickedDialog) { + clickedDialog.dismiss(); + Observable.fromCallable((Callable) () -> DBWriter.removeFeedNewFlag(selectedFeed.getId())) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(result -> callback.run(), + error -> Log.e(TAG, Log.getStackTraceString(error))); + } + }; + dialog.createNewDialog().show(); + + } else if (menuItemId == R.id.edit_tags) { + TagSettingsDialog.newInstance(Collections.singletonList(selectedFeed.getPreferences())) + .show(fragment.getChildFragmentManager(), TagSettingsDialog.TAG); + } else if (menuItemId == R.id.rename_item) { + new RenameFeedDialog(fragment.getActivity(), selectedFeed).show(); + } else if (menuItemId == R.id.remove_feed) { + RemoveFeedDialog.show(context, selectedFeed, null); + } else { + return false; + } + return true; + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/screen/subscriptions/FeedMultiSelectActionHandler.java b/app/src/main/java/de/danoeh/antennapod/ui/screen/subscriptions/FeedMultiSelectActionHandler.java new file mode 100644 index 000000000..f2c89f12f --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/screen/subscriptions/FeedMultiSelectActionHandler.java @@ -0,0 +1,138 @@ +package de.danoeh.antennapod.ui.screen.subscriptions; + +import android.util.Log; + +import androidx.annotation.PluralsRes; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import androidx.core.util.Consumer; + +import com.google.android.material.snackbar.Snackbar; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.storage.database.DBWriter; +import de.danoeh.antennapod.databinding.PlaybackSpeedFeedSettingDialogBinding; +import de.danoeh.antennapod.ui.screen.feed.RemoveFeedDialog; +import de.danoeh.antennapod.ui.screen.feed.preferences.TagSettingsDialog; +import de.danoeh.antennapod.model.feed.Feed; +import de.danoeh.antennapod.model.feed.FeedPreferences; +import de.danoeh.antennapod.ui.screen.preferences.PreferenceListDialog; +import de.danoeh.antennapod.ui.screen.preferences.PreferenceSwitchDialog; + +public class FeedMultiSelectActionHandler { + private static final String TAG = "FeedSelectHandler"; + private final MainActivity activity; + private final List selectedItems; + + public FeedMultiSelectActionHandler(MainActivity activity, List selectedItems) { + this.activity = activity; + this.selectedItems = selectedItems; + } + + public void handleAction(int id) { + if (id == R.id.remove_feed) { + RemoveFeedDialog.show(activity, selectedItems); + } else if (id == R.id.notify_new_episodes) { + notifyNewEpisodesPrefHandler(); + } else if (id == R.id.keep_updated) { + keepUpdatedPrefHandler(); + } else if (id == R.id.autodownload) { + autoDownloadPrefHandler(); + } else if (id == R.id.autoDeleteDownload) { + autoDeleteEpisodesPrefHandler(); + } else if (id == R.id.playback_speed) { + playbackSpeedPrefHandler(); + } else if (id == R.id.edit_tags) { + editFeedPrefTags(); + } else { + Log.e(TAG, "Unrecognized speed dial action item. Do nothing. id=" + id); + } + } + + private void notifyNewEpisodesPrefHandler() { + PreferenceSwitchDialog preferenceSwitchDialog = new PreferenceSwitchDialog(activity, + activity.getString(R.string.episode_notification), + activity.getString(R.string.episode_notification_summary)); + preferenceSwitchDialog.setOnPreferenceChangedListener(enabled -> + saveFeedPreferences(feedPreferences -> feedPreferences.setShowEpisodeNotification(enabled))); + preferenceSwitchDialog.openDialog(); + } + + private void autoDownloadPrefHandler() { + PreferenceSwitchDialog preferenceSwitchDialog = new PreferenceSwitchDialog(activity, + activity.getString(R.string.auto_download_settings_label), + activity.getString(R.string.auto_download_label)); + preferenceSwitchDialog.setOnPreferenceChangedListener(enabled -> + saveFeedPreferences(feedPreferences -> feedPreferences.setAutoDownload(enabled))); + preferenceSwitchDialog.openDialog(); + } + + private void playbackSpeedPrefHandler() { + PlaybackSpeedFeedSettingDialogBinding viewBinding = + PlaybackSpeedFeedSettingDialogBinding.inflate(activity.getLayoutInflater()); + viewBinding.seekBar.setProgressChangedListener(speed -> + viewBinding.currentSpeedLabel.setText(String.format(Locale.getDefault(), "%.2fx", speed))); + viewBinding.useGlobalCheckbox.setOnCheckedChangeListener((buttonView, isChecked) -> { + viewBinding.seekBar.setEnabled(!isChecked); + viewBinding.seekBar.setAlpha(isChecked ? 0.4f : 1f); + viewBinding.currentSpeedLabel.setAlpha(isChecked ? 0.4f : 1f); + }); + viewBinding.seekBar.updateSpeed(1.0f); + new MaterialAlertDialogBuilder(activity) + .setTitle(R.string.playback_speed) + .setView(viewBinding.getRoot()) + .setPositiveButton(android.R.string.ok, (dialog, which) -> { + float newSpeed = viewBinding.useGlobalCheckbox.isChecked() + ? FeedPreferences.SPEED_USE_GLOBAL : viewBinding.seekBar.getCurrentSpeed(); + saveFeedPreferences(feedPreferences -> feedPreferences.setFeedPlaybackSpeed(newSpeed)); + }) + .setNegativeButton(R.string.cancel_label, null) + .show(); + } + + private void autoDeleteEpisodesPrefHandler() { + PreferenceListDialog preferenceListDialog = new PreferenceListDialog(activity, + activity.getString(R.string.auto_delete_label)); + String[] items = activity.getResources().getStringArray(R.array.spnAutoDeleteItems); + preferenceListDialog.openDialog(items); + preferenceListDialog.setOnPreferenceChangedListener(which -> { + FeedPreferences.AutoDeleteAction autoDeleteAction = FeedPreferences.AutoDeleteAction.fromCode(which); + saveFeedPreferences(feedPreferences -> feedPreferences.setAutoDeleteAction(autoDeleteAction)); + }); + } + + private void keepUpdatedPrefHandler() { + PreferenceSwitchDialog preferenceSwitchDialog = new PreferenceSwitchDialog(activity, + activity.getString(R.string.kept_updated), + activity.getString(R.string.keep_updated_summary)); + preferenceSwitchDialog.setOnPreferenceChangedListener(keepUpdated -> + saveFeedPreferences(feedPreferences -> feedPreferences.setKeepUpdated(keepUpdated))); + preferenceSwitchDialog.openDialog(); + } + + private void showMessage(@PluralsRes int msgId, int numItems) { + activity.showSnackbarAbovePlayer(activity.getResources() + .getQuantityString(msgId, numItems, numItems), Snackbar.LENGTH_LONG); + } + + private void saveFeedPreferences(Consumer preferencesConsumer) { + for (Feed feed : selectedItems) { + preferencesConsumer.accept(feed.getPreferences()); + DBWriter.setFeedPreferences(feed.getPreferences()); + } + showMessage(R.plurals.updated_feeds_batch_label, selectedItems.size()); + } + + private void editFeedPrefTags() { + ArrayList preferencesList = new ArrayList<>(); + for (Feed feed : selectedItems) { + preferencesList.add(feed.getPreferences()); + } + TagSettingsDialog.newInstance(preferencesList).show(activity.getSupportFragmentManager(), + TagSettingsDialog.TAG); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/screen/subscriptions/FeedSortDialog.java b/app/src/main/java/de/danoeh/antennapod/ui/screen/subscriptions/FeedSortDialog.java new file mode 100644 index 000000000..7e42581ce --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/screen/subscriptions/FeedSortDialog.java @@ -0,0 +1,39 @@ +package de.danoeh.antennapod.ui.screen.subscriptions; + +import android.content.Context; + +import com.google.android.material.dialog.MaterialAlertDialogBuilder; + +import de.danoeh.antennapod.model.feed.FeedOrder; +import org.greenrobot.eventbus.EventBus; + +import java.util.Arrays; +import java.util.List; + +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.event.UnreadItemsUpdateEvent; +import de.danoeh.antennapod.storage.preferences.UserPreferences; + +public class FeedSortDialog { + public static void showDialog(Context context) { + MaterialAlertDialogBuilder dialog = new MaterialAlertDialogBuilder(context); + dialog.setTitle(context.getString(R.string.pref_nav_drawer_feed_order_title)); + dialog.setNegativeButton(android.R.string.cancel, (d, listener) -> d.dismiss()); + + int selected = UserPreferences.getFeedOrder().id; + List entryValues = + Arrays.asList(context.getResources().getStringArray(R.array.nav_drawer_feed_order_values)); + final int selectedIndex = entryValues.indexOf("" + selected); + + String[] items = context.getResources().getStringArray(R.array.nav_drawer_feed_order_options); + dialog.setSingleChoiceItems(items, selectedIndex, (d, which) -> { + if (selectedIndex != which) { + UserPreferences.setFeedOrder(FeedOrder.fromOrdinal(Integer.parseInt(entryValues.get(which)))); + //Update subscriptions + EventBus.getDefault().post(new UnreadItemsUpdateEvent()); + } + d.dismiss(); + }); + dialog.show(); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/screen/subscriptions/HorizontalFeedListAdapter.java b/app/src/main/java/de/danoeh/antennapod/ui/screen/subscriptions/HorizontalFeedListAdapter.java new file mode 100644 index 000000000..e66b447f8 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/screen/subscriptions/HorizontalFeedListAdapter.java @@ -0,0 +1,143 @@ +package de.danoeh.antennapod.ui.screen.subscriptions; + +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import androidx.annotation.NonNull; +import androidx.annotation.StringRes; +import androidx.cardview.widget.CardView; +import androidx.recyclerview.widget.RecyclerView; +import com.bumptech.glide.Glide; +import com.bumptech.glide.request.RequestOptions; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.model.feed.Feed; +import de.danoeh.antennapod.ui.screen.feed.FeedItemlistFragment; +import de.danoeh.antennapod.ui.common.SquareImageView; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.List; + +import android.view.ContextMenu; +import android.view.MenuInflater; +import androidx.annotation.Nullable; + +public class HorizontalFeedListAdapter extends RecyclerView.Adapter + implements View.OnCreateContextMenuListener { + private final WeakReference mainActivityRef; + private final List data = new ArrayList<>(); + private int dummyViews = 0; + private Feed longPressedItem; + private @StringRes int endButtonText = 0; + private Runnable endButtonAction = null; + + public HorizontalFeedListAdapter(MainActivity mainActivity) { + this.mainActivityRef = new WeakReference<>(mainActivity); + } + + public void setDummyViews(int dummyViews) { + this.dummyViews = dummyViews; + } + + public void updateData(List newData) { + data.clear(); + data.addAll(newData); + notifyDataSetChanged(); + } + + @NonNull + @Override + public Holder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View convertView = View.inflate(mainActivityRef.get(), R.layout.horizontal_feed_item, null); + return new Holder(convertView); + } + + @Override + public void onBindViewHolder(@NonNull Holder holder, int position) { + if (position == getItemCount() - 1 && endButtonAction != null) { + holder.cardView.setVisibility(View.GONE); + holder.actionButton.setVisibility(View.VISIBLE); + holder.actionButton.setText(endButtonText); + holder.actionButton.setOnClickListener(v -> endButtonAction.run()); + return; + } + holder.cardView.setVisibility(View.VISIBLE); + holder.actionButton.setVisibility(View.GONE); + if (position >= data.size()) { + holder.itemView.setAlpha(0.1f); + Glide.with(mainActivityRef.get()).clear(holder.imageView); + holder.imageView.setImageResource(R.color.medium_gray); + return; + } + + holder.itemView.setAlpha(1.0f); + final Feed podcast = data.get(position); + holder.imageView.setContentDescription(podcast.getTitle()); + holder.imageView.setOnClickListener(v -> + mainActivityRef.get().loadChildFragment(FeedItemlistFragment.newInstance(podcast.getId()))); + + holder.imageView.setOnCreateContextMenuListener(this); + holder.imageView.setOnLongClickListener(v -> { + int currentItemPosition = holder.getBindingAdapterPosition(); + longPressedItem = data.get(currentItemPosition); + return false; + }); + + Glide.with(mainActivityRef.get()) + .load(podcast.getImageUrl()) + .apply(new RequestOptions() + .placeholder(R.color.light_gray) + .fitCenter() + .dontAnimate()) + .into(holder.imageView); + } + + @Nullable + public Feed getLongPressedItem() { + return longPressedItem; + } + + @Override + public long getItemId(int position) { + if (position >= data.size()) { + return RecyclerView.NO_ID; // Dummy views + } + return data.get(position).getId(); + } + + @Override + public int getItemCount() { + return dummyViews + data.size() + ((endButtonAction == null) ? 0 : 1); + } + + @Override + public void onCreateContextMenu(ContextMenu contextMenu, View view, ContextMenu.ContextMenuInfo contextMenuInfo) { + MenuInflater inflater = mainActivityRef.get().getMenuInflater(); + if (longPressedItem == null) { + return; + } + inflater.inflate(R.menu.nav_feed_context, contextMenu); + contextMenu.setHeaderTitle(longPressedItem.getTitle()); + } + + public void setEndButton(@StringRes int text, Runnable action) { + endButtonAction = action; + endButtonText = text; + notifyDataSetChanged(); + } + + static class Holder extends RecyclerView.ViewHolder { + SquareImageView imageView; + CardView cardView; + Button actionButton; + + public Holder(@NonNull View itemView) { + super(itemView); + imageView = itemView.findViewById(R.id.discovery_cover); + imageView.setDirection(SquareImageView.DIRECTION_HEIGHT); + actionButton = itemView.findViewById(R.id.actionButton); + cardView = itemView.findViewById(R.id.cardView); + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/screen/subscriptions/SubscriptionFragment.java b/app/src/main/java/de/danoeh/antennapod/ui/screen/subscriptions/SubscriptionFragment.java new file mode 100644 index 000000000..f115db0ff --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/screen/subscriptions/SubscriptionFragment.java @@ -0,0 +1,364 @@ +package de.danoeh.antennapod.ui.screen.subscriptions; + +import android.content.Context; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.util.Log; +import android.view.ContextMenu; +import android.view.LayoutInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; +import android.widget.ProgressBar; + +import androidx.annotation.NonNull; +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; + +import com.google.android.material.appbar.MaterialToolbar; +import com.google.android.material.floatingactionbutton.FloatingActionButton; +import com.leinardi.android.speeddial.SpeedDialView; + +import de.danoeh.antennapod.ui.screen.AddFeedFragment; +import de.danoeh.antennapod.ui.screen.SearchFragment; +import de.danoeh.antennapod.net.download.serviceinterface.FeedUpdateManager; +import org.greenrobot.eventbus.EventBus; +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.ui.MenuItemUtils; +import de.danoeh.antennapod.storage.database.DBReader; +import de.danoeh.antennapod.storage.database.NavDrawerData; +import de.danoeh.antennapod.ui.screen.feed.RenameFeedDialog; +import de.danoeh.antennapod.event.FeedListUpdateEvent; +import de.danoeh.antennapod.event.FeedUpdateRunningEvent; +import de.danoeh.antennapod.event.UnreadItemsUpdateEvent; +import de.danoeh.antennapod.model.feed.Feed; +import de.danoeh.antennapod.storage.preferences.UserPreferences; +import de.danoeh.antennapod.ui.statistics.StatisticsFragment; +import de.danoeh.antennapod.ui.view.EmptyViewHandler; +import de.danoeh.antennapod.ui.view.LiftOnScrollListener; +import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; +import io.reactivex.schedulers.Schedulers; + +/** + * Fragment for displaying feed subscriptions + */ +public class SubscriptionFragment extends Fragment + implements MaterialToolbar.OnMenuItemClickListener, + SubscriptionsRecyclerAdapter.OnSelectModeListener { + public static final String TAG = "SubscriptionFragment"; + private static final String PREFS = "SubscriptionFragment"; + private static final String PREF_NUM_COLUMNS = "columns"; + private static final String KEY_UP_ARROW = "up_arrow"; + private static final String ARGUMENT_FOLDER = "folder"; + + private static final int MIN_NUM_COLUMNS = 2; + private static final int[] COLUMN_CHECKBOX_IDS = { + R.id.subscription_num_columns_2, + R.id.subscription_num_columns_3, + R.id.subscription_num_columns_4, + R.id.subscription_num_columns_5}; + + private RecyclerView subscriptionRecycler; + private SubscriptionsRecyclerAdapter subscriptionAdapter; + private EmptyViewHandler emptyView; + private LinearLayout feedsFilteredMsg; + private MaterialToolbar toolbar; + private SwipeRefreshLayout swipeRefreshLayout; + private ProgressBar progressBar; + private String displayedFolder = null; + private boolean displayUpArrow; + + private Disposable disposable; + private SharedPreferences prefs; + + private FloatingActionButton subscriptionAddButton; + + private SpeedDialView speedDialView; + + private List listItems; + + public static SubscriptionFragment newInstance(String folderTitle) { + SubscriptionFragment fragment = new SubscriptionFragment(); + Bundle args = new Bundle(); + args.putString(ARGUMENT_FOLDER, folderTitle); + fragment.setArguments(args); + return fragment; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + prefs = requireActivity().getSharedPreferences(PREFS, Context.MODE_PRIVATE); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View root = inflater.inflate(R.layout.fragment_subscriptions, container, false); + toolbar = root.findViewById(R.id.toolbar); + toolbar.setOnMenuItemClickListener(this); + toolbar.setOnLongClickListener(v -> { + subscriptionRecycler.scrollToPosition(5); + subscriptionRecycler.post(() -> subscriptionRecycler.smoothScrollToPosition(0)); + return false; + }); + displayUpArrow = getParentFragmentManager().getBackStackEntryCount() != 0; + if (savedInstanceState != null) { + displayUpArrow = savedInstanceState.getBoolean(KEY_UP_ARROW); + } + ((MainActivity) getActivity()).setupToolbarToggle(toolbar, displayUpArrow); + toolbar.inflateMenu(R.menu.subscriptions); + for (int i = 0; i < COLUMN_CHECKBOX_IDS.length; i++) { + // Do this in Java to localize numbers + toolbar.getMenu().findItem(COLUMN_CHECKBOX_IDS[i]) + .setTitle(String.format(Locale.getDefault(), "%d", i + MIN_NUM_COLUMNS)); + } + refreshToolbarState(); + + if (getArguments() != null) { + displayedFolder = getArguments().getString(ARGUMENT_FOLDER, null); + if (displayedFolder != null) { + toolbar.setTitle(displayedFolder); + } + } + + subscriptionRecycler = root.findViewById(R.id.subscriptions_grid); + subscriptionRecycler.addItemDecoration(new SubscriptionsRecyclerAdapter.GridDividerItemDecorator()); + registerForContextMenu(subscriptionRecycler); + subscriptionRecycler.addOnScrollListener(new LiftOnScrollListener(root.findViewById(R.id.appbar))); + subscriptionAdapter = new SubscriptionsRecyclerAdapter((MainActivity) getActivity()) { + @Override + public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { + super.onCreateContextMenu(menu, v, menuInfo); + MenuItemUtils.setOnClickListeners(menu, SubscriptionFragment.this::onContextItemSelected); + } + }; + setColumnNumber(prefs.getInt(PREF_NUM_COLUMNS, getDefaultNumOfColumns())); + subscriptionAdapter.setOnSelectModeListener(this); + subscriptionRecycler.setAdapter(subscriptionAdapter); + setupEmptyView(); + + progressBar = root.findViewById(R.id.progressBar); + progressBar.setVisibility(View.VISIBLE); + + subscriptionAddButton = root.findViewById(R.id.subscriptions_add); + subscriptionAddButton.setOnClickListener(view -> { + if (getActivity() instanceof MainActivity) { + ((MainActivity) getActivity()).loadChildFragment(new AddFeedFragment()); + } + }); + + feedsFilteredMsg = root.findViewById(R.id.feeds_filtered_message); + feedsFilteredMsg.setOnClickListener((l) -> + new SubscriptionsFilterDialog().show(getChildFragmentManager(), "filter")); + + swipeRefreshLayout = root.findViewById(R.id.swipeRefresh); + swipeRefreshLayout.setDistanceToTriggerSync(getResources().getInteger(R.integer.swipe_refresh_distance)); + swipeRefreshLayout.setOnRefreshListener(() -> FeedUpdateManager.getInstance().runOnceOrAsk(requireContext())); + + speedDialView = root.findViewById(R.id.fabSD); + speedDialView.setOverlayLayout(root.findViewById(R.id.fabSDOverlay)); + speedDialView.inflate(R.menu.nav_feed_action_speeddial); + speedDialView.setOnActionSelectedListener(actionItem -> { + new FeedMultiSelectActionHandler((MainActivity) getActivity(), subscriptionAdapter.getSelectedItems()) + .handleAction(actionItem.getId()); + return true; + }); + + return root; + } + + @Override + public void onSaveInstanceState(@NonNull Bundle outState) { + outState.putBoolean(KEY_UP_ARROW, displayUpArrow); + super.onSaveInstanceState(outState); + } + + private void refreshToolbarState() { + int columns = prefs.getInt(PREF_NUM_COLUMNS, getDefaultNumOfColumns()); + toolbar.getMenu().findItem(COLUMN_CHECKBOX_IDS[columns - MIN_NUM_COLUMNS]).setChecked(true); + } + + @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) + public void onEventMainThread(FeedUpdateRunningEvent event) { + swipeRefreshLayout.setRefreshing(event.isFeedUpdateRunning); + } + + @Override + public boolean onMenuItemClick(MenuItem item) { + final int itemId = item.getItemId(); + if (itemId == R.id.refresh_item) { + FeedUpdateManager.getInstance().runOnceOrAsk(requireContext()); + return true; + } else if (itemId == R.id.subscriptions_filter) { + new SubscriptionsFilterDialog().show(getChildFragmentManager(), "filter"); + return true; + } else if (itemId == R.id.subscriptions_sort) { + FeedSortDialog.showDialog(requireContext()); + return true; + } else if (itemId == R.id.subscription_num_columns_2) { + setColumnNumber(2); + return true; + } else if (itemId == R.id.subscription_num_columns_3) { + setColumnNumber(3); + return true; + } else if (itemId == R.id.subscription_num_columns_4) { + setColumnNumber(4); + return true; + } else if (itemId == R.id.subscription_num_columns_5) { + setColumnNumber(5); + return true; + } else if (itemId == R.id.action_search) { + ((MainActivity) getActivity()).loadChildFragment(SearchFragment.newInstance()); + return true; + } else if (itemId == R.id.action_statistics) { + ((MainActivity) getActivity()).loadChildFragment(new StatisticsFragment()); + return true; + } + return false; + } + + private void setColumnNumber(int columns) { + GridLayoutManager gridLayoutManager = new GridLayoutManager(getContext(), + columns, RecyclerView.VERTICAL, false); + subscriptionAdapter.setColumnCount(columns); + subscriptionRecycler.setLayoutManager(gridLayoutManager); + prefs.edit().putInt(PREF_NUM_COLUMNS, columns).apply(); + refreshToolbarState(); + } + + private void setupEmptyView() { + emptyView = new EmptyViewHandler(getContext()); + emptyView.setIcon(R.drawable.ic_subscriptions); + emptyView.setTitle(R.string.no_subscriptions_head_label); + emptyView.setMessage(R.string.no_subscriptions_label); + emptyView.attachToRecyclerView(subscriptionRecycler); + } + + @Override + public void onStart() { + super.onStart(); + EventBus.getDefault().register(this); + loadSubscriptions(); + } + + @Override + public void onStop() { + super.onStop(); + EventBus.getDefault().unregister(this); + if (disposable != null) { + disposable.dispose(); + } + if (subscriptionAdapter != null) { + subscriptionAdapter.endSelectMode(); + } + } + + private void loadSubscriptions() { + if (disposable != null) { + disposable.dispose(); + } + emptyView.hide(); + disposable = Observable.fromCallable( + () -> { + NavDrawerData data = DBReader.getNavDrawerData(UserPreferences.getSubscriptionsFilter(), + UserPreferences.getFeedOrder(), UserPreferences.getFeedCounterSetting()); + List items = data.items; + for (NavDrawerData.DrawerItem item : items) { + if (item.type == NavDrawerData.DrawerItem.Type.TAG + && item.getTitle().equals(displayedFolder)) { + return ((NavDrawerData.TagDrawerItem) item).children; + } + } + return items; + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + result -> { + if (listItems != null && listItems.size() > result.size()) { + // We have fewer items. This can result in items being selected that are no longer visible. + subscriptionAdapter.endSelectMode(); + } + listItems = result; + progressBar.setVisibility(View.GONE); + subscriptionAdapter.setItems(result); + emptyView.updateVisibility(); + }, error -> { + Log.e(TAG, Log.getStackTraceString(error)); + }); + + if (UserPreferences.getSubscriptionsFilter().isEnabled()) { + feedsFilteredMsg.setVisibility(View.VISIBLE); + } else { + feedsFilteredMsg.setVisibility(View.GONE); + } + } + + private int getDefaultNumOfColumns() { + return getResources().getInteger(R.integer.subscriptions_default_num_of_columns); + } + + @Override + public boolean onContextItemSelected(MenuItem item) { + NavDrawerData.DrawerItem drawerItem = subscriptionAdapter.getSelectedItem(); + if (drawerItem == null) { + return false; + } + int itemId = item.getItemId(); + if (drawerItem.type == NavDrawerData.DrawerItem.Type.TAG && itemId == R.id.rename_folder_item) { + new RenameFeedDialog(getActivity(), drawerItem).show(); + return true; + } + + Feed feed = ((NavDrawerData.FeedDrawerItem) drawerItem).feed; + if (itemId == R.id.multi_select) { + return subscriptionAdapter.onContextItemSelected(item); + } + return FeedMenuHandler.onMenuItemClicked(this, item.getItemId(), feed, this::loadSubscriptions); + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onFeedListChanged(FeedListUpdateEvent event) { + loadSubscriptions(); + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onUnreadItemsChanged(UnreadItemsUpdateEvent event) { + loadSubscriptions(); + } + + @Override + public void onEndSelectMode() { + speedDialView.close(); + speedDialView.setVisibility(View.GONE); + subscriptionAddButton.setVisibility(View.VISIBLE); + subscriptionAdapter.setItems(listItems); + } + + @Override + public void onStartSelectMode() { + List feedsOnly = new ArrayList<>(); + for (NavDrawerData.DrawerItem item : listItems) { + if (item.type == NavDrawerData.DrawerItem.Type.FEED) { + feedsOnly.add(item); + } + } + subscriptionAdapter.setItems(feedsOnly); + speedDialView.setVisibility(View.VISIBLE); + subscriptionAddButton.setVisibility(View.GONE); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/screen/subscriptions/SubscriptionsFilterDialog.java b/app/src/main/java/de/danoeh/antennapod/ui/screen/subscriptions/SubscriptionsFilterDialog.java new file mode 100644 index 000000000..53ced5f11 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/screen/subscriptions/SubscriptionsFilterDialog.java @@ -0,0 +1,132 @@ +package de.danoeh.antennapod.ui.screen.subscriptions; + +import android.app.Dialog; +import android.os.Bundle; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.FrameLayout; +import android.widget.LinearLayout; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.android.material.bottomsheet.BottomSheetBehavior; +import com.google.android.material.bottomsheet.BottomSheetDialog; +import com.google.android.material.bottomsheet.BottomSheetDialogFragment; +import com.google.android.material.button.MaterialButtonToggleGroup; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.databinding.FilterDialogBinding; +import de.danoeh.antennapod.databinding.FilterDialogRowBinding; +import de.danoeh.antennapod.event.UnreadItemsUpdateEvent; +import de.danoeh.antennapod.model.feed.SubscriptionsFilter; +import de.danoeh.antennapod.storage.preferences.UserPreferences; +import org.greenrobot.eventbus.EventBus; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +public class SubscriptionsFilterDialog extends BottomSheetDialogFragment { + private LinearLayout rows; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + SubscriptionsFilter subscriptionsFilter = UserPreferences.getSubscriptionsFilter(); + FilterDialogBinding dialogBinding = FilterDialogBinding.inflate(inflater); + rows = dialogBinding.filterRows; + + for (SubscriptionsFilterGroup item : SubscriptionsFilterGroup.values()) { + FilterDialogRowBinding binding = FilterDialogRowBinding.inflate(inflater); + binding.getRoot().addOnButtonCheckedListener( + (group, checkedId, isChecked) -> updateFilter(getFilterValues())); + binding.buttonGroup.setWeightSum(item.values.length); + binding.filterButton1.setText(item.values[0].displayName); + binding.filterButton1.setTag(item.values[0].filterId); + if (item.values.length == 2) { + binding.filterButton2.setText(item.values[1].displayName); + binding.filterButton2.setTag(item.values[1].filterId); + } else { + binding.filterButton2.setVisibility(View.GONE); + } + binding.filterButton1.setMaxLines(3); + binding.filterButton1.setSingleLine(false); + binding.filterButton2.setMaxLines(3); + binding.filterButton2.setSingleLine(false); + rows.addView(binding.getRoot(), rows.getChildCount() - 1); + } + + final Set filterValues = new HashSet<>(Arrays.asList(subscriptionsFilter.getValues())); + for (String filterId : filterValues) { + if (!TextUtils.isEmpty(filterId)) { + Button button = dialogBinding.getRoot().findViewWithTag(filterId); + if (button != null) { + ((MaterialButtonToggleGroup) button.getParent()).check(button.getId()); + } + } + } + + dialogBinding.confirmFiltermenu.setOnClickListener(view -> { + updateFilter(getFilterValues()); + dismiss(); + }); + dialogBinding.resetFiltermenu.setOnClickListener(view -> { + updateFilter(Collections.emptySet()); + for (int i = 0; i < rows.getChildCount(); i++) { + if (rows.getChildAt(i) instanceof MaterialButtonToggleGroup) { + ((MaterialButtonToggleGroup) rows.getChildAt(i)).clearChecked(); + } + } + }); + return dialogBinding.getRoot(); + } + + private Set getFilterValues() { + Set filterValues = new HashSet<>(); + for (int i = 0; i < rows.getChildCount(); i++) { + if (!(rows.getChildAt(i) instanceof MaterialButtonToggleGroup)) { + continue; + } + MaterialButtonToggleGroup group = (MaterialButtonToggleGroup) rows.getChildAt(i); + if (group.getCheckedButtonId() == View.NO_ID) { + continue; + } + String tag = (String) group.findViewById(group.getCheckedButtonId()).getTag(); + if (tag == null) { // Clear buttons use no tag + continue; + } + filterValues.add(tag); + } + return filterValues; + } + + @NonNull + @Override + public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { + Dialog dialog = super.onCreateDialog(savedInstanceState); + dialog.setOnShowListener(dialogInterface -> { + BottomSheetDialog bottomSheetDialog = (BottomSheetDialog) dialogInterface; + setupFullHeight(bottomSheetDialog); + }); + return dialog; + } + + private void setupFullHeight(BottomSheetDialog bottomSheetDialog) { + FrameLayout bottomSheet = bottomSheetDialog.findViewById(R.id.design_bottom_sheet); + if (bottomSheet != null) { + BottomSheetBehavior behavior = BottomSheetBehavior.from(bottomSheet); + ViewGroup.LayoutParams layoutParams = bottomSheet.getLayoutParams(); + bottomSheet.setLayoutParams(layoutParams); + behavior.setState(BottomSheetBehavior.STATE_EXPANDED); + } + } + + private static void updateFilter(Set filterValues) { + SubscriptionsFilter subscriptionsFilter = new SubscriptionsFilter(filterValues.toArray(new String[0])); + UserPreferences.setSubscriptionsFilter(subscriptionsFilter); + EventBus.getDefault().post(new UnreadItemsUpdateEvent()); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/screen/subscriptions/SubscriptionsFilterGroup.java b/app/src/main/java/de/danoeh/antennapod/ui/screen/subscriptions/SubscriptionsFilterGroup.java new file mode 100644 index 000000000..41dce16f0 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/screen/subscriptions/SubscriptionsFilterGroup.java @@ -0,0 +1,33 @@ +package de.danoeh.antennapod.ui.screen.subscriptions; + +import de.danoeh.antennapod.core.R; + +public enum SubscriptionsFilterGroup { + COUNTER_GREATER_ZERO(new ItemProperties(R.string.subscriptions_counter_greater_zero, "counter_greater_zero")), + AUTO_DOWNLOAD(new ItemProperties(R.string.auto_downloaded, "enabled_auto_download"), + new ItemProperties(R.string.not_auto_downloaded, "disabled_auto_download")), + UPDATED(new ItemProperties(R.string.kept_updated, "enabled_updates"), + new ItemProperties(R.string.not_kept_updated, "disabled_updates")), + NEW_EPISODE_NOTIFICATION(new ItemProperties(R.string.new_episode_notification_enabled, + "episode_notification_enabled"), + new ItemProperties(R.string.new_episode_notification_disabled, "episode_notification_disabled")); + + + public final ItemProperties[] values; + + SubscriptionsFilterGroup(ItemProperties... values) { + this.values = values; + } + + public static class ItemProperties { + + public final int displayName; + public final String filterId; + + public ItemProperties(int displayName, String filterId) { + this.displayName = displayName; + this.filterId = filterId; + } + + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/screen/subscriptions/SubscriptionsRecyclerAdapter.java b/app/src/main/java/de/danoeh/antennapod/ui/screen/subscriptions/SubscriptionsRecyclerAdapter.java new file mode 100644 index 000000000..fecb191b3 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/screen/subscriptions/SubscriptionsRecyclerAdapter.java @@ -0,0 +1,309 @@ +package de.danoeh.antennapod.ui.screen.subscriptions; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.view.ContextMenu; +import android.view.InputDevice; +import android.view.LayoutInflater; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CheckBox; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.appcompat.content.res.AppCompatResources; +import androidx.cardview.widget.CardView; +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.RecyclerView; + +import com.google.android.material.elevation.SurfaceColors; +import java.lang.ref.WeakReference; +import java.text.NumberFormat; +import java.util.ArrayList; +import java.util.List; + +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.ui.CoverLoader; +import de.danoeh.antennapod.ui.SelectableAdapter; +import de.danoeh.antennapod.storage.preferences.UserPreferences; +import de.danoeh.antennapod.storage.database.NavDrawerData; +import de.danoeh.antennapod.ui.screen.feed.FeedItemlistFragment; +import de.danoeh.antennapod.model.feed.Feed; + +/** + * Adapter for subscriptions + */ +public class SubscriptionsRecyclerAdapter extends SelectableAdapter + implements View.OnCreateContextMenuListener { + private static final int COVER_WITH_TITLE = 1; + + private final WeakReference mainActivityRef; + private List listItems; + private NavDrawerData.DrawerItem selectedItem = null; + int longPressedPosition = 0; // used to init actionMode + private int columnCount = 3; + + public SubscriptionsRecyclerAdapter(MainActivity mainActivity) { + super(mainActivity); + this.mainActivityRef = new WeakReference<>(mainActivity); + this.listItems = new ArrayList<>(); + setHasStableIds(true); + } + + public void setColumnCount(int columnCount) { + this.columnCount = columnCount; + } + + public Object getItem(int position) { + return listItems.get(position); + } + + public NavDrawerData.DrawerItem getSelectedItem() { + return selectedItem; + } + + @NonNull + @Override + public SubscriptionViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View itemView = LayoutInflater.from(mainActivityRef.get()).inflate(R.layout.subscription_item, parent, false); + itemView.findViewById(R.id.titleLabel).setVisibility(viewType == COVER_WITH_TITLE ? View.VISIBLE : View.GONE); + return new SubscriptionViewHolder(itemView); + } + + @Override + public void onBindViewHolder(@NonNull SubscriptionViewHolder holder, int position) { + NavDrawerData.DrawerItem drawerItem = listItems.get(position); + boolean isFeed = drawerItem.type == NavDrawerData.DrawerItem.Type.FEED; + holder.bind(drawerItem); + holder.itemView.setOnCreateContextMenuListener(this); + if (inActionMode()) { + if (isFeed) { + holder.selectCheckbox.setVisibility(View.VISIBLE); + holder.selectView.setVisibility(View.VISIBLE); + } + holder.selectCheckbox.setChecked((isSelected(position))); + holder.selectCheckbox.setOnCheckedChangeListener((buttonView, isChecked) + -> setSelected(holder.getBindingAdapterPosition(), isChecked)); + holder.coverImage.setAlpha(0.6f); + holder.count.setVisibility(View.GONE); + } else { + holder.selectView.setVisibility(View.GONE); + holder.coverImage.setAlpha(1.0f); + } + + holder.itemView.setOnLongClickListener(v -> { + if (!inActionMode()) { + if (isFeed) { + longPressedPosition = holder.getBindingAdapterPosition(); + } + selectedItem = drawerItem; + } + return false; + }); + + holder.itemView.setOnTouchListener((v, e) -> { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (e.isFromSource(InputDevice.SOURCE_MOUSE) + && e.getButtonState() == MotionEvent.BUTTON_SECONDARY) { + if (!inActionMode()) { + if (isFeed) { + longPressedPosition = holder.getBindingAdapterPosition(); + } + selectedItem = drawerItem; + } + } + } + return false; + }); + holder.itemView.setOnClickListener(v -> { + if (isFeed) { + if (inActionMode()) { + holder.selectCheckbox.setChecked(!isSelected(holder.getBindingAdapterPosition())); + } else { + Fragment fragment = FeedItemlistFragment + .newInstance(((NavDrawerData.FeedDrawerItem) drawerItem).feed.getId()); + mainActivityRef.get().loadChildFragment(fragment); + } + } else if (!inActionMode()) { + Fragment fragment = SubscriptionFragment.newInstance(drawerItem.getTitle()); + mainActivityRef.get().loadChildFragment(fragment); + } + }); + + } + + @Override + public int getItemCount() { + return listItems.size(); + } + + @Override + public long getItemId(int position) { + if (position >= listItems.size()) { + return RecyclerView.NO_ID; // Dummy views + } + return listItems.get(position).id; + } + + @Override + public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { + if (inActionMode() || selectedItem == null) { + return; + } + MenuInflater inflater = mainActivityRef.get().getMenuInflater(); + if (selectedItem.type == NavDrawerData.DrawerItem.Type.FEED) { + inflater.inflate(R.menu.nav_feed_context, menu); + menu.findItem(R.id.multi_select).setVisible(true); + } else { + inflater.inflate(R.menu.nav_folder_context, menu); + } + menu.setHeaderTitle(selectedItem.getTitle()); + } + + public boolean onContextItemSelected(MenuItem item) { + if (item.getItemId() == R.id.multi_select) { + startSelectMode(longPressedPosition); + return true; + } + return false; + } + + public List getSelectedItems() { + List items = new ArrayList<>(); + for (int i = 0; i < getItemCount(); i++) { + if (isSelected(i)) { + NavDrawerData.DrawerItem drawerItem = listItems.get(i); + if (drawerItem.type == NavDrawerData.DrawerItem.Type.FEED) { + Feed feed = ((NavDrawerData.FeedDrawerItem) drawerItem).feed; + items.add(feed); + } + } + } + return items; + } + + public void setItems(List listItems) { + this.listItems = listItems; + notifyDataSetChanged(); + } + + @Override + public void setSelected(int pos, boolean selected) { + NavDrawerData.DrawerItem drawerItem = listItems.get(pos); + if (drawerItem.type == NavDrawerData.DrawerItem.Type.FEED) { + super.setSelected(pos, selected); + } + } + + @Override + public int getItemViewType(int position) { + return UserPreferences.shouldShowSubscriptionTitle() ? COVER_WITH_TITLE : 0; + } + + public class SubscriptionViewHolder extends RecyclerView.ViewHolder { + private final TextView title; + private final ImageView coverImage; + private final TextView count; + private final TextView fallbackTitle; + private final FrameLayout selectView; + private final CheckBox selectCheckbox; + private final CardView card; + private final View errorIcon; + + public SubscriptionViewHolder(@NonNull View itemView) { + super(itemView); + title = itemView.findViewById(R.id.titleLabel); + coverImage = itemView.findViewById(R.id.coverImage); + count = itemView.findViewById(R.id.countViewPill); + fallbackTitle = itemView.findViewById(R.id.fallbackTitleLabel); + selectView = itemView.findViewById(R.id.selectContainer); + selectCheckbox = itemView.findViewById(R.id.selectCheckBox); + card = itemView.findViewById(R.id.outerContainer); + errorIcon = itemView.findViewById(R.id.errorIcon); + } + + public void bind(NavDrawerData.DrawerItem drawerItem) { + Drawable drawable = AppCompatResources.getDrawable(selectView.getContext(), + R.drawable.ic_checkbox_background); + selectView.setBackground(drawable); // Setting this in XML crashes API <= 21 + title.setText(drawerItem.getTitle()); + fallbackTitle.setText(drawerItem.getTitle()); + coverImage.setContentDescription(drawerItem.getTitle()); + if (drawerItem.getCounter() > 0) { + count.setText(NumberFormat.getInstance().format(drawerItem.getCounter())); + count.setVisibility(View.VISIBLE); + } else { + count.setVisibility(View.GONE); + } + + CoverLoader coverLoader = new CoverLoader(); + boolean textAndImageCombined; + if (drawerItem.type == NavDrawerData.DrawerItem.Type.FEED) { + Feed feed = ((NavDrawerData.FeedDrawerItem) drawerItem).feed; + textAndImageCombined = feed.isLocalFeed() && feed.getImageUrl() != null + && feed.getImageUrl().startsWith(Feed.PREFIX_GENERATIVE_COVER); + coverLoader.withUri(feed.getImageUrl()); + errorIcon.setVisibility(feed.hasLastUpdateFailed() ? View.VISIBLE : View.GONE); + } else { + textAndImageCombined = true; + coverLoader.withResource(R.drawable.ic_tag); + errorIcon.setVisibility(View.GONE); + } + if (UserPreferences.shouldShowSubscriptionTitle()) { + // No need for fallback title when already showing title + fallbackTitle.setVisibility(View.GONE); + } else { + coverLoader.withPlaceholderView(fallbackTitle, textAndImageCombined); + } + coverLoader.withCoverView(coverImage); + coverLoader.load(); + + float density = mainActivityRef.get().getResources().getDisplayMetrics().density; + card.setCardBackgroundColor(SurfaceColors.getColorForElevation(mainActivityRef.get(), 1 * density)); + + int textPadding = columnCount <= 3 ? 16 : 8; + title.setPadding(textPadding, textPadding, textPadding, textPadding); + fallbackTitle.setPadding(textPadding, textPadding, textPadding, textPadding); + + int textSize = 14; + if (columnCount == 3) { + textSize = 15; + } else if (columnCount == 2) { + textSize = 16; + } + title.setTextSize(textSize); + fallbackTitle.setTextSize(textSize); + } + } + + public static float convertDpToPixel(Context context, float dp) { + return dp * context.getResources().getDisplayMetrics().density; + } + + public static class GridDividerItemDecorator extends RecyclerView.ItemDecoration { + @Override + public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { + super.onDraw(c, parent, state); + } + + @Override + public void getItemOffsets(@NonNull Rect outRect, + @NonNull View view, + @NonNull RecyclerView parent, + @NonNull RecyclerView.State state) { + super.getItemOffsets(outRect, view, parent, state); + Context context = parent.getContext(); + int insetOffset = (int) convertDpToPixel(context, 1f); + outRect.set(insetOffset, insetOffset, insetOffset, insetOffset); + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/share/ShareDialog.java b/app/src/main/java/de/danoeh/antennapod/ui/share/ShareDialog.java new file mode 100644 index 000000000..963c0b14b --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/share/ShareDialog.java @@ -0,0 +1,99 @@ +package de.danoeh.antennapod.ui.share; + +import android.content.Context; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.android.material.bottomsheet.BottomSheetDialogFragment; +import de.danoeh.antennapod.databinding.ShareEpisodeDialogBinding; +import de.danoeh.antennapod.model.feed.FeedItem; + +public class ShareDialog extends BottomSheetDialogFragment { + private static final String ARGUMENT_FEED_ITEM = "feedItem"; + private static final String PREF_NAME = "ShareDialog"; + private static final String PREF_SHARE_EPISODE_START_AT = "prefShareEpisodeStartAt"; + private static final String PREF_SHARE_EPISODE_TYPE = "prefShareEpisodeType"; + + private Context ctx; + private FeedItem item; + private SharedPreferences prefs; + + private ShareEpisodeDialogBinding viewBinding; + + public ShareDialog() { + // Empty constructor required for DialogFragment + } + + public static ShareDialog newInstance(FeedItem item) { + Bundle arguments = new Bundle(); + arguments.putSerializable(ARGUMENT_FEED_ITEM, item); + ShareDialog dialog = new ShareDialog(); + dialog.setArguments(arguments); + return dialog; + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + if (getArguments() != null) { + ctx = getActivity(); + item = (FeedItem) getArguments().getSerializable(ARGUMENT_FEED_ITEM); + prefs = getActivity().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); + } + + viewBinding = ShareEpisodeDialogBinding.inflate(inflater); + viewBinding.shareDialogRadioGroup.setOnCheckedChangeListener((group, checkedId) -> + viewBinding.sharePositionCheckbox.setEnabled(checkedId == viewBinding.shareSocialRadio.getId())); + + setupOptions(); + + viewBinding.shareButton.setOnClickListener((v) -> { + boolean includePlaybackPosition = viewBinding.sharePositionCheckbox.isChecked(); + int position; + if (viewBinding.shareSocialRadio.isChecked()) { + ShareUtils.shareFeedItemLinkWithDownloadLink(ctx, item, includePlaybackPosition); + position = 1; + } else if (viewBinding.shareMediaReceiverRadio.isChecked()) { + ShareUtils.shareMediaDownloadLink(ctx, item.getMedia()); + position = 2; + } else if (viewBinding.shareMediaFileRadio.isChecked()) { + ShareUtils.shareFeedItemFile(ctx, item.getMedia()); + position = 3; + } else { + throw new IllegalStateException("Unknown share method"); + } + prefs.edit() + .putBoolean(PREF_SHARE_EPISODE_START_AT, includePlaybackPosition) + .putInt(PREF_SHARE_EPISODE_TYPE, position) + .apply(); + dismiss(); + }); + return viewBinding.getRoot(); + } + + private void setupOptions() { + final boolean hasMedia = item.getMedia() != null; + boolean downloaded = hasMedia && item.getMedia().isDownloaded(); + viewBinding.shareMediaFileRadio.setVisibility(downloaded ? View.VISIBLE : View.GONE); + + boolean hasDownloadUrl = hasMedia && item.getMedia().getDownloadUrl() != null; + if (!hasDownloadUrl) { + viewBinding.shareMediaReceiverRadio.setVisibility(View.GONE); + } + int type = prefs.getInt(PREF_SHARE_EPISODE_TYPE, 1); + if ((type == 2 && !hasDownloadUrl) || (type == 3 && !downloaded)) { + type = 1; + } + viewBinding.shareSocialRadio.setChecked(type == 1); + viewBinding.shareMediaReceiverRadio.setChecked(type == 2); + viewBinding.shareMediaFileRadio.setChecked(type == 3); + + boolean switchIsChecked = prefs.getBoolean(PREF_SHARE_EPISODE_START_AT, false); + viewBinding.sharePositionCheckbox.setChecked(switchIsChecked); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/share/ShareUtils.java b/app/src/main/java/de/danoeh/antennapod/ui/share/ShareUtils.java new file mode 100644 index 000000000..77d5f4305 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/share/ShareUtils.java @@ -0,0 +1,92 @@ +package de.danoeh.antennapod.ui.share; + +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.core.app.ShareCompat; +import androidx.core.content.FileProvider; + +import de.danoeh.antennapod.core.util.FeedItemUtil; +import de.danoeh.antennapod.ui.common.Converter; +import java.io.File; +import java.net.URLEncoder; + +import de.danoeh.antennapod.core.R; +import de.danoeh.antennapod.model.feed.Feed; +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedMedia; + +/** Utility methods for sharing data */ +public class ShareUtils { + private static final String TAG = "ShareUtils"; + + private ShareUtils() { + } + + public static void shareLink(@NonNull Context context, @NonNull String text) { + Intent intent = new ShareCompat.IntentBuilder(context) + .setType("text/plain") + .setText(text) + .setChooserTitle(R.string.share_url_label) + .createChooserIntent(); + context.startActivity(intent); + } + + public static void shareFeedLink(Context context, Feed feed) { + String text = feed.getTitle() + + "\n\n" + + "https://antennapod.org/deeplink/subscribe/?url=" + + URLEncoder.encode(feed.getDownloadUrl()) + + "&title=" + + URLEncoder.encode(feed.getTitle()); + shareLink(context, text); + } + + public static boolean hasLinkToShare(FeedItem item) { + return FeedItemUtil.getLinkWithFallback(item) != null; + } + + public static void shareMediaDownloadLink(Context context, FeedMedia media) { + shareLink(context, media.getDownloadUrl()); + } + + public static void shareFeedItemLinkWithDownloadLink(Context context, FeedItem item, boolean withPosition) { + String text = item.getFeed().getTitle() + ": " + item.getTitle(); + int pos = 0; + if (item.getMedia() != null && withPosition) { + text += "\n" + context.getResources().getString(R.string.share_starting_position_label) + ": "; + pos = item.getMedia().getPosition(); + text += Converter.getDurationStringLong(pos); + } + + if (hasLinkToShare(item)) { + text += "\n\n" + context.getResources().getString(R.string.share_dialog_episode_website_label) + ": "; + text += FeedItemUtil.getLinkWithFallback(item); + } + + if (item.getMedia() != null && item.getMedia().getDownloadUrl() != null) { + text += "\n\n" + context.getResources().getString(R.string.share_dialog_media_file_label) + ": "; + text += item.getMedia().getDownloadUrl(); + if (withPosition) { + text += "#t=" + pos / 1000; + } + } + shareLink(context, text); + } + + public static void shareFeedItemFile(Context context, FeedMedia media) { + Uri fileUri = FileProvider.getUriForFile(context, context.getString(R.string.provider_authority), + new File(media.getLocalFileUrl())); + + new ShareCompat.IntentBuilder(context) + .setType(media.getMimeType()) + .addStream(fileUri) + .setChooserTitle(R.string.share_file_label) + .startChooser(); + + Log.e(TAG, "shareFeedItemFile called"); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/swipeactions/AddToQueueSwipeAction.java b/app/src/main/java/de/danoeh/antennapod/ui/swipeactions/AddToQueueSwipeAction.java new file mode 100644 index 000000000..0870adff9 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/swipeactions/AddToQueueSwipeAction.java @@ -0,0 +1,47 @@ +package de.danoeh.antennapod.ui.swipeactions; + +import android.content.Context; + +import androidx.fragment.app.Fragment; + +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.storage.database.DBWriter; +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedItemFilter; + +public class AddToQueueSwipeAction implements SwipeAction { + + @Override + public String getId() { + return ADD_TO_QUEUE; + } + + @Override + public int getActionIcon() { + return R.drawable.ic_playlist_play; + } + + @Override + public int getActionColor() { + return R.attr.colorAccent; + } + + @Override + public String getTitle(Context context) { + return context.getString(R.string.add_to_queue_label); + } + + @Override + public void performAction(FeedItem item, Fragment fragment, FeedItemFilter filter) { + if (!item.isTagged(FeedItem.TAG_QUEUE)) { + DBWriter.addQueueItem(fragment.requireContext(), item); + } else { + new RemoveFromQueueSwipeAction().performAction(item, fragment, filter); + } + } + + @Override + public boolean willRemove(FeedItemFilter filter, FeedItem item) { + return filter.showQueued || filter.showNew; + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/swipeactions/DeleteSwipeAction.java b/app/src/main/java/de/danoeh/antennapod/ui/swipeactions/DeleteSwipeAction.java new file mode 100644 index 000000000..1ecfd11e0 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/swipeactions/DeleteSwipeAction.java @@ -0,0 +1,50 @@ +package de.danoeh.antennapod.ui.swipeactions; + +import android.content.Context; +import androidx.fragment.app.Fragment; + +import java.util.Collections; + +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.storage.database.DBWriter; +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedItemFilter; +import de.danoeh.antennapod.ui.view.LocalDeleteModal; + +public class DeleteSwipeAction implements SwipeAction { + + @Override + public String getId() { + return DELETE; + } + + @Override + public int getActionIcon() { + return R.drawable.ic_delete; + } + + @Override + public int getActionColor() { + return R.attr.icon_red; + } + + @Override + public String getTitle(Context context) { + return context.getString(R.string.delete_episode_label); + } + + @Override + public void performAction(FeedItem item, Fragment fragment, FeedItemFilter filter) { + if (!item.isDownloaded() && !item.getFeed().isLocalFeed()) { + return; + } + LocalDeleteModal.showLocalFeedDeleteWarningIfNecessary( + fragment.requireContext(), Collections.singletonList(item), + () -> DBWriter.deleteFeedMediaOfItem(fragment.requireContext(), item.getMedia())); + } + + @Override + public boolean willRemove(FeedItemFilter filter, FeedItem item) { + return filter.showDownloaded && (item.isDownloaded() || item.getFeed().isLocalFeed()); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/swipeactions/MarkFavoriteSwipeAction.java b/app/src/main/java/de/danoeh/antennapod/ui/swipeactions/MarkFavoriteSwipeAction.java new file mode 100644 index 000000000..8ce0f8482 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/swipeactions/MarkFavoriteSwipeAction.java @@ -0,0 +1,43 @@ +package de.danoeh.antennapod.ui.swipeactions; + +import android.content.Context; + +import androidx.fragment.app.Fragment; + +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.storage.database.DBWriter; +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedItemFilter; + +public class MarkFavoriteSwipeAction implements SwipeAction { + + @Override + public String getId() { + return MARK_FAV; + } + + @Override + public int getActionIcon() { + return R.drawable.ic_star; + } + + @Override + public int getActionColor() { + return R.attr.icon_yellow; + } + + @Override + public String getTitle(Context context) { + return context.getString(R.string.add_to_favorite_label); + } + + @Override + public void performAction(FeedItem item, Fragment fragment, FeedItemFilter filter) { + DBWriter.toggleFavoriteItem(item); + } + + @Override + public boolean willRemove(FeedItemFilter filter, FeedItem item) { + return filter.showIsFavorite || filter.showNotFavorite; + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/swipeactions/RemoveFromHistorySwipeAction.java b/app/src/main/java/de/danoeh/antennapod/ui/swipeactions/RemoveFromHistorySwipeAction.java new file mode 100644 index 000000000..e757ece42 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/swipeactions/RemoveFromHistorySwipeAction.java @@ -0,0 +1,58 @@ +package de.danoeh.antennapod.ui.swipeactions; + +import android.content.Context; + +import androidx.fragment.app.Fragment; + +import com.google.android.material.snackbar.Snackbar; + +import java.util.Date; + +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.storage.database.DBWriter; +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedItemFilter; + +public class RemoveFromHistorySwipeAction implements SwipeAction { + + public static final String TAG = "RemoveFromHistorySwipeAction"; + + @Override + public String getId() { + return REMOVE_FROM_HISTORY; + } + + @Override + public int getActionIcon() { + return R.drawable.ic_history_remove; + } + + @Override + public int getActionColor() { + return R.attr.icon_purple; + } + + @Override + public String getTitle(Context context) { + return context.getString(R.string.remove_history_label); + } + + @Override + public void performAction(FeedItem item, Fragment fragment, FeedItemFilter filter) { + + Date playbackCompletionDate = item.getMedia().getPlaybackCompletionDate(); + + DBWriter.deleteFromPlaybackHistory(item); + + ((MainActivity) fragment.requireActivity()) + .showSnackbarAbovePlayer(R.string.removed_history_label, Snackbar.LENGTH_LONG) + .setAction(fragment.getString(R.string.undo), + v -> DBWriter.addItemToPlaybackHistory(item.getMedia(), playbackCompletionDate)); + } + + @Override + public boolean willRemove(FeedItemFilter filter, FeedItem item) { + return true; + } +} \ No newline at end of file diff --git a/app/src/main/java/de/danoeh/antennapod/ui/swipeactions/RemoveFromInboxSwipeAction.java b/app/src/main/java/de/danoeh/antennapod/ui/swipeactions/RemoveFromInboxSwipeAction.java new file mode 100644 index 000000000..c7bebecab --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/swipeactions/RemoveFromInboxSwipeAction.java @@ -0,0 +1,45 @@ +package de.danoeh.antennapod.ui.swipeactions; + +import android.content.Context; + +import androidx.fragment.app.Fragment; + +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.ui.episodeslist.FeedItemMenuHandler; +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedItemFilter; + +public class RemoveFromInboxSwipeAction implements SwipeAction { + + @Override + public String getId() { + return REMOVE_FROM_INBOX; + } + + @Override + public int getActionIcon() { + return R.drawable.ic_check; + } + + @Override + public int getActionColor() { + return R.attr.icon_purple; + } + + @Override + public String getTitle(Context context) { + return context.getString(R.string.remove_inbox_label); + } + + @Override + public void performAction(FeedItem item, Fragment fragment, FeedItemFilter filter) { + if (item.isNew()) { + FeedItemMenuHandler.markReadWithUndo(fragment, item, FeedItem.UNPLAYED, willRemove(filter, item)); + } + } + + @Override + public boolean willRemove(FeedItemFilter filter, FeedItem item) { + return filter.showNew; + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/swipeactions/RemoveFromQueueSwipeAction.java b/app/src/main/java/de/danoeh/antennapod/ui/swipeactions/RemoveFromQueueSwipeAction.java new file mode 100644 index 000000000..dbdc3126b --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/swipeactions/RemoveFromQueueSwipeAction.java @@ -0,0 +1,57 @@ +package de.danoeh.antennapod.ui.swipeactions; + +import android.content.Context; + +import androidx.fragment.app.Fragment; + +import com.google.android.material.snackbar.Snackbar; + +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.storage.database.DBReader; +import de.danoeh.antennapod.storage.database.DBWriter; +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedItemFilter; + +public class RemoveFromQueueSwipeAction implements SwipeAction { + + @Override + public String getId() { + return REMOVE_FROM_QUEUE; + } + + @Override + public int getActionIcon() { + return R.drawable.ic_playlist_remove; + } + + @Override + public int getActionColor() { + return R.attr.colorAccent; + } + + @Override + public String getTitle(Context context) { + return context.getString(R.string.remove_from_queue_label); + } + + @Override + public void performAction(FeedItem item, Fragment fragment, FeedItemFilter filter) { + int position = DBReader.getQueueIDList().indexOf(item.getId()); + + DBWriter.removeQueueItem(fragment.requireActivity(), true, item); + + if (willRemove(filter, item)) { + ((MainActivity) fragment.requireActivity()).showSnackbarAbovePlayer( + fragment.getResources().getQuantityString(R.plurals.removed_from_queue_batch_label, 1, 1), + Snackbar.LENGTH_LONG) + .setAction(fragment.getString(R.string.undo), v -> + DBWriter.addQueueItemAt(fragment.requireActivity(), item.getId(), position, false)); + } + } + + @Override + public boolean willRemove(FeedItemFilter filter, FeedItem item) { + return filter.showQueued || filter.showNotQueued; + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/swipeactions/ShowFirstSwipeDialogAction.java b/app/src/main/java/de/danoeh/antennapod/ui/swipeactions/ShowFirstSwipeDialogAction.java new file mode 100644 index 000000000..3c0afee81 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/swipeactions/ShowFirstSwipeDialogAction.java @@ -0,0 +1,42 @@ +package de.danoeh.antennapod.ui.swipeactions; + +import android.content.Context; + +import androidx.fragment.app.Fragment; + +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedItemFilter; + +public class ShowFirstSwipeDialogAction implements SwipeAction { + + @Override + public String getId() { + return "SHOW_FIRST_SWIPE_DIALOG"; + } + + @Override + public int getActionIcon() { + return R.drawable.ic_settings; + } + + @Override + public int getActionColor() { + return R.attr.icon_gray; + } + + @Override + public String getTitle(Context context) { + return ""; + } + + @Override + public void performAction(FeedItem item, Fragment fragment, FeedItemFilter filter) { + //handled in SwipeActions + } + + @Override + public boolean willRemove(FeedItemFilter filter, FeedItem item) { + return false; + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/swipeactions/StartDownloadSwipeAction.java b/app/src/main/java/de/danoeh/antennapod/ui/swipeactions/StartDownloadSwipeAction.java new file mode 100644 index 000000000..0df3bcaec --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/swipeactions/StartDownloadSwipeAction.java @@ -0,0 +1,44 @@ +package de.danoeh.antennapod.ui.swipeactions; + +import android.content.Context; +import androidx.fragment.app.Fragment; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.actionbutton.DownloadActionButton; +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedItemFilter; + +public class StartDownloadSwipeAction implements SwipeAction { + + @Override + public String getId() { + return START_DOWNLOAD; + } + + @Override + public int getActionIcon() { + return R.drawable.ic_download; + } + + @Override + public int getActionColor() { + return R.attr.icon_green; + } + + @Override + public String getTitle(Context context) { + return context.getString(R.string.download_label); + } + + @Override + public void performAction(FeedItem item, Fragment fragment, FeedItemFilter filter) { + if (!item.isDownloaded() && !item.getFeed().isLocalFeed()) { + new DownloadActionButton(item) + .onClick(fragment.requireContext()); + } + } + + @Override + public boolean willRemove(FeedItemFilter filter, FeedItem item) { + return false; + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/swipeactions/SwipeAction.java b/app/src/main/java/de/danoeh/antennapod/ui/swipeactions/SwipeAction.java new file mode 100644 index 000000000..007bdeff3 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/swipeactions/SwipeAction.java @@ -0,0 +1,36 @@ +package de.danoeh.antennapod.ui.swipeactions; + +import android.content.Context; + +import androidx.annotation.AttrRes; +import androidx.annotation.DrawableRes; +import androidx.fragment.app.Fragment; + +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedItemFilter; + +public interface SwipeAction { + + String ADD_TO_QUEUE = "ADD_TO_QUEUE"; + String REMOVE_FROM_INBOX = "REMOVE_FROM_INBOX"; + String START_DOWNLOAD = "START_DOWNLOAD"; + String MARK_FAV = "MARK_FAV"; + String TOGGLE_PLAYED = "MARK_PLAYED"; + String REMOVE_FROM_QUEUE = "REMOVE_FROM_QUEUE"; + String DELETE = "DELETE"; + String REMOVE_FROM_HISTORY = "REMOVE_FROM_HISTORY"; + + String getId(); + + String getTitle(Context context); + + @DrawableRes + int getActionIcon(); + + @AttrRes + int getActionColor(); + + void performAction(FeedItem item, Fragment fragment, FeedItemFilter filter); + + boolean willRemove(FeedItemFilter filter, FeedItem item); +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/swipeactions/SwipeActions.java b/app/src/main/java/de/danoeh/antennapod/ui/swipeactions/SwipeActions.java new file mode 100644 index 000000000..fdc22094e --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/swipeactions/SwipeActions.java @@ -0,0 +1,267 @@ +package de.danoeh.antennapod.ui.swipeactions; + +import android.content.Context; +import android.content.SharedPreferences; +import android.graphics.Canvas; + +import androidx.annotation.NonNull; +import androidx.core.graphics.ColorUtils; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.Lifecycle; +import androidx.lifecycle.LifecycleObserver; +import androidx.lifecycle.OnLifecycleEvent; +import androidx.recyclerview.widget.ItemTouchHelper; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.ui.screen.AllEpisodesFragment; +import de.danoeh.antennapod.ui.screen.download.CompletedDownloadsFragment; +import de.danoeh.antennapod.ui.screen.InboxFragment; +import de.danoeh.antennapod.ui.screen.PlaybackHistoryFragment; +import de.danoeh.antennapod.ui.screen.queue.QueueFragment; +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedItemFilter; +import de.danoeh.antennapod.ui.common.ThemeUtils; +import de.danoeh.antennapod.ui.episodeslist.EpisodeItemViewHolder; +import it.xabaras.android.recyclerview.swipedecorator.RecyclerViewSwipeDecorator; + +public class SwipeActions extends ItemTouchHelper.SimpleCallback implements LifecycleObserver { + public static final String PREF_NAME = "SwipeActionsPrefs"; + public static final String KEY_PREFIX_SWIPEACTIONS = "PrefSwipeActions"; + public static final String KEY_PREFIX_NO_ACTION = "PrefNoSwipeAction"; + + private static final List swipeActions = Collections.unmodifiableList( + Arrays.asList(new AddToQueueSwipeAction(), new RemoveFromInboxSwipeAction(), + new StartDownloadSwipeAction(), new MarkFavoriteSwipeAction(), + new TogglePlaybackStateSwipeAction(), new RemoveFromQueueSwipeAction(), + new DeleteSwipeAction(), new RemoveFromHistorySwipeAction())); + + private final Fragment fragment; + private final String tag; + private FeedItemFilter filter = null; + + Actions actions; + boolean swipeOutEnabled = true; + int swipedOutTo = 0; + private final ItemTouchHelper itemTouchHelper = new ItemTouchHelper(this); + + public SwipeActions(int dragDirs, Fragment fragment, String tag) { + super(dragDirs, ItemTouchHelper.RIGHT | ItemTouchHelper.LEFT); + this.fragment = fragment; + this.tag = tag; + reloadPreference(); + fragment.getLifecycle().addObserver(this); + } + + public SwipeActions(Fragment fragment, String tag) { + this(0, fragment, tag); + } + + public static SwipeAction getAction(String key) { + for (SwipeAction action : swipeActions) { + if (action.getId().equals(key)) { + return action; + } + } + return null; + } + + @OnLifecycleEvent(Lifecycle.Event.ON_START) + public void reloadPreference() { + actions = getPrefs(fragment.requireContext(), tag); + } + + public void setFilter(FeedItemFilter filter) { + this.filter = filter; + } + + public SwipeActions attachTo(RecyclerView recyclerView) { + itemTouchHelper.attachToRecyclerView(recyclerView); + return this; + } + + public void detach() { + itemTouchHelper.attachToRecyclerView(null); + } + + private static Actions getPrefs(Context context, String tag, String defaultActions) { + SharedPreferences prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); + String prefsString = prefs.getString(KEY_PREFIX_SWIPEACTIONS + tag, defaultActions); + + return new Actions(prefsString); + } + + private static Actions getPrefs(Context context, String tag) { + return getPrefs(context, tag, ""); + } + + public static Actions getPrefsWithDefaults(Context context, String tag) { + String defaultActions; + switch (tag) { + case InboxFragment.TAG: + defaultActions = SwipeAction.ADD_TO_QUEUE + "," + SwipeAction.REMOVE_FROM_INBOX; + break; + case QueueFragment.TAG: + defaultActions = SwipeAction.REMOVE_FROM_QUEUE + "," + SwipeAction.REMOVE_FROM_QUEUE; + break; + case CompletedDownloadsFragment.TAG: + defaultActions = SwipeAction.DELETE + "," + SwipeAction.DELETE; + break; + case PlaybackHistoryFragment.TAG: + defaultActions = SwipeAction.REMOVE_FROM_HISTORY + "," + SwipeAction.REMOVE_FROM_HISTORY; + break; + default: + case AllEpisodesFragment.TAG: + defaultActions = SwipeAction.MARK_FAV + "," + SwipeAction.START_DOWNLOAD; + break; + } + + return getPrefs(context, tag, defaultActions); + } + + public static boolean isSwipeActionEnabled(Context context, String tag) { + SharedPreferences prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); + return prefs.getBoolean(KEY_PREFIX_NO_ACTION + tag, true); + } + + private boolean isSwipeActionEnabled() { + return isSwipeActionEnabled(fragment.requireContext(), tag); + } + + @Override + public boolean onMove(@NonNull RecyclerView recyclerView, + @NonNull RecyclerView.ViewHolder viewHolder, + @NonNull RecyclerView.ViewHolder target) { + return false; + } + + @Override + public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int swipeDir) { + if (!actions.hasActions()) { + //open settings dialog if no prefs are set + new SwipeActionsDialog(fragment.requireContext(), tag).show(this::reloadPreference); + return; + } + + FeedItem item = ((EpisodeItemViewHolder) viewHolder).getFeedItem(); + + (swipeDir == ItemTouchHelper.RIGHT ? actions.right : actions.left) + .performAction(item, fragment, filter); + } + + @Override + public void onChildDraw(@NonNull Canvas c, @NonNull RecyclerView recyclerView, + @NonNull RecyclerView.ViewHolder viewHolder, + float dx, float dy, int actionState, boolean isCurrentlyActive) { + SwipeAction right; + SwipeAction left; + if (actions.hasActions()) { + right = actions.right; + left = actions.left; + } else { + right = left = new ShowFirstSwipeDialogAction(); + } + + //check if it will be removed + FeedItem item = ((EpisodeItemViewHolder) viewHolder).getFeedItem(); + boolean rightWillRemove = right.willRemove(filter, item); + boolean leftWillRemove = left.willRemove(filter, item); + boolean wontLeave = (dx > 0 && !rightWillRemove) || (dx < 0 && !leftWillRemove); + + //Limit swipe if it's not removed + int maxMovement = recyclerView.getWidth() * 2 / 5; + float sign = dx > 0 ? 1 : -1; + float limitMovement = Math.min(maxMovement, sign * dx); + float displacementPercentage = limitMovement / maxMovement; + boolean swipeThresholdReached = displacementPercentage >= 0.85; + + if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE && wontLeave) { + swipeOutEnabled = false; + // Move slower when getting near the maxMovement + dx = sign * maxMovement * 0.7f * (float) Math.sin((Math.PI / 2) * displacementPercentage); + + if (isCurrentlyActive) { + int dir = dx > 0 ? ItemTouchHelper.RIGHT : ItemTouchHelper.LEFT; + swipedOutTo = swipeThresholdReached ? dir : 0; + } + } else { + swipeOutEnabled = true; + } + + //add color and icon + Context context = fragment.requireContext(); + int themeColor = ThemeUtils.getColorFromAttr(context, android.R.attr.colorBackground); + int actionColor = ThemeUtils.getColorFromAttr(context, + dx > 0 ? right.getActionColor() : left.getActionColor()); + RecyclerViewSwipeDecorator.Builder builder = new RecyclerViewSwipeDecorator.Builder( + c, recyclerView, viewHolder, dx, dy, actionState, isCurrentlyActive) + .addSwipeRightActionIcon(right.getActionIcon()) + .addSwipeLeftActionIcon(left.getActionIcon()) + .addSwipeRightBackgroundColor(ThemeUtils.getColorFromAttr(context, R.attr.background_elevated)) + .addSwipeLeftBackgroundColor(ThemeUtils.getColorFromAttr(context, R.attr.background_elevated)) + .setActionIconTint(ColorUtils.blendARGB(themeColor, actionColor, + (!wontLeave || swipeThresholdReached) ? 1.0f : 0.7f)); + builder.create().decorate(); + + super.onChildDraw(c, recyclerView, viewHolder, dx, dy, actionState, isCurrentlyActive); + } + + @Override + public float getSwipeEscapeVelocity(float defaultValue) { + return swipeOutEnabled ? defaultValue * 1.5f : Float.MAX_VALUE; + } + + @Override + public float getSwipeVelocityThreshold(float defaultValue) { + return swipeOutEnabled ? defaultValue * 0.6f : 0; + } + + @Override + public float getSwipeThreshold(@NonNull RecyclerView.ViewHolder viewHolder) { + return swipeOutEnabled ? 0.6f : 1.0f; + } + + @Override + public void clearView(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) { + super.clearView(recyclerView, viewHolder); + + if (swipedOutTo != 0) { + onSwiped(viewHolder, swipedOutTo); + swipedOutTo = 0; + } + } + + @Override + public int getMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) { + if (!isSwipeActionEnabled()) { + return makeMovementFlags(getDragDirs(recyclerView, viewHolder), 0); + } else { + return super.getMovementFlags(recyclerView, viewHolder); + } + } + + public void startDrag(EpisodeItemViewHolder holder) { + itemTouchHelper.startDrag(holder); + } + + public static class Actions { + public SwipeAction right = null; + public SwipeAction left = null; + + public Actions(String prefs) { + String[] actions = prefs.split(","); + if (actions.length == 2) { + right = getAction(actions[0]); + left = getAction(actions[1]); + } + } + + public boolean hasActions() { + return right != null && left != null; + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/swipeactions/SwipeActionsDialog.java b/app/src/main/java/de/danoeh/antennapod/ui/swipeactions/SwipeActionsDialog.java new file mode 100644 index 000000000..a02a685f0 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/swipeactions/SwipeActionsDialog.java @@ -0,0 +1,213 @@ +package de.danoeh.antennapod.ui.swipeactions; + +import android.content.Context; +import android.content.SharedPreferences; +import android.graphics.PorterDuff; +import android.graphics.drawable.Drawable; +import android.view.LayoutInflater; +import android.view.View; + +import androidx.appcompat.app.AlertDialog; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import androidx.appcompat.content.res.AppCompatResources; +import androidx.core.graphics.drawable.DrawableCompat; +import androidx.gridlayout.widget.GridLayout; + +import java.util.ArrayList; +import java.util.List; + +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.databinding.FeeditemlistItemBinding; +import de.danoeh.antennapod.databinding.SwipeactionsDialogBinding; +import de.danoeh.antennapod.databinding.SwipeactionsPickerBinding; +import de.danoeh.antennapod.databinding.SwipeactionsPickerItemBinding; +import de.danoeh.antennapod.databinding.SwipeactionsRowBinding; +import de.danoeh.antennapod.ui.screen.AllEpisodesFragment; +import de.danoeh.antennapod.ui.screen.download.CompletedDownloadsFragment; +import de.danoeh.antennapod.ui.screen.feed.FeedItemlistFragment; +import de.danoeh.antennapod.ui.screen.InboxFragment; +import de.danoeh.antennapod.ui.screen.PlaybackHistoryFragment; +import de.danoeh.antennapod.ui.screen.queue.QueueFragment; +import de.danoeh.antennapod.ui.common.ThemeUtils; + +public class SwipeActionsDialog { + private static final int LEFT = 1; + private static final int RIGHT = 0; + + private final Context context; + private final String tag; + + private SwipeAction rightAction; + private SwipeAction leftAction; + private List keys; + + public SwipeActionsDialog(Context context, String tag) { + this.context = context; + this.tag = tag; + } + + public void show(Callback prefsChanged) { + SwipeActions.Actions actions = SwipeActions.getPrefsWithDefaults(context, tag); + leftAction = actions.left; + rightAction = actions.right; + + final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(context); + + keys = new ArrayList<>(); + if (tag.equals(QueueFragment.TAG)) { + keys.add(new RemoveFromQueueSwipeAction()); + } else { + keys.add(new AddToQueueSwipeAction()); + } + if (!tag.equals(CompletedDownloadsFragment.TAG)) { + keys.add(new StartDownloadSwipeAction()); + } + if (!tag.equals(CompletedDownloadsFragment.TAG) + && ! tag.equals(QueueFragment.TAG) + && !tag.equals(PlaybackHistoryFragment.TAG)) { + keys.add(new RemoveFromInboxSwipeAction()); + } + if (!tag.equals(InboxFragment.TAG)) { + keys.add(new DeleteSwipeAction()); + } + keys.add(new MarkFavoriteSwipeAction()); + if (tag.equals(PlaybackHistoryFragment.TAG)) { + keys.add(new RemoveFromHistorySwipeAction()); + } + if (!tag.equals(InboxFragment.TAG)) { + keys.add(new TogglePlaybackStateSwipeAction()); + } + + String forFragment = ""; + switch (tag) { + case InboxFragment.TAG: + forFragment = context.getString(R.string.inbox_label); + break; + case AllEpisodesFragment.TAG: + forFragment = context.getString(R.string.episodes_label); + break; + case CompletedDownloadsFragment.TAG: + forFragment = context.getString(R.string.downloads_label); + break; + case FeedItemlistFragment.TAG: + forFragment = context.getString(R.string.individual_subscription); + break; + case QueueFragment.TAG: + forFragment = context.getString(R.string.queue_label); + break; + case PlaybackHistoryFragment.TAG: + forFragment = context.getString(R.string.playback_history_label); + break; + default: break; + } + + builder.setTitle(context.getString(R.string.swipeactions_label) + " - " + forFragment); + SwipeactionsDialogBinding viewBinding = SwipeactionsDialogBinding.inflate(LayoutInflater.from(context)); + builder.setView(viewBinding.getRoot()); + + viewBinding.enableSwitch.setOnCheckedChangeListener((compoundButton, b) -> { + viewBinding.actionLeftContainer.getRoot().setAlpha(b ? 1.0f : 0.4f); + viewBinding.actionRightContainer.getRoot().setAlpha(b ? 1.0f : 0.4f); + }); + + viewBinding.enableSwitch.setChecked(SwipeActions.isSwipeActionEnabled(context, tag)); + + setupSwipeDirectionView(viewBinding.actionLeftContainer, LEFT); + setupSwipeDirectionView(viewBinding.actionRightContainer, RIGHT); + + builder.setPositiveButton(R.string.confirm_label, (dialog, which) -> { + savePrefs(tag, rightAction.getId(), leftAction.getId()); + saveActionsEnabledPrefs(viewBinding.enableSwitch.isChecked()); + prefsChanged.onCall(); + }); + + builder.setNegativeButton(R.string.cancel_label, null); + builder.create().show(); + } + + private void setupSwipeDirectionView(SwipeactionsRowBinding view, int direction) { + SwipeAction action = direction == LEFT ? leftAction : rightAction; + + view.swipeDirectionLabel.setText(direction == LEFT ? R.string.swipe_left : R.string.swipe_right); + view.swipeActionLabel.setText(action.getTitle(context)); + populateMockEpisode(view.mockEpisode); + if (direction == RIGHT && view.previewContainer.getChildAt(0) != view.swipeIcon) { + view.previewContainer.removeView(view.swipeIcon); + view.previewContainer.addView(view.swipeIcon, 0); + } + + view.swipeIcon.setImageResource(action.getActionIcon()); + view.swipeIcon.setColorFilter(ThemeUtils.getColorFromAttr(context, action.getActionColor())); + + view.changeButton.setOnClickListener(v -> showPicker(view, direction)); + view.previewContainer.setOnClickListener(v -> showPicker(view, direction)); + } + + private void showPicker(SwipeactionsRowBinding view, int direction) { + MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(context); + builder.setTitle(direction == LEFT ? R.string.swipe_left : R.string.swipe_right); + + SwipeactionsPickerBinding picker = SwipeactionsPickerBinding.inflate(LayoutInflater.from(context)); + builder.setView(picker.getRoot()); + builder.setNegativeButton(R.string.cancel_label, null); + AlertDialog dialog = builder.show(); + + for (int i = 0; i < keys.size(); i++) { + final int actionIndex = i; + SwipeAction action = keys.get(actionIndex); + SwipeactionsPickerItemBinding item = SwipeactionsPickerItemBinding.inflate(LayoutInflater.from(context)); + item.swipeActionLabel.setText(action.getTitle(context)); + + Drawable icon = DrawableCompat.wrap(AppCompatResources.getDrawable(context, action.getActionIcon())); + icon.mutate(); + icon.setTintMode(PorterDuff.Mode.SRC_ATOP); + if ((direction == LEFT && leftAction == action) || (direction == RIGHT && rightAction == action)) { + icon.setTint(ThemeUtils.getColorFromAttr(context, action.getActionColor())); + item.swipeActionLabel.setTextColor(ThemeUtils.getColorFromAttr(context, action.getActionColor())); + } else { + icon.setTint(ThemeUtils.getColorFromAttr(context, R.attr.action_icon_color)); + } + item.swipeIcon.setImageDrawable(icon); + + item.getRoot().setOnClickListener(v -> { + if (direction == LEFT) { + leftAction = keys.get(actionIndex); + } else { + rightAction = keys.get(actionIndex); + } + setupSwipeDirectionView(view, direction); + dialog.dismiss(); + }); + GridLayout.LayoutParams param = new GridLayout.LayoutParams( + GridLayout.spec(GridLayout.UNDEFINED, GridLayout.BASELINE), + GridLayout.spec(GridLayout.UNDEFINED, GridLayout.FILL, 1f)); + param.width = 0; + picker.pickerGridLayout.addView(item.getRoot(), param); + } + picker.pickerGridLayout.setColumnCount(2); + picker.pickerGridLayout.setRowCount((keys.size() + 1) / 2); + } + + private void populateMockEpisode(FeeditemlistItemBinding view) { + view.container.setAlpha(0.3f); + view.secondaryActionButton.secondaryActionButton.setVisibility(View.GONE); + view.dragHandle.setVisibility(View.GONE); + view.statusInbox.setVisibility(View.GONE); + view.txtvTitle.setText("███████"); + view.txtvPosition.setText("█████"); + } + + private void savePrefs(String tag, String right, String left) { + SharedPreferences prefs = context.getSharedPreferences(SwipeActions.PREF_NAME, Context.MODE_PRIVATE); + prefs.edit().putString(SwipeActions.KEY_PREFIX_SWIPEACTIONS + tag, right + "," + left).apply(); + } + + private void saveActionsEnabledPrefs(Boolean enabled) { + SharedPreferences prefs = context.getSharedPreferences(SwipeActions.PREF_NAME, Context.MODE_PRIVATE); + prefs.edit().putBoolean(SwipeActions.KEY_PREFIX_NO_ACTION + tag, enabled).apply(); + } + + public interface Callback { + void onCall(); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/swipeactions/TogglePlaybackStateSwipeAction.java b/app/src/main/java/de/danoeh/antennapod/ui/swipeactions/TogglePlaybackStateSwipeAction.java new file mode 100644 index 000000000..aa83d5e3e --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/swipeactions/TogglePlaybackStateSwipeAction.java @@ -0,0 +1,48 @@ +package de.danoeh.antennapod.ui.swipeactions; + +import android.content.Context; + +import androidx.fragment.app.Fragment; + +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.ui.episodeslist.FeedItemMenuHandler; +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedItemFilter; + +public class TogglePlaybackStateSwipeAction implements SwipeAction { + + @Override + public String getId() { + return TOGGLE_PLAYED; + } + + @Override + public int getActionIcon() { + return R.drawable.ic_mark_played; + } + + @Override + public int getActionColor() { + return R.attr.icon_gray; + } + + @Override + public String getTitle(Context context) { + return context.getString(R.string.toggle_played_label); + } + + @Override + public void performAction(FeedItem item, Fragment fragment, FeedItemFilter filter) { + int newState = item.getPlayState() == FeedItem.UNPLAYED ? FeedItem.PLAYED : FeedItem.UNPLAYED; + FeedItemMenuHandler.markReadWithUndo(fragment, item, newState, willRemove(filter, item)); + } + + @Override + public boolean willRemove(FeedItemFilter filter, FeedItem item) { + if (item.getPlayState() == FeedItem.NEW) { + return filter.showPlayed || filter.showNew; + } else { + return filter.showUnplayed || filter.showPlayed || filter.showNew; + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/view/EmptyViewHandler.java b/app/src/main/java/de/danoeh/antennapod/ui/view/EmptyViewHandler.java new file mode 100644 index 000000000..b467f6f0e --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/view/EmptyViewHandler.java @@ -0,0 +1,152 @@ +package de.danoeh.antennapod.ui.view; + +import android.content.Context; +import android.database.DataSetObserver; +import android.view.Gravity; +import android.widget.AbsListView; +import android.widget.FrameLayout; +import android.widget.ListAdapter; +import androidx.annotation.DrawableRes; +import androidx.coordinatorlayout.widget.CoordinatorLayout; +import androidx.recyclerview.widget.RecyclerView; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.RelativeLayout; +import android.widget.TextView; + +import de.danoeh.antennapod.R; + +public class EmptyViewHandler { + private boolean layoutAdded = false; + private ListAdapter listAdapter; + private RecyclerView.Adapter recyclerAdapter; + + private final View emptyView; + private final TextView tvTitle; + private final TextView tvMessage; + private final ImageView ivIcon; + + public EmptyViewHandler(Context context) { + emptyView = View.inflate(context, R.layout.empty_view_layout, null); + tvTitle = emptyView.findViewById(R.id.emptyViewTitle); + tvMessage = emptyView.findViewById(R.id.emptyViewMessage); + ivIcon = emptyView.findViewById(R.id.emptyViewIcon); + } + + public void setTitle(int title) { + tvTitle.setText(title); + } + + public void setMessage(int message) { + tvMessage.setText(message); + } + + public void setMessage(String message) { + tvMessage.setText(message); + } + + public void setIcon(@DrawableRes int icon) { + ivIcon.setImageResource(icon); + ivIcon.setVisibility(View.VISIBLE); + } + + public void hide() { + emptyView.setVisibility(View.GONE); + } + + public void attachToListView(AbsListView listView) { + if (layoutAdded) { + throw new IllegalStateException("Can not attach EmptyView multiple times"); + } + addToParentView(listView); + layoutAdded = true; + listView.setEmptyView(emptyView); + updateAdapter(listView.getAdapter()); + } + + public void attachToRecyclerView(RecyclerView recyclerView) { + if (layoutAdded) { + throw new IllegalStateException("Can not attach EmptyView multiple times"); + } + addToParentView(recyclerView); + layoutAdded = true; + updateAdapter(recyclerView.getAdapter()); + } + + private void addToParentView(View view) { + ViewGroup parent = ((ViewGroup) view.getParent()); + while (parent != null) { + if (parent instanceof RelativeLayout) { + parent.addView(emptyView); + RelativeLayout.LayoutParams layoutParams = + (RelativeLayout.LayoutParams) emptyView.getLayoutParams(); + layoutParams.addRule(RelativeLayout.CENTER_IN_PARENT, RelativeLayout.TRUE); + emptyView.setLayoutParams(layoutParams); + break; + } else if (parent instanceof FrameLayout) { + parent.addView(emptyView); + FrameLayout.LayoutParams layoutParams = + (FrameLayout.LayoutParams) emptyView.getLayoutParams(); + layoutParams.gravity = Gravity.CENTER; + emptyView.setLayoutParams(layoutParams); + break; + } else if (parent instanceof CoordinatorLayout) { + parent.addView(emptyView); + CoordinatorLayout.LayoutParams layoutParams = + (CoordinatorLayout.LayoutParams) emptyView.getLayoutParams(); + layoutParams.gravity = Gravity.CENTER; + emptyView.setLayoutParams(layoutParams); + break; + } + parent = (ViewGroup) parent.getParent(); + } + } + + public void updateAdapter(RecyclerView.Adapter adapter) { + if (this.recyclerAdapter != null) { + this.recyclerAdapter.unregisterAdapterDataObserver(adapterObserver); + } + this.recyclerAdapter = adapter; + if (adapter != null) { + adapter.registerAdapterDataObserver(adapterObserver); + } + updateVisibility(); + } + + private void updateAdapter(ListAdapter adapter) { + if (this.listAdapter != null) { + this.listAdapter.unregisterDataSetObserver(listAdapterObserver); + } + this.listAdapter = adapter; + if (adapter != null) { + adapter.registerDataSetObserver(listAdapterObserver); + } + updateVisibility(); + } + + private final SimpleAdapterDataObserver adapterObserver = new SimpleAdapterDataObserver() { + @Override + public void anythingChanged() { + updateVisibility(); + } + }; + + private final DataSetObserver listAdapterObserver = new DataSetObserver() { + public void onChanged() { + updateVisibility(); + } + }; + + public void updateVisibility() { + boolean empty; + if (recyclerAdapter != null) { + empty = recyclerAdapter.getItemCount() == 0; + } else if (listAdapter != null) { + empty = listAdapter.isEmpty(); + } else { + empty = true; + } + emptyView.setVisibility(empty ? View.VISIBLE : View.GONE); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/view/ItemOffsetDecoration.java b/app/src/main/java/de/danoeh/antennapod/ui/view/ItemOffsetDecoration.java new file mode 100644 index 000000000..4c3adcbc1 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/view/ItemOffsetDecoration.java @@ -0,0 +1,24 @@ +package de.danoeh.antennapod.ui.view; + +import android.content.Context; +import android.graphics.Rect; +import android.view.View; +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +/** + * Source: https://stackoverflow.com/a/30794046 + */ +public class ItemOffsetDecoration extends RecyclerView.ItemDecoration { + private final int itemOffset; + + public ItemOffsetDecoration(@NonNull Context context, int itemOffsetDp) { + itemOffset = (int) (itemOffsetDp * context.getResources().getDisplayMetrics().density); + } + + @Override + public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { + super.getItemOffsets(outRect, view, parent, state); + outRect.set(itemOffset, itemOffset, itemOffset, itemOffset); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/view/LiftOnScrollListener.java b/app/src/main/java/de/danoeh/antennapod/ui/view/LiftOnScrollListener.java new file mode 100644 index 000000000..e046dd43e --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/view/LiftOnScrollListener.java @@ -0,0 +1,59 @@ +package de.danoeh.antennapod.ui.view; + +import android.animation.ValueAnimator; +import android.view.View; +import androidx.annotation.NonNull; +import androidx.core.widget.NestedScrollView; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +/** + * Workaround for app:liftOnScroll flickering when in SwipeRefreshLayout + */ +public class LiftOnScrollListener extends RecyclerView.OnScrollListener + implements NestedScrollView.OnScrollChangeListener { + private final ValueAnimator animator; + private boolean animatingToScrolled = false; + + public LiftOnScrollListener(View appBar) { + animator = ValueAnimator.ofFloat(0, appBar.getContext().getResources().getDisplayMetrics().density * 8); + animator.addUpdateListener(animation -> appBar.setElevation((float) animation.getAnimatedValue())); + } + + @Override + public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { + elevate(isScrolled(recyclerView)); + } + + private boolean isScrolled(RecyclerView recyclerView) { + int firstItem = ((LinearLayoutManager) recyclerView.getLayoutManager()).findFirstVisibleItemPosition(); + if (firstItem < 0) { + return false; + } else if (firstItem > 0) { + return true; + } + View firstItemView = recyclerView.getLayoutManager().findViewByPosition(firstItem); + if (firstItemView == null) { + return false; + } else { + return firstItemView.getTop() < 0; + } + } + + @Override + public void onScrollChange(@NonNull NestedScrollView v, int scrollX, int scrollY, int oldScrollX, int oldScrollY) { + elevate(scrollY != 0); + } + + private void elevate(boolean isScrolled) { + if (isScrolled == animatingToScrolled) { + return; + } + animatingToScrolled = isScrolled; + if (isScrolled) { + animator.start(); + } else { + animator.reverse(); + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/view/LocalDeleteModal.java b/app/src/main/java/de/danoeh/antennapod/ui/view/LocalDeleteModal.java new file mode 100644 index 000000000..f0a1105c1 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/view/LocalDeleteModal.java @@ -0,0 +1,32 @@ +package de.danoeh.antennapod.ui.view; + +import android.content.Context; + +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import de.danoeh.antennapod.ui.i18n.R; +import de.danoeh.antennapod.model.feed.FeedItem; + +public class LocalDeleteModal { + public static void showLocalFeedDeleteWarningIfNecessary(Context context, Iterable items, + Runnable deleteCommand) { + boolean anyLocalFeed = false; + for (FeedItem item : items) { + if (item.getFeed().isLocalFeed()) { + anyLocalFeed = true; + break; + } + } + + if (!anyLocalFeed) { + deleteCommand.run(); + return; + } + + new MaterialAlertDialogBuilder(context) + .setTitle(R.string.delete_episode_label) + .setMessage(R.string.delete_local_feed_warning_body) + .setPositiveButton(R.string.delete_label, (dialog, which) -> deleteCommand.run()) + .setNegativeButton(R.string.cancel_label, null) + .show(); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/view/LockableBottomSheetBehavior.java b/app/src/main/java/de/danoeh/antennapod/ui/view/LockableBottomSheetBehavior.java new file mode 100644 index 000000000..aa506aaea --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/view/LockableBottomSheetBehavior.java @@ -0,0 +1,86 @@ +package de.danoeh.antennapod.ui.view; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; +import androidx.coordinatorlayout.widget.CoordinatorLayout; +import com.google.android.material.bottomsheet.ViewPagerBottomSheetBehavior; + +/** + * Based on https://stackoverflow.com/a/40798214 + */ +public class LockableBottomSheetBehavior extends ViewPagerBottomSheetBehavior { + private boolean isLocked = false; + + public LockableBottomSheetBehavior() {} + + public LockableBottomSheetBehavior(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public void setLocked(boolean locked) { + isLocked = locked; + } + + @Override + public boolean onInterceptTouchEvent(CoordinatorLayout parent, V child, MotionEvent event) { + boolean handled = false; + + if (!isLocked) { + handled = super.onInterceptTouchEvent(parent, child, event); + } + + return handled; + } + + @Override + public boolean onTouchEvent(CoordinatorLayout parent, V child, MotionEvent event) { + boolean handled = false; + + if (!isLocked) { + handled = super.onTouchEvent(parent, child, event); + } + + return handled; + } + + @Override + public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, V child, View directTargetChild, + View target, int axes, int type) { + boolean handled = false; + + if (!isLocked) { + handled = super.onStartNestedScroll(coordinatorLayout, child, directTargetChild, target, axes, type); + } + + return handled; + } + + @Override + public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, V child, View target, + int dx, int dy, int[] consumed, int type) { + if (!isLocked) { + super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type); + } + } + + @Override + public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target, int type) { + if (!isLocked) { + super.onStopNestedScroll(coordinatorLayout, child, target, type); + } + } + + @Override + public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, V child, View target, + float velocityX, float velocityY) { + boolean handled = false; + + if (!isLocked) { + handled = super.onNestedPreFling(coordinatorLayout, child, target, velocityX, velocityY); + } + + return handled; + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/view/NestedScrollableHost.java b/app/src/main/java/de/danoeh/antennapod/ui/view/NestedScrollableHost.java new file mode 100644 index 000000000..b29792d90 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/view/NestedScrollableHost.java @@ -0,0 +1,197 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * 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. + * + * Source: https://github.com/android/views-widgets-samples/blob/87e58d1/ViewPager2/app/src/main/java/androidx/viewpager2/integration/testapp/NestedScrollableHost.kt + * And modified for our need + */ + +package de.danoeh.antennapod.ui.view; + +import android.content.Context; +import android.content.res.TypedArray; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewTreeObserver; +import android.widget.FrameLayout; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.viewpager2.widget.ViewPager2; + +import de.danoeh.antennapod.R; + +import static androidx.viewpager2.widget.ViewPager2.ORIENTATION_HORIZONTAL; +import static androidx.viewpager2.widget.ViewPager2.ORIENTATION_VERTICAL; + + +/** + * Layout to wrap a scrollable component inside a ViewPager2. Provided as a solution to the problem + * where pages of ViewPager2 have nested scrollable elements that scroll in the same direction as + * ViewPager2. The scrollable element needs to be the immediate and only child of this host layout. + * + * This solution has limitations when using multiple levels of nested scrollable elements + * (e.g. a horizontal RecyclerView in a vertical RecyclerView in a horizontal ViewPager2). + */ // KhaledAlharthi/NestedScrollableHost.java +public class NestedScrollableHost extends FrameLayout { + + private ViewPager2 parentViewPager; + private int touchSlop = 0; + private float initialX = 0f; + private float initialY = 0f; + private int preferVertical = 1; + private int preferHorizontal = 1; + private int scrollDirection = 0; + + public NestedScrollableHost(@NonNull Context context) { + super(context); + init(context); + } + + public NestedScrollableHost(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + init(context); + setAttributes(context, attrs); + } + + public NestedScrollableHost(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(context); + setAttributes(context, attrs); + } + + public NestedScrollableHost(@NonNull Context context, @Nullable AttributeSet attrs, + int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + init(context); + setAttributes(context, attrs); + } + + private void setAttributes(@NonNull Context context, @Nullable AttributeSet attrs) { + TypedArray a = context.getTheme().obtainStyledAttributes( + attrs, + R.styleable.NestedScrollableHost, + 0, 0); + + try { + preferHorizontal = a.getInteger(R.styleable.NestedScrollableHost_preferHorizontal, 1); + preferVertical = a.getInteger(R.styleable.NestedScrollableHost_preferVertical, 1); + scrollDirection = a.getInteger(R.styleable.NestedScrollableHost_scrollDirection, 0); + } finally { + a.recycle(); + } + + } + + private void init(Context context) { + touchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); + + + getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { + @Override + public boolean onPreDraw() { + View v = (View) getParent(); + while (v != null && !(v instanceof ViewPager2) || isntSameDirection(v)) { + v = (View) v.getParent(); + } + parentViewPager = (ViewPager2) v; + + getViewTreeObserver().removeOnPreDrawListener(this); + return false; + } + }); + } + + private Boolean isntSameDirection(View v) { + int orientation = 0; + switch (scrollDirection) { + default: + case 0: + return false; + case 1: + orientation = ORIENTATION_VERTICAL; + break; + case 2: + orientation = ORIENTATION_HORIZONTAL; + break; + } + return ((v instanceof ViewPager2) && ((ViewPager2) v).getOrientation() != orientation); + } + + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + handleInterceptTouchEvent(ev); + return super.onInterceptTouchEvent(ev); + } + + + private boolean canChildScroll(int orientation, float delta) { + int direction = (int) -delta; + View child = getChildAt(0); + if (orientation == 0) { + return child.canScrollHorizontally(direction); + } else if (orientation == 1) { + return child.canScrollVertically(direction); + } else { + throw new IllegalArgumentException(); + } + } + + private void handleInterceptTouchEvent(MotionEvent e) { + if (parentViewPager == null) { + return; + } + int orientation = parentViewPager.getOrientation(); + boolean preferedDirection = preferHorizontal + preferVertical > 2; + + // Early return if child can't scroll in same direction as parent and theres no prefered scroll direction + if (!canChildScroll(orientation, -1f) && !canChildScroll(orientation, 1f) && !preferedDirection) { + return; + } + + + if (e.getAction() == MotionEvent.ACTION_DOWN) { + initialX = e.getX(); + initialY = e.getY(); + getParent().requestDisallowInterceptTouchEvent(true); + } else if (e.getAction() == MotionEvent.ACTION_MOVE) { + float dx = e.getX() - initialX; + float dy = e.getY() - initialY; + boolean isVpHorizontal = orientation == ViewPager2.ORIENTATION_HORIZONTAL; + + // assuming ViewPager2 touch-slop is 2x touch-slop of child + float scaledDx = Math.abs(dx) * (isVpHorizontal ? 1f : 0.5f) * preferHorizontal; + float scaledDy = Math.abs(dy) * (isVpHorizontal ? 0.5f : 1f) * preferVertical; + if (scaledDx > touchSlop || scaledDy > touchSlop) { + if (isVpHorizontal == (scaledDy > scaledDx)) { + // Gesture is perpendicular, allow all parents to intercept + getParent().requestDisallowInterceptTouchEvent(preferedDirection); + } else { + // Gesture is parallel, query child if movement in that direction is possible + if (canChildScroll(orientation, isVpHorizontal ? dx : dy)) { + // Child can scroll, disallow all parents to intercept + getParent().requestDisallowInterceptTouchEvent(true); + } else { + // Child cannot scroll, allow all parents to intercept + getParent().requestDisallowInterceptTouchEvent(false); + } + } + } + + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/view/ShownotesWebView.java b/app/src/main/java/de/danoeh/antennapod/ui/view/ShownotesWebView.java new file mode 100644 index 000000000..67a659418 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/view/ShownotesWebView.java @@ -0,0 +1,189 @@ +package de.danoeh.antennapod.ui.view; + +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; +import android.content.Intent; +import android.graphics.Color; +import android.net.Uri; +import android.os.Build; +import android.util.AttributeSet; +import android.util.Log; +import android.view.ContextMenu; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.webkit.WebSettings; +import android.webkit.WebView; +import android.webkit.WebViewClient; + +import androidx.core.content.ContextCompat; +import androidx.core.util.Consumer; + +import com.google.android.material.snackbar.Snackbar; + +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.ui.MenuItemUtils; +import de.danoeh.antennapod.ui.common.Converter; +import de.danoeh.antennapod.core.util.IntentUtils; +import de.danoeh.antennapod.net.common.NetworkUtils; +import de.danoeh.antennapod.ui.share.ShareUtils; +import de.danoeh.antennapod.ui.cleaner.ShownotesCleaner; + +public class ShownotesWebView extends WebView implements View.OnLongClickListener { + private static final String TAG = "ShownotesWebView"; + + /** + * URL that was selected via long-press. + */ + private String selectedUrl; + private Consumer timecodeSelectedListener; + private Runnable pageFinishedListener; + + public ShownotesWebView(Context context) { + super(context); + setup(); + } + + public ShownotesWebView(Context context, AttributeSet attrs) { + super(context, attrs); + setup(); + } + + public ShownotesWebView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + setup(); + } + + private void setup() { + setBackgroundColor(Color.TRANSPARENT); + if (!NetworkUtils.networkAvailable()) { + getSettings().setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK); + // Use cached resources, even if they have expired + } + getSettings().setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW); + getSettings().setUseWideViewPort(false); + getSettings().setLoadWithOverviewMode(true); + setOnLongClickListener(this); + + setWebViewClient(new WebViewClient() { + @Override + public boolean shouldOverrideUrlLoading(WebView view, String url) { + if (ShownotesCleaner.isTimecodeLink(url) && timecodeSelectedListener != null) { + timecodeSelectedListener.accept(ShownotesCleaner.getTimecodeLinkTime(url)); + } else { + IntentUtils.openInBrowser(getContext(), url); + } + return true; + } + + @Override + public void onPageFinished(WebView view, String url) { + super.onPageFinished(view, url); + Log.d(TAG, "Page finished"); + if (pageFinishedListener != null) { + pageFinishedListener.run(); + } + } + }); + } + + @Override + public boolean onLongClick(View v) { + WebView.HitTestResult r = getHitTestResult(); + if (r != null && r.getType() == WebView.HitTestResult.SRC_ANCHOR_TYPE) { + Log.d(TAG, "Link of webview was long-pressed. Extra: " + r.getExtra()); + selectedUrl = r.getExtra(); + showContextMenu(); + return true; + } else if (r != null && r.getType() == HitTestResult.EMAIL_TYPE) { + Log.d(TAG, "E-Mail of webview was long-pressed. Extra: " + r.getExtra()); + ClipboardManager clipboardManager = ContextCompat.getSystemService(this.getContext(), + ClipboardManager.class); + if (clipboardManager != null) { + clipboardManager.setPrimaryClip(ClipData.newPlainText("AntennaPod", r.getExtra())); + } + if (Build.VERSION.SDK_INT <= 32 && this.getContext() instanceof MainActivity) { + ((MainActivity) this.getContext()).showSnackbarAbovePlayer( + getResources().getString(R.string.copied_to_clipboard), + Snackbar.LENGTH_SHORT); + } + return true; + } + selectedUrl = null; + return false; + } + + public boolean onContextItemSelected(MenuItem item) { + if (selectedUrl == null) { + return false; + } + + final int itemId = item.getItemId(); + if (itemId == R.id.open_in_browser_item) { + IntentUtils.openInBrowser(getContext(), selectedUrl); + } else if (itemId == R.id.share_url_item) { + ShareUtils.shareLink(getContext(), selectedUrl); + } else if (itemId == R.id.copy_url_item) { + ClipData clipData = ClipData.newPlainText(selectedUrl, selectedUrl); + ClipboardManager cm = (ClipboardManager) getContext() + .getSystemService(Context.CLIPBOARD_SERVICE); + cm.setPrimaryClip(clipData); + if (Build.VERSION.SDK_INT < 32) { + Snackbar s = Snackbar.make(this, R.string.copied_to_clipboard, Snackbar.LENGTH_LONG); + s.getView().setElevation(100); + s.show(); + } + } else if (itemId == R.id.go_to_position_item) { + if (ShownotesCleaner.isTimecodeLink(selectedUrl) && timecodeSelectedListener != null) { + timecodeSelectedListener.accept(ShownotesCleaner.getTimecodeLinkTime(selectedUrl)); + } else { + Log.e(TAG, "Selected go_to_position_item, but URL was no timecode link: " + selectedUrl); + } + } else { + selectedUrl = null; + return false; + } + selectedUrl = null; + return true; + } + + @Override + protected void onCreateContextMenu(ContextMenu menu) { + super.onCreateContextMenu(menu); + if (selectedUrl == null) { + return; + } + + if (ShownotesCleaner.isTimecodeLink(selectedUrl)) { + menu.add(Menu.NONE, R.id.go_to_position_item, Menu.NONE, R.string.go_to_position_label); + menu.setHeaderTitle(Converter.getDurationStringLong(ShownotesCleaner.getTimecodeLinkTime(selectedUrl))); + } else { + Uri uri = Uri.parse(selectedUrl); + final Intent intent = new Intent(Intent.ACTION_VIEW, uri); + if (IntentUtils.isCallable(getContext(), intent)) { + menu.add(Menu.NONE, R.id.open_in_browser_item, Menu.NONE, R.string.open_in_browser_label); + } + menu.add(Menu.NONE, R.id.copy_url_item, Menu.NONE, R.string.copy_url_label); + menu.add(Menu.NONE, R.id.share_url_item, Menu.NONE, R.string.share_url_label); + menu.setHeaderTitle(selectedUrl); + } + MenuItemUtils.setOnClickListeners(menu, this::onContextItemSelected); + } + + public void setTimecodeSelectedListener(Consumer timecodeSelectedListener) { + this.timecodeSelectedListener = timecodeSelectedListener; + } + + public void setPageFinishedListener(Runnable pageFinishedListener) { + this.pageFinishedListener = pageFinishedListener; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + setMeasuredDimension(Math.max(getMeasuredWidth(), getMinimumWidth()), + Math.max(getMeasuredHeight(), getMinimumHeight())); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/view/SimpleAdapterDataObserver.java b/app/src/main/java/de/danoeh/antennapod/ui/view/SimpleAdapterDataObserver.java new file mode 100644 index 000000000..2e00aa436 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/view/SimpleAdapterDataObserver.java @@ -0,0 +1,41 @@ +package de.danoeh.antennapod.ui.view; + +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.RecyclerView; + +/** + * AdapterDataObserver that relays all events to the method anythingChanged(). + */ +public abstract class SimpleAdapterDataObserver extends RecyclerView.AdapterDataObserver { + public abstract void anythingChanged(); + + @Override + public void onChanged() { + anythingChanged(); + } + + @Override + public void onItemRangeChanged(int positionStart, int itemCount) { + anythingChanged(); + } + + @Override + public void onItemRangeChanged(int positionStart, int itemCount, @Nullable Object payload) { + anythingChanged(); + } + + @Override + public void onItemRangeInserted(int positionStart, int itemCount) { + anythingChanged(); + } + + @Override + public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) { + anythingChanged(); + } + + @Override + public void onItemRangeRemoved(int positionStart, int itemCount) { + anythingChanged(); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/view/AspectRatioVideoView.java b/app/src/main/java/de/danoeh/antennapod/view/AspectRatioVideoView.java deleted file mode 100644 index 4c116fa2d..000000000 --- a/app/src/main/java/de/danoeh/antennapod/view/AspectRatioVideoView.java +++ /dev/null @@ -1,115 +0,0 @@ -package de.danoeh.antennapod.view; - -/* - * Copyright (C) Google Inc. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import android.content.Context; -import android.util.AttributeSet; -import android.widget.VideoView; - -public class AspectRatioVideoView extends VideoView { - - - private int mVideoWidth; - private int mVideoHeight; - private float mAvailableWidth = -1; - private float mAvailableHeight = -1; - - public AspectRatioVideoView(Context context) { - this(context, null); - } - - public AspectRatioVideoView(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - public AspectRatioVideoView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - - mVideoWidth = 0; - mVideoHeight = 0; - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - if (mVideoWidth <= 0 || mVideoHeight <= 0) { - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - return; - } - - if (mAvailableWidth < 0 || mAvailableHeight < 0) { - mAvailableWidth = getWidth(); - mAvailableHeight = getHeight(); - } - - float heightRatio = (float) mVideoHeight / mAvailableHeight; - float widthRatio = (float) mVideoWidth / mAvailableWidth; - - int scaledHeight; - int scaledWidth; - - if (heightRatio > widthRatio) { - scaledHeight = (int) Math.ceil((float) mVideoHeight - / heightRatio); - scaledWidth = (int) Math.ceil((float) mVideoWidth - / heightRatio); - } else { - scaledHeight = (int) Math.ceil((float) mVideoHeight - / widthRatio); - scaledWidth = (int) Math.ceil((float) mVideoWidth - / widthRatio); - } - - setMeasuredDimension(scaledWidth, scaledHeight); - } - - /** - * Source code originally from: - * http://clseto.mysinablog.com/index.php?op=ViewArticle&articleId=2992625 - * - * @param videoWidth - * @param videoHeight - */ - public void setVideoSize(int videoWidth, int videoHeight) { - // Set the new video size - mVideoWidth = videoWidth; - mVideoHeight = videoHeight; - - /* - * If this isn't set the video is stretched across the - * SurfaceHolders display surface (i.e. the SurfaceHolder - * as the same size and the video is drawn to fit this - * display area). We want the size to be the video size - * and allow the aspectratio to handle how the surface is shown - */ - getHolder().setFixedSize(videoWidth, videoHeight); - - requestLayout(); - invalidate(); - } - - /** - * Sets the maximum size that the view might expand to - * @param width - * @param height - */ - public void setAvailableSize(float width, float height) { - mAvailableWidth = width; - mAvailableHeight = height; - requestLayout(); - } - -} diff --git a/app/src/main/java/de/danoeh/antennapod/view/ChapterSeekBar.java b/app/src/main/java/de/danoeh/antennapod/view/ChapterSeekBar.java deleted file mode 100644 index d35206647..000000000 --- a/app/src/main/java/de/danoeh/antennapod/view/ChapterSeekBar.java +++ /dev/null @@ -1,148 +0,0 @@ -package de.danoeh.antennapod.view; - -import android.content.Context; -import android.graphics.Canvas; -import android.graphics.Paint; -import android.os.Handler; -import android.os.Looper; -import android.util.AttributeSet; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.ui.common.ThemeUtils; - -public class ChapterSeekBar extends androidx.appcompat.widget.AppCompatSeekBar { - - private float top; - private float width; - private float center; - private float bottom; - private float density; - private float progressPrimary; - private float progressSecondary; - private float[] dividerPos; - private boolean isHighlighted = false; - private final Paint paintBackground = new Paint(); - private final Paint paintProgressPrimary = new Paint(); - - public ChapterSeekBar(Context context) { - super(context); - init(context); - } - - public ChapterSeekBar(Context context, AttributeSet attrs) { - super(context, attrs); - init(context); - } - - public ChapterSeekBar(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - init(context); - } - - private void init(Context context) { - setBackground(null); // Removes the thumb shadow - dividerPos = null; - density = context.getResources().getDisplayMetrics().density; - - paintBackground.setColor(ThemeUtils.getColorFromAttr(getContext(), R.attr.colorSurfaceVariant)); - paintBackground.setAlpha(128); - paintProgressPrimary.setColor(ThemeUtils.getColorFromAttr(getContext(), R.attr.colorPrimary)); - } - - /** - * Sets the relative positions of the chapter dividers. - * @param dividerPos of the chapter dividers relative to the duration of the media. - */ - public void setDividerPos(final float[] dividerPos) { - if (dividerPos != null) { - this.dividerPos = new float[dividerPos.length + 2]; - this.dividerPos[0] = 0; - System.arraycopy(dividerPos, 0, this.dividerPos, 1, dividerPos.length); - this.dividerPos[this.dividerPos.length - 1] = 1; - } else { - this.dividerPos = null; - } - invalidate(); - } - - public void highlightCurrentChapter() { - isHighlighted = true; - new Handler(Looper.getMainLooper()).postDelayed(new Runnable() { - @Override - public void run() { - isHighlighted = false; - invalidate(); - } - }, 1000); - } - - @Override - protected synchronized void onDraw(Canvas canvas) { - center = (getBottom() - getPaddingBottom() - getTop() - getPaddingTop()) / 2.0f; - top = center - density * 1.5f; - bottom = center + density * 1.5f; - width = (float) (getRight() - getPaddingRight() - getLeft() - getPaddingLeft()); - progressSecondary = getSecondaryProgress() / (float) getMax() * width; - progressPrimary = getProgress() / (float) getMax() * width; - - if (dividerPos == null) { - drawProgress(canvas); - } else { - drawProgressChapters(canvas); - } - drawThumb(canvas); - } - - private void drawProgress(Canvas canvas) { - final int saveCount = canvas.save(); - canvas.translate(getPaddingLeft(), getPaddingTop()); - canvas.drawRect(0, top, width, bottom, paintBackground); - canvas.drawRect(0, top, progressSecondary, bottom, paintBackground); - canvas.drawRect(0, top, progressPrimary, bottom, paintProgressPrimary); - canvas.restoreToCount(saveCount); - } - - private void drawProgressChapters(Canvas canvas) { - final int saveCount = canvas.save(); - int currChapter = 1; - float chapterMargin = density * 1.2f; - float topExpanded = center - density * 2.0f; - float bottomExpanded = center + density * 2.0f; - - canvas.translate(getPaddingLeft(), getPaddingTop()); - - for (int i = 1; i < dividerPos.length; i++) { - float right = dividerPos[i] * width - chapterMargin; - float left = dividerPos[i - 1] * width; - float rightCurr = dividerPos[currChapter] * width - chapterMargin; - float leftCurr = dividerPos[currChapter - 1] * width; - - canvas.drawRect(left, top, right, bottom, paintBackground); - - if (progressSecondary > 0 && progressSecondary < width) { - if (right < progressSecondary) { - canvas.drawRect(left, top, right, bottom, paintBackground); - } else if (progressSecondary > left) { - canvas.drawRect(left, top, progressSecondary, bottom, paintBackground); - } - } - - if (right < progressPrimary) { - currChapter = i + 1; - canvas.drawRect(left, top, right, bottom, paintProgressPrimary); - } else if (isHighlighted || isPressed()) { - canvas.drawRect(leftCurr, topExpanded, rightCurr, bottomExpanded, paintBackground); - canvas.drawRect(leftCurr, topExpanded, progressPrimary, bottomExpanded, paintProgressPrimary); - } else { - canvas.drawRect(leftCurr, top, progressPrimary, bottom, paintProgressPrimary); - } - } - canvas.restoreToCount(saveCount); - } - - private void drawThumb(Canvas canvas) { - final int saveCount = canvas.save(); - canvas.translate(getPaddingLeft() - getThumbOffset(), getPaddingTop()); - getThumb().draw(canvas); - canvas.restoreToCount(saveCount); - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/view/EmptyViewHandler.java b/app/src/main/java/de/danoeh/antennapod/view/EmptyViewHandler.java deleted file mode 100644 index 2ecaaa5b3..000000000 --- a/app/src/main/java/de/danoeh/antennapod/view/EmptyViewHandler.java +++ /dev/null @@ -1,152 +0,0 @@ -package de.danoeh.antennapod.view; - -import android.content.Context; -import android.database.DataSetObserver; -import android.view.Gravity; -import android.widget.AbsListView; -import android.widget.FrameLayout; -import android.widget.ListAdapter; -import androidx.annotation.DrawableRes; -import androidx.coordinatorlayout.widget.CoordinatorLayout; -import androidx.recyclerview.widget.RecyclerView; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.RelativeLayout; -import android.widget.TextView; - -import de.danoeh.antennapod.R; - -public class EmptyViewHandler { - private boolean layoutAdded = false; - private ListAdapter listAdapter; - private RecyclerView.Adapter recyclerAdapter; - - private final View emptyView; - private final TextView tvTitle; - private final TextView tvMessage; - private final ImageView ivIcon; - - public EmptyViewHandler(Context context) { - emptyView = View.inflate(context, R.layout.empty_view_layout, null); - tvTitle = emptyView.findViewById(R.id.emptyViewTitle); - tvMessage = emptyView.findViewById(R.id.emptyViewMessage); - ivIcon = emptyView.findViewById(R.id.emptyViewIcon); - } - - public void setTitle(int title) { - tvTitle.setText(title); - } - - public void setMessage(int message) { - tvMessage.setText(message); - } - - public void setMessage(String message) { - tvMessage.setText(message); - } - - public void setIcon(@DrawableRes int icon) { - ivIcon.setImageResource(icon); - ivIcon.setVisibility(View.VISIBLE); - } - - public void hide() { - emptyView.setVisibility(View.GONE); - } - - public void attachToListView(AbsListView listView) { - if (layoutAdded) { - throw new IllegalStateException("Can not attach EmptyView multiple times"); - } - addToParentView(listView); - layoutAdded = true; - listView.setEmptyView(emptyView); - updateAdapter(listView.getAdapter()); - } - - public void attachToRecyclerView(RecyclerView recyclerView) { - if (layoutAdded) { - throw new IllegalStateException("Can not attach EmptyView multiple times"); - } - addToParentView(recyclerView); - layoutAdded = true; - updateAdapter(recyclerView.getAdapter()); - } - - private void addToParentView(View view) { - ViewGroup parent = ((ViewGroup) view.getParent()); - while (parent != null) { - if (parent instanceof RelativeLayout) { - parent.addView(emptyView); - RelativeLayout.LayoutParams layoutParams = - (RelativeLayout.LayoutParams) emptyView.getLayoutParams(); - layoutParams.addRule(RelativeLayout.CENTER_IN_PARENT, RelativeLayout.TRUE); - emptyView.setLayoutParams(layoutParams); - break; - } else if (parent instanceof FrameLayout) { - parent.addView(emptyView); - FrameLayout.LayoutParams layoutParams = - (FrameLayout.LayoutParams) emptyView.getLayoutParams(); - layoutParams.gravity = Gravity.CENTER; - emptyView.setLayoutParams(layoutParams); - break; - } else if (parent instanceof CoordinatorLayout) { - parent.addView(emptyView); - CoordinatorLayout.LayoutParams layoutParams = - (CoordinatorLayout.LayoutParams) emptyView.getLayoutParams(); - layoutParams.gravity = Gravity.CENTER; - emptyView.setLayoutParams(layoutParams); - break; - } - parent = (ViewGroup) parent.getParent(); - } - } - - public void updateAdapter(RecyclerView.Adapter adapter) { - if (this.recyclerAdapter != null) { - this.recyclerAdapter.unregisterAdapterDataObserver(adapterObserver); - } - this.recyclerAdapter = adapter; - if (adapter != null) { - adapter.registerAdapterDataObserver(adapterObserver); - } - updateVisibility(); - } - - private void updateAdapter(ListAdapter adapter) { - if (this.listAdapter != null) { - this.listAdapter.unregisterDataSetObserver(listAdapterObserver); - } - this.listAdapter = adapter; - if (adapter != null) { - adapter.registerDataSetObserver(listAdapterObserver); - } - updateVisibility(); - } - - private final SimpleAdapterDataObserver adapterObserver = new SimpleAdapterDataObserver() { - @Override - public void anythingChanged() { - updateVisibility(); - } - }; - - private final DataSetObserver listAdapterObserver = new DataSetObserver() { - public void onChanged() { - updateVisibility(); - } - }; - - public void updateVisibility() { - boolean empty; - if (recyclerAdapter != null) { - empty = recyclerAdapter.getItemCount() == 0; - } else if (listAdapter != null) { - empty = listAdapter.isEmpty(); - } else { - empty = true; - } - emptyView.setVisibility(empty ? View.VISIBLE : View.GONE); - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/view/EpisodeItemListRecyclerView.java b/app/src/main/java/de/danoeh/antennapod/view/EpisodeItemListRecyclerView.java deleted file mode 100644 index fb1c533c5..000000000 --- a/app/src/main/java/de/danoeh/antennapod/view/EpisodeItemListRecyclerView.java +++ /dev/null @@ -1,84 +0,0 @@ -package de.danoeh.antennapod.view; - -import android.content.Context; -import android.content.SharedPreferences; -import android.content.res.Configuration; -import android.util.AttributeSet; -import android.view.View; -import androidx.appcompat.view.ContextThemeWrapper; -import androidx.recyclerview.widget.DividerItemDecoration; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; -import de.danoeh.antennapod.R; -import io.reactivex.annotations.Nullable; - -public class EpisodeItemListRecyclerView extends RecyclerView { - private static final String TAG = "EpisodeItemListRecyclerView"; - private static final String PREF_PREFIX_SCROLL_POSITION = "scroll_position_"; - private static final String PREF_PREFIX_SCROLL_OFFSET = "scroll_offset_"; - - private LinearLayoutManager layoutManager; - - public EpisodeItemListRecyclerView(Context context) { - super(new ContextThemeWrapper(context, R.style.FastScrollRecyclerView)); - setup(); - } - - public EpisodeItemListRecyclerView(Context context, @Nullable AttributeSet attrs) { - super(new ContextThemeWrapper(context, R.style.FastScrollRecyclerView), attrs); - setup(); - } - - public EpisodeItemListRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { - super(new ContextThemeWrapper(context, R.style.FastScrollRecyclerView), attrs, defStyleAttr); - setup(); - } - - private void setup() { - layoutManager = new LinearLayoutManager(getContext()); - layoutManager.setRecycleChildrenOnDetach(true); - setLayoutManager(layoutManager); - setHasFixedSize(true); - addItemDecoration(new DividerItemDecoration(getContext(), layoutManager.getOrientation())); - setClipToPadding(false); - } - - @Override - protected void onConfigurationChanged(Configuration newConfig) { - super.onConfigurationChanged(newConfig); - int horizontalSpacing = (int) getResources().getDimension(R.dimen.additional_horizontal_spacing); - setPadding(horizontalSpacing, getPaddingTop(), horizontalSpacing, getPaddingBottom()); - } - - public void saveScrollPosition(String tag) { - int firstItem = layoutManager.findFirstVisibleItemPosition(); - View firstItemView = layoutManager.findViewByPosition(firstItem); - float topOffset; - if (firstItemView == null) { - topOffset = 0; - } else { - topOffset = firstItemView.getTop(); - } - - getContext().getSharedPreferences(TAG, Context.MODE_PRIVATE).edit() - .putInt(PREF_PREFIX_SCROLL_POSITION + tag, firstItem) - .putInt(PREF_PREFIX_SCROLL_OFFSET + tag, (int) topOffset) - .apply(); - } - - public void restoreScrollPosition(String tag) { - SharedPreferences prefs = getContext().getSharedPreferences(TAG, Context.MODE_PRIVATE); - int position = prefs.getInt(PREF_PREFIX_SCROLL_POSITION + tag, 0); - int offset = prefs.getInt(PREF_PREFIX_SCROLL_OFFSET + tag, 0); - if (position > 0 || offset > 0) { - layoutManager.scrollToPositionWithOffset(position, offset); - } - } - - public boolean isScrolledToBottom() { - int visibleEpisodeCount = getChildCount(); - int totalEpisodeCount = layoutManager.getItemCount(); - int firstVisibleEpisode = layoutManager.findFirstVisibleItemPosition(); - return (totalEpisodeCount - visibleEpisodeCount) <= (firstVisibleEpisode + 3); - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/view/ItemOffsetDecoration.java b/app/src/main/java/de/danoeh/antennapod/view/ItemOffsetDecoration.java deleted file mode 100644 index 4a1267d81..000000000 --- a/app/src/main/java/de/danoeh/antennapod/view/ItemOffsetDecoration.java +++ /dev/null @@ -1,24 +0,0 @@ -package de.danoeh.antennapod.view; - -import android.content.Context; -import android.graphics.Rect; -import android.view.View; -import androidx.annotation.NonNull; -import androidx.recyclerview.widget.RecyclerView; - -/** - * Source: https://stackoverflow.com/a/30794046 - */ -public class ItemOffsetDecoration extends RecyclerView.ItemDecoration { - private final int itemOffset; - - public ItemOffsetDecoration(@NonNull Context context, int itemOffsetDp) { - itemOffset = (int) (itemOffsetDp * context.getResources().getDisplayMetrics().density); - } - - @Override - public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { - super.getItemOffsets(outRect, view, parent, state); - outRect.set(itemOffset, itemOffset, itemOffset, itemOffset); - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/view/LiftOnScrollListener.java b/app/src/main/java/de/danoeh/antennapod/view/LiftOnScrollListener.java deleted file mode 100644 index 020fb40e8..000000000 --- a/app/src/main/java/de/danoeh/antennapod/view/LiftOnScrollListener.java +++ /dev/null @@ -1,59 +0,0 @@ -package de.danoeh.antennapod.view; - -import android.animation.ValueAnimator; -import android.view.View; -import androidx.annotation.NonNull; -import androidx.core.widget.NestedScrollView; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; - -/** - * Workaround for app:liftOnScroll flickering when in SwipeRefreshLayout - */ -public class LiftOnScrollListener extends RecyclerView.OnScrollListener - implements NestedScrollView.OnScrollChangeListener { - private final ValueAnimator animator; - private boolean animatingToScrolled = false; - - public LiftOnScrollListener(View appBar) { - animator = ValueAnimator.ofFloat(0, appBar.getContext().getResources().getDisplayMetrics().density * 8); - animator.addUpdateListener(animation -> appBar.setElevation((float) animation.getAnimatedValue())); - } - - @Override - public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { - elevate(isScrolled(recyclerView)); - } - - private boolean isScrolled(RecyclerView recyclerView) { - int firstItem = ((LinearLayoutManager) recyclerView.getLayoutManager()).findFirstVisibleItemPosition(); - if (firstItem < 0) { - return false; - } else if (firstItem > 0) { - return true; - } - View firstItemView = recyclerView.getLayoutManager().findViewByPosition(firstItem); - if (firstItemView == null) { - return false; - } else { - return firstItemView.getTop() < 0; - } - } - - @Override - public void onScrollChange(@NonNull NestedScrollView v, int scrollX, int scrollY, int oldScrollX, int oldScrollY) { - elevate(scrollY != 0); - } - - private void elevate(boolean isScrolled) { - if (isScrolled == animatingToScrolled) { - return; - } - animatingToScrolled = isScrolled; - if (isScrolled) { - animator.start(); - } else { - animator.reverse(); - } - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/view/LocalDeleteModal.java b/app/src/main/java/de/danoeh/antennapod/view/LocalDeleteModal.java deleted file mode 100644 index a3f541e06..000000000 --- a/app/src/main/java/de/danoeh/antennapod/view/LocalDeleteModal.java +++ /dev/null @@ -1,32 +0,0 @@ -package de.danoeh.antennapod.view; - -import android.content.Context; - -import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import de.danoeh.antennapod.ui.i18n.R; -import de.danoeh.antennapod.model.feed.FeedItem; - -public class LocalDeleteModal { - public static void showLocalFeedDeleteWarningIfNecessary(Context context, Iterable items, - Runnable deleteCommand) { - boolean anyLocalFeed = false; - for (FeedItem item : items) { - if (item.getFeed().isLocalFeed()) { - anyLocalFeed = true; - break; - } - } - - if (!anyLocalFeed) { - deleteCommand.run(); - return; - } - - new MaterialAlertDialogBuilder(context) - .setTitle(R.string.delete_episode_label) - .setMessage(R.string.delete_local_feed_warning_body) - .setPositiveButton(R.string.delete_label, (dialog, which) -> deleteCommand.run()) - .setNegativeButton(R.string.cancel_label, null) - .show(); - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/view/LockableBottomSheetBehavior.java b/app/src/main/java/de/danoeh/antennapod/view/LockableBottomSheetBehavior.java deleted file mode 100644 index 1b96c7c4f..000000000 --- a/app/src/main/java/de/danoeh/antennapod/view/LockableBottomSheetBehavior.java +++ /dev/null @@ -1,86 +0,0 @@ -package de.danoeh.antennapod.view; - -import android.content.Context; -import android.util.AttributeSet; -import android.view.MotionEvent; -import android.view.View; -import androidx.coordinatorlayout.widget.CoordinatorLayout; -import com.google.android.material.bottomsheet.ViewPagerBottomSheetBehavior; - -/** - * Based on https://stackoverflow.com/a/40798214 - */ -public class LockableBottomSheetBehavior extends ViewPagerBottomSheetBehavior { - private boolean isLocked = false; - - public LockableBottomSheetBehavior() {} - - public LockableBottomSheetBehavior(Context context, AttributeSet attrs) { - super(context, attrs); - } - - public void setLocked(boolean locked) { - isLocked = locked; - } - - @Override - public boolean onInterceptTouchEvent(CoordinatorLayout parent, V child, MotionEvent event) { - boolean handled = false; - - if (!isLocked) { - handled = super.onInterceptTouchEvent(parent, child, event); - } - - return handled; - } - - @Override - public boolean onTouchEvent(CoordinatorLayout parent, V child, MotionEvent event) { - boolean handled = false; - - if (!isLocked) { - handled = super.onTouchEvent(parent, child, event); - } - - return handled; - } - - @Override - public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, V child, View directTargetChild, - View target, int axes, int type) { - boolean handled = false; - - if (!isLocked) { - handled = super.onStartNestedScroll(coordinatorLayout, child, directTargetChild, target, axes, type); - } - - return handled; - } - - @Override - public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, V child, View target, - int dx, int dy, int[] consumed, int type) { - if (!isLocked) { - super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type); - } - } - - @Override - public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target, int type) { - if (!isLocked) { - super.onStopNestedScroll(coordinatorLayout, child, target, type); - } - } - - @Override - public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, V child, View target, - float velocityX, float velocityY) { - boolean handled = false; - - if (!isLocked) { - handled = super.onNestedPreFling(coordinatorLayout, child, target, velocityX, velocityY); - } - - return handled; - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/view/NestedScrollableHost.java b/app/src/main/java/de/danoeh/antennapod/view/NestedScrollableHost.java deleted file mode 100644 index 660aa1ea9..000000000 --- a/app/src/main/java/de/danoeh/antennapod/view/NestedScrollableHost.java +++ /dev/null @@ -1,197 +0,0 @@ -/* - * Copyright 2019 The Android Open Source Project - * - * 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. - * - * Source: https://github.com/android/views-widgets-samples/blob/87e58d1/ViewPager2/app/src/main/java/androidx/viewpager2/integration/testapp/NestedScrollableHost.kt - * And modified for our need - */ - -package de.danoeh.antennapod.view; - -import android.content.Context; -import android.content.res.TypedArray; -import android.util.AttributeSet; -import android.view.MotionEvent; -import android.view.View; -import android.view.ViewConfiguration; -import android.view.ViewTreeObserver; -import android.widget.FrameLayout; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.viewpager2.widget.ViewPager2; - -import de.danoeh.antennapod.R; - -import static androidx.viewpager2.widget.ViewPager2.ORIENTATION_HORIZONTAL; -import static androidx.viewpager2.widget.ViewPager2.ORIENTATION_VERTICAL; - - -/** - * Layout to wrap a scrollable component inside a ViewPager2. Provided as a solution to the problem - * where pages of ViewPager2 have nested scrollable elements that scroll in the same direction as - * ViewPager2. The scrollable element needs to be the immediate and only child of this host layout. - * - * This solution has limitations when using multiple levels of nested scrollable elements - * (e.g. a horizontal RecyclerView in a vertical RecyclerView in a horizontal ViewPager2). - */ // KhaledAlharthi/NestedScrollableHost.java -public class NestedScrollableHost extends FrameLayout { - - private ViewPager2 parentViewPager; - private int touchSlop = 0; - private float initialX = 0f; - private float initialY = 0f; - private int preferVertical = 1; - private int preferHorizontal = 1; - private int scrollDirection = 0; - - public NestedScrollableHost(@NonNull Context context) { - super(context); - init(context); - } - - public NestedScrollableHost(@NonNull Context context, @Nullable AttributeSet attrs) { - super(context, attrs); - init(context); - setAttributes(context, attrs); - } - - public NestedScrollableHost(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - init(context); - setAttributes(context, attrs); - } - - public NestedScrollableHost(@NonNull Context context, @Nullable AttributeSet attrs, - int defStyleAttr, int defStyleRes) { - super(context, attrs, defStyleAttr, defStyleRes); - init(context); - setAttributes(context, attrs); - } - - private void setAttributes(@NonNull Context context, @Nullable AttributeSet attrs) { - TypedArray a = context.getTheme().obtainStyledAttributes( - attrs, - R.styleable.NestedScrollableHost, - 0, 0); - - try { - preferHorizontal = a.getInteger(R.styleable.NestedScrollableHost_preferHorizontal, 1); - preferVertical = a.getInteger(R.styleable.NestedScrollableHost_preferVertical, 1); - scrollDirection = a.getInteger(R.styleable.NestedScrollableHost_scrollDirection, 0); - } finally { - a.recycle(); - } - - } - - private void init(Context context) { - touchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); - - - getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { - @Override - public boolean onPreDraw() { - View v = (View) getParent(); - while (v != null && !(v instanceof ViewPager2) || isntSameDirection(v)) { - v = (View) v.getParent(); - } - parentViewPager = (ViewPager2) v; - - getViewTreeObserver().removeOnPreDrawListener(this); - return false; - } - }); - } - - private Boolean isntSameDirection(View v) { - int orientation = 0; - switch (scrollDirection) { - default: - case 0: - return false; - case 1: - orientation = ORIENTATION_VERTICAL; - break; - case 2: - orientation = ORIENTATION_HORIZONTAL; - break; - } - return ((v instanceof ViewPager2) && ((ViewPager2) v).getOrientation() != orientation); - } - - - @Override - public boolean onInterceptTouchEvent(MotionEvent ev) { - handleInterceptTouchEvent(ev); - return super.onInterceptTouchEvent(ev); - } - - - private boolean canChildScroll(int orientation, float delta) { - int direction = (int) -delta; - View child = getChildAt(0); - if (orientation == 0) { - return child.canScrollHorizontally(direction); - } else if (orientation == 1) { - return child.canScrollVertically(direction); - } else { - throw new IllegalArgumentException(); - } - } - - private void handleInterceptTouchEvent(MotionEvent e) { - if (parentViewPager == null) { - return; - } - int orientation = parentViewPager.getOrientation(); - boolean preferedDirection = preferHorizontal + preferVertical > 2; - - // Early return if child can't scroll in same direction as parent and theres no prefered scroll direction - if (!canChildScroll(orientation, -1f) && !canChildScroll(orientation, 1f) && !preferedDirection) { - return; - } - - - if (e.getAction() == MotionEvent.ACTION_DOWN) { - initialX = e.getX(); - initialY = e.getY(); - getParent().requestDisallowInterceptTouchEvent(true); - } else if (e.getAction() == MotionEvent.ACTION_MOVE) { - float dx = e.getX() - initialX; - float dy = e.getY() - initialY; - boolean isVpHorizontal = orientation == ViewPager2.ORIENTATION_HORIZONTAL; - - // assuming ViewPager2 touch-slop is 2x touch-slop of child - float scaledDx = Math.abs(dx) * (isVpHorizontal ? 1f : 0.5f) * preferHorizontal; - float scaledDy = Math.abs(dy) * (isVpHorizontal ? 0.5f : 1f) * preferVertical; - if (scaledDx > touchSlop || scaledDy > touchSlop) { - if (isVpHorizontal == (scaledDy > scaledDx)) { - // Gesture is perpendicular, allow all parents to intercept - getParent().requestDisallowInterceptTouchEvent(preferedDirection); - } else { - // Gesture is parallel, query child if movement in that direction is possible - if (canChildScroll(orientation, isVpHorizontal ? dx : dy)) { - // Child can scroll, disallow all parents to intercept - getParent().requestDisallowInterceptTouchEvent(true); - } else { - // Child cannot scroll, allow all parents to intercept - getParent().requestDisallowInterceptTouchEvent(false); - } - } - } - - } - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/view/NoRelayoutTextView.java b/app/src/main/java/de/danoeh/antennapod/view/NoRelayoutTextView.java deleted file mode 100644 index cbb2ef0af..000000000 --- a/app/src/main/java/de/danoeh/antennapod/view/NoRelayoutTextView.java +++ /dev/null @@ -1,42 +0,0 @@ -package de.danoeh.antennapod.view; - -import android.content.Context; -import android.util.AttributeSet; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.widget.AppCompatTextView; - -public class NoRelayoutTextView extends AppCompatTextView { - private boolean requestLayoutEnabled = false; - private float maxTextLength = 0; - - public NoRelayoutTextView(@NonNull Context context) { - super(context); - } - - public NoRelayoutTextView(@NonNull Context context, @Nullable AttributeSet attrs) { - super(context, attrs); - } - - public NoRelayoutTextView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - } - - @Override - public void requestLayout() { - if (requestLayoutEnabled) { - super.requestLayout(); - } - requestLayoutEnabled = false; - } - - @Override - public void setText(CharSequence text, BufferType type) { - float textLength = getPaint().measureText(text.toString()); - if (textLength > maxTextLength) { - maxTextLength = textLength; - requestLayoutEnabled = true; - } - super.setText(text, type); - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/view/PlayButton.java b/app/src/main/java/de/danoeh/antennapod/view/PlayButton.java deleted file mode 100644 index 04b277fb4..000000000 --- a/app/src/main/java/de/danoeh/antennapod/view/PlayButton.java +++ /dev/null @@ -1,52 +0,0 @@ -package de.danoeh.antennapod.view; - -import android.content.Context; -import android.util.AttributeSet; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.widget.AppCompatImageButton; -import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat; -import de.danoeh.antennapod.R; - -public class PlayButton extends AppCompatImageButton { - private boolean isShowPlay = true; - private boolean isVideoScreen = false; - - public PlayButton(@NonNull Context context) { - super(context); - } - - public PlayButton(@NonNull Context context, @Nullable AttributeSet attrs) { - super(context, attrs); - } - - public PlayButton(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - } - - public void setIsVideoScreen(boolean isVideoScreen) { - this.isVideoScreen = isVideoScreen; - } - - public void setIsShowPlay(boolean showPlay) { - if (this.isShowPlay != showPlay) { - this.isShowPlay = showPlay; - setContentDescription(getContext().getString(showPlay ? R.string.play_label : R.string.pause_label)); - if (isVideoScreen) { - setImageResource(showPlay ? R.drawable.ic_play_video_white : R.drawable.ic_pause_video_white); - } else if (!isShown()) { - setImageResource(showPlay ? R.drawable.ic_play_48dp : R.drawable.ic_pause); - } else if (showPlay) { - AnimatedVectorDrawableCompat drawable = AnimatedVectorDrawableCompat.create( - getContext(), R.drawable.ic_animate_pause_play); - setImageDrawable(drawable); - drawable.start(); - } else { - AnimatedVectorDrawableCompat drawable = AnimatedVectorDrawableCompat.create( - getContext(), R.drawable.ic_animate_play_pause); - setImageDrawable(drawable); - drawable.start(); - } - } - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/view/PlaybackSpeedSeekBar.java b/app/src/main/java/de/danoeh/antennapod/view/PlaybackSpeedSeekBar.java deleted file mode 100644 index 33f0d47b8..000000000 --- a/app/src/main/java/de/danoeh/antennapod/view/PlaybackSpeedSeekBar.java +++ /dev/null @@ -1,76 +0,0 @@ -package de.danoeh.antennapod.view; - -import android.content.Context; -import android.util.AttributeSet; -import android.view.View; -import android.widget.FrameLayout; -import android.widget.SeekBar; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.util.Consumer; -import de.danoeh.antennapod.R; - -public class PlaybackSpeedSeekBar extends FrameLayout { - private SeekBar seekBar; - private Consumer progressChangedListener; - - public PlaybackSpeedSeekBar(@NonNull Context context) { - super(context); - setup(); - } - - public PlaybackSpeedSeekBar(@NonNull Context context, @Nullable AttributeSet attrs) { - super(context, attrs); - setup(); - } - - public PlaybackSpeedSeekBar(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - setup(); - } - - private void setup() { - View.inflate(getContext(), R.layout.playback_speed_seek_bar, this); - seekBar = findViewById(R.id.playback_speed); - findViewById(R.id.butDecSpeed).setOnClickListener(v -> seekBar.setProgress(seekBar.getProgress() - 2)); - findViewById(R.id.butIncSpeed).setOnClickListener(v -> seekBar.setProgress(seekBar.getProgress() + 2)); - - seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { - @Override - public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { - float playbackSpeed = (progress + 10) / 20.0f; - if (progressChangedListener != null) { - progressChangedListener.accept(playbackSpeed); - } - } - - @Override - public void onStartTrackingTouch(SeekBar seekBar) { - } - - @Override - public void onStopTrackingTouch(SeekBar seekBar) { - } - }); - } - - public void updateSpeed(float speedMultiplier) { - seekBar.setProgress(Math.round((20 * speedMultiplier) - 10)); - } - - public void setProgressChangedListener(Consumer progressChangedListener) { - this.progressChangedListener = progressChangedListener; - } - - public float getCurrentSpeed() { - return (seekBar.getProgress() + 10) / 20.0f; - } - - @Override - public void setEnabled(boolean enabled) { - super.setEnabled(enabled); - seekBar.setEnabled(enabled); - findViewById(R.id.butDecSpeed).setEnabled(enabled); - findViewById(R.id.butIncSpeed).setEnabled(enabled); - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/view/ShownotesWebView.java b/app/src/main/java/de/danoeh/antennapod/view/ShownotesWebView.java deleted file mode 100644 index f470b0aac..000000000 --- a/app/src/main/java/de/danoeh/antennapod/view/ShownotesWebView.java +++ /dev/null @@ -1,189 +0,0 @@ -package de.danoeh.antennapod.view; - -import android.content.ClipData; -import android.content.ClipboardManager; -import android.content.Context; -import android.content.Intent; -import android.graphics.Color; -import android.net.Uri; -import android.os.Build; -import android.util.AttributeSet; -import android.util.Log; -import android.view.ContextMenu; -import android.view.Menu; -import android.view.MenuItem; -import android.view.View; -import android.webkit.WebSettings; -import android.webkit.WebView; -import android.webkit.WebViewClient; - -import androidx.core.content.ContextCompat; -import androidx.core.util.Consumer; - -import com.google.android.material.snackbar.Snackbar; - -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.activity.MainActivity; -import de.danoeh.antennapod.core.menuhandler.MenuItemUtils; -import de.danoeh.antennapod.ui.common.Converter; -import de.danoeh.antennapod.core.util.IntentUtils; -import de.danoeh.antennapod.net.common.NetworkUtils; -import de.danoeh.antennapod.core.util.ShareUtils; -import de.danoeh.antennapod.core.util.gui.ShownotesCleaner; - -public class ShownotesWebView extends WebView implements View.OnLongClickListener { - private static final String TAG = "ShownotesWebView"; - - /** - * URL that was selected via long-press. - */ - private String selectedUrl; - private Consumer timecodeSelectedListener; - private Runnable pageFinishedListener; - - public ShownotesWebView(Context context) { - super(context); - setup(); - } - - public ShownotesWebView(Context context, AttributeSet attrs) { - super(context, attrs); - setup(); - } - - public ShownotesWebView(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - setup(); - } - - private void setup() { - setBackgroundColor(Color.TRANSPARENT); - if (!NetworkUtils.networkAvailable()) { - getSettings().setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK); - // Use cached resources, even if they have expired - } - getSettings().setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW); - getSettings().setUseWideViewPort(false); - getSettings().setLoadWithOverviewMode(true); - setOnLongClickListener(this); - - setWebViewClient(new WebViewClient() { - @Override - public boolean shouldOverrideUrlLoading(WebView view, String url) { - if (ShownotesCleaner.isTimecodeLink(url) && timecodeSelectedListener != null) { - timecodeSelectedListener.accept(ShownotesCleaner.getTimecodeLinkTime(url)); - } else { - IntentUtils.openInBrowser(getContext(), url); - } - return true; - } - - @Override - public void onPageFinished(WebView view, String url) { - super.onPageFinished(view, url); - Log.d(TAG, "Page finished"); - if (pageFinishedListener != null) { - pageFinishedListener.run(); - } - } - }); - } - - @Override - public boolean onLongClick(View v) { - WebView.HitTestResult r = getHitTestResult(); - if (r != null && r.getType() == WebView.HitTestResult.SRC_ANCHOR_TYPE) { - Log.d(TAG, "Link of webview was long-pressed. Extra: " + r.getExtra()); - selectedUrl = r.getExtra(); - showContextMenu(); - return true; - } else if (r != null && r.getType() == HitTestResult.EMAIL_TYPE) { - Log.d(TAG, "E-Mail of webview was long-pressed. Extra: " + r.getExtra()); - ClipboardManager clipboardManager = ContextCompat.getSystemService(this.getContext(), - ClipboardManager.class); - if (clipboardManager != null) { - clipboardManager.setPrimaryClip(ClipData.newPlainText("AntennaPod", r.getExtra())); - } - if (Build.VERSION.SDK_INT <= 32 && this.getContext() instanceof MainActivity) { - ((MainActivity) this.getContext()).showSnackbarAbovePlayer( - getResources().getString(R.string.copied_to_clipboard), - Snackbar.LENGTH_SHORT); - } - return true; - } - selectedUrl = null; - return false; - } - - public boolean onContextItemSelected(MenuItem item) { - if (selectedUrl == null) { - return false; - } - - final int itemId = item.getItemId(); - if (itemId == R.id.open_in_browser_item) { - IntentUtils.openInBrowser(getContext(), selectedUrl); - } else if (itemId == R.id.share_url_item) { - ShareUtils.shareLink(getContext(), selectedUrl); - } else if (itemId == R.id.copy_url_item) { - ClipData clipData = ClipData.newPlainText(selectedUrl, selectedUrl); - ClipboardManager cm = (ClipboardManager) getContext() - .getSystemService(Context.CLIPBOARD_SERVICE); - cm.setPrimaryClip(clipData); - if (Build.VERSION.SDK_INT < 32) { - Snackbar s = Snackbar.make(this, R.string.copied_to_clipboard, Snackbar.LENGTH_LONG); - s.getView().setElevation(100); - s.show(); - } - } else if (itemId == R.id.go_to_position_item) { - if (ShownotesCleaner.isTimecodeLink(selectedUrl) && timecodeSelectedListener != null) { - timecodeSelectedListener.accept(ShownotesCleaner.getTimecodeLinkTime(selectedUrl)); - } else { - Log.e(TAG, "Selected go_to_position_item, but URL was no timecode link: " + selectedUrl); - } - } else { - selectedUrl = null; - return false; - } - selectedUrl = null; - return true; - } - - @Override - protected void onCreateContextMenu(ContextMenu menu) { - super.onCreateContextMenu(menu); - if (selectedUrl == null) { - return; - } - - if (ShownotesCleaner.isTimecodeLink(selectedUrl)) { - menu.add(Menu.NONE, R.id.go_to_position_item, Menu.NONE, R.string.go_to_position_label); - menu.setHeaderTitle(Converter.getDurationStringLong(ShownotesCleaner.getTimecodeLinkTime(selectedUrl))); - } else { - Uri uri = Uri.parse(selectedUrl); - final Intent intent = new Intent(Intent.ACTION_VIEW, uri); - if (IntentUtils.isCallable(getContext(), intent)) { - menu.add(Menu.NONE, R.id.open_in_browser_item, Menu.NONE, R.string.open_in_browser_label); - } - menu.add(Menu.NONE, R.id.copy_url_item, Menu.NONE, R.string.copy_url_label); - menu.add(Menu.NONE, R.id.share_url_item, Menu.NONE, R.string.share_url_label); - menu.setHeaderTitle(selectedUrl); - } - MenuItemUtils.setOnClickListeners(menu, this::onContextItemSelected); - } - - public void setTimecodeSelectedListener(Consumer timecodeSelectedListener) { - this.timecodeSelectedListener = timecodeSelectedListener; - } - - public void setPageFinishedListener(Runnable pageFinishedListener) { - this.pageFinishedListener = pageFinishedListener; - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - setMeasuredDimension(Math.max(getMeasuredWidth(), getMinimumWidth()), - Math.max(getMeasuredHeight(), getMinimumHeight())); - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/view/SimpleAdapterDataObserver.java b/app/src/main/java/de/danoeh/antennapod/view/SimpleAdapterDataObserver.java deleted file mode 100644 index 5bd335532..000000000 --- a/app/src/main/java/de/danoeh/antennapod/view/SimpleAdapterDataObserver.java +++ /dev/null @@ -1,41 +0,0 @@ -package de.danoeh.antennapod.view; - -import androidx.annotation.Nullable; -import androidx.recyclerview.widget.RecyclerView; - -/** - * AdapterDataObserver that relays all events to the method anythingChanged(). - */ -public abstract class SimpleAdapterDataObserver extends RecyclerView.AdapterDataObserver { - public abstract void anythingChanged(); - - @Override - public void onChanged() { - anythingChanged(); - } - - @Override - public void onItemRangeChanged(int positionStart, int itemCount) { - anythingChanged(); - } - - @Override - public void onItemRangeChanged(int positionStart, int itemCount, @Nullable Object payload) { - anythingChanged(); - } - - @Override - public void onItemRangeInserted(int positionStart, int itemCount) { - anythingChanged(); - } - - @Override - public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) { - anythingChanged(); - } - - @Override - public void onItemRangeRemoved(int positionStart, int itemCount) { - anythingChanged(); - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/view/ToolbarIconTintManager.java b/app/src/main/java/de/danoeh/antennapod/view/ToolbarIconTintManager.java deleted file mode 100644 index 67c2e2555..000000000 --- a/app/src/main/java/de/danoeh/antennapod/view/ToolbarIconTintManager.java +++ /dev/null @@ -1,59 +0,0 @@ -package de.danoeh.antennapod.view; - -import android.content.Context; -import android.graphics.PorterDuff.Mode; -import android.graphics.PorterDuffColorFilter; -import android.graphics.drawable.Drawable; -import android.view.ContextThemeWrapper; -import com.google.android.material.appbar.MaterialToolbar; -import com.google.android.material.appbar.AppBarLayout; -import com.google.android.material.appbar.CollapsingToolbarLayout; -import de.danoeh.antennapod.R; - -public abstract class ToolbarIconTintManager implements AppBarLayout.OnOffsetChangedListener { - private final Context context; - private final CollapsingToolbarLayout collapsingToolbar; - private final MaterialToolbar toolbar; - private boolean isTinted = false; - - public ToolbarIconTintManager(Context context, MaterialToolbar toolbar, CollapsingToolbarLayout collapsingToolbar) { - this.context = context; - this.collapsingToolbar = collapsingToolbar; - this.toolbar = toolbar; - } - - @Override - public void onOffsetChanged(AppBarLayout appBarLayout, int offset) { - boolean tint = (collapsingToolbar.getHeight() + offset) > (2 * collapsingToolbar.getMinimumHeight()); - if (isTinted != tint) { - isTinted = tint; - updateTint(); - } - } - - public void updateTint() { - if (isTinted) { - doTint(new ContextThemeWrapper(context, R.style.Theme_AntennaPod_Dark)); - safeSetColorFilter(toolbar.getNavigationIcon(), new PorterDuffColorFilter(0xffffffff, Mode.SRC_ATOP)); - safeSetColorFilter(toolbar.getOverflowIcon(), new PorterDuffColorFilter(0xffffffff, Mode.SRC_ATOP)); - safeSetColorFilter(toolbar.getCollapseIcon(), new PorterDuffColorFilter(0xffffffff, Mode.SRC_ATOP)); - } else { - doTint(context); - safeSetColorFilter(toolbar.getNavigationIcon(), null); - safeSetColorFilter(toolbar.getOverflowIcon(), null); - safeSetColorFilter(toolbar.getCollapseIcon(), null); - } - } - - private void safeSetColorFilter(Drawable icon, PorterDuffColorFilter filter) { - if (icon != null) { - icon.setColorFilter(filter); - } - } - - /** - * View expansion was changed. Icons need to be tinted - * @param themedContext ContextThemeWrapper with dark theme while expanded - */ - protected abstract void doTint(Context themedContext); -} diff --git a/app/src/main/java/de/danoeh/antennapod/view/viewholder/DownloadLogItemViewHolder.java b/app/src/main/java/de/danoeh/antennapod/view/viewholder/DownloadLogItemViewHolder.java deleted file mode 100644 index ffb679830..000000000 --- a/app/src/main/java/de/danoeh/antennapod/view/viewholder/DownloadLogItemViewHolder.java +++ /dev/null @@ -1,40 +0,0 @@ -package de.danoeh.antennapod.view.viewholder; - -import android.content.Context; -import android.os.Build; -import android.text.Layout; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.TextView; -import androidx.recyclerview.widget.RecyclerView; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.ui.common.CircularProgressBar; - -public class DownloadLogItemViewHolder extends RecyclerView.ViewHolder { - public final View secondaryActionButton; - public final ImageView secondaryActionIcon; - public final CircularProgressBar secondaryActionProgress; - public final ImageView icon; - public final TextView title; - public final TextView status; - public final TextView reason; - public final TextView tapForDetails; - - public DownloadLogItemViewHolder(Context context, ViewGroup parent) { - super(LayoutInflater.from(context).inflate(R.layout.downloadlog_item, parent, false)); - status = itemView.findViewById(R.id.status); - icon = itemView.findViewById(R.id.icon); - reason = itemView.findViewById(R.id.txtvReason); - tapForDetails = itemView.findViewById(R.id.txtvTapForDetails); - secondaryActionButton = itemView.findViewById(R.id.secondaryActionButton); - secondaryActionProgress = itemView.findViewById(R.id.secondaryActionProgress); - secondaryActionIcon = itemView.findViewById(R.id.secondaryActionIcon); - title = itemView.findViewById(R.id.txtvTitle); - if (Build.VERSION.SDK_INT >= 23) { - title.setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_FULL); - } - itemView.setTag(this); - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/view/viewholder/EpisodeItemViewHolder.java b/app/src/main/java/de/danoeh/antennapod/view/viewholder/EpisodeItemViewHolder.java deleted file mode 100644 index fd547aa09..000000000 --- a/app/src/main/java/de/danoeh/antennapod/view/viewholder/EpisodeItemViewHolder.java +++ /dev/null @@ -1,280 +0,0 @@ -package de.danoeh.antennapod.view.viewholder; - -import android.os.Build; -import android.text.Layout; -import android.text.format.Formatter; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.ProgressBar; -import android.widget.TextView; - -import androidx.cardview.widget.CardView; -import androidx.recyclerview.widget.RecyclerView; - -import com.google.android.material.elevation.SurfaceColors; - -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.activity.MainActivity; -import de.danoeh.antennapod.adapter.CoverLoader; -import de.danoeh.antennapod.adapter.actionbutton.ItemActionButton; -import de.danoeh.antennapod.playback.service.PlaybackStatus; -import de.danoeh.antennapod.core.util.download.MediaSizeLoader; -import de.danoeh.antennapod.event.playback.PlaybackPositionEvent; -import de.danoeh.antennapod.ui.common.DateFormatter; -import de.danoeh.antennapod.model.feed.FeedItem; -import de.danoeh.antennapod.model.feed.FeedMedia; -import de.danoeh.antennapod.model.playback.MediaType; -import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterface; -import de.danoeh.antennapod.storage.preferences.UserPreferences; -import de.danoeh.antennapod.ui.common.Converter; -import de.danoeh.antennapod.net.common.NetworkUtils; -import de.danoeh.antennapod.model.playback.Playable; -import de.danoeh.antennapod.ui.common.CircularProgressBar; -import de.danoeh.antennapod.ui.common.ThemeUtils; -import de.danoeh.antennapod.ui.episodes.ImageResourceUtils; - -/** - * Holds the view which shows FeedItems. - */ -public class EpisodeItemViewHolder extends RecyclerView.ViewHolder { - private static final String TAG = "EpisodeItemViewHolder"; - - private final View container; - public final ImageView dragHandle; - private final TextView placeholder; - private final ImageView cover; - private final TextView title; - private final TextView pubDate; - private final TextView position; - private final TextView duration; - private final TextView size; - public final ImageView isInbox; - public final ImageView isInQueue; - private final ImageView isVideo; - public final ImageView isFavorite; - private final ProgressBar progressBar; - public final View secondaryActionButton; - public final ImageView secondaryActionIcon; - private final CircularProgressBar secondaryActionProgress; - private final TextView separatorIcons; - private final View leftPadding; - public final CardView coverHolder; - - private final MainActivity activity; - private FeedItem item; - - public EpisodeItemViewHolder(MainActivity activity, ViewGroup parent) { - super(LayoutInflater.from(activity).inflate(R.layout.feeditemlist_item, parent, false)); - this.activity = activity; - container = itemView.findViewById(R.id.container); - dragHandle = itemView.findViewById(R.id.drag_handle); - placeholder = itemView.findViewById(R.id.txtvPlaceholder); - cover = itemView.findViewById(R.id.imgvCover); - title = itemView.findViewById(R.id.txtvTitle); - if (Build.VERSION.SDK_INT >= 23) { - title.setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_FULL); - } - pubDate = itemView.findViewById(R.id.txtvPubDate); - position = itemView.findViewById(R.id.txtvPosition); - duration = itemView.findViewById(R.id.txtvDuration); - progressBar = itemView.findViewById(R.id.progressBar); - isInQueue = itemView.findViewById(R.id.ivInPlaylist); - isVideo = itemView.findViewById(R.id.ivIsVideo); - isInbox = itemView.findViewById(R.id.statusInbox); - isFavorite = itemView.findViewById(R.id.isFavorite); - size = itemView.findViewById(R.id.size); - separatorIcons = itemView.findViewById(R.id.separatorIcons); - secondaryActionProgress = itemView.findViewById(R.id.secondaryActionProgress); - secondaryActionButton = itemView.findViewById(R.id.secondaryActionButton); - secondaryActionIcon = itemView.findViewById(R.id.secondaryActionIcon); - coverHolder = itemView.findViewById(R.id.coverHolder); - leftPadding = itemView.findViewById(R.id.left_padding); - itemView.setTag(this); - } - - public void bind(FeedItem item) { - this.item = item; - placeholder.setText(item.getFeed().getTitle()); - title.setText(item.getTitle()); - if (item.isPlayed()) { - leftPadding.setContentDescription(item.getTitle() + ". " + activity.getString(R.string.is_played)); - } else { - leftPadding.setContentDescription(item.getTitle()); - } - pubDate.setText(DateFormatter.formatAbbrev(activity, item.getPubDate())); - pubDate.setContentDescription(DateFormatter.formatForAccessibility(item.getPubDate())); - isInbox.setVisibility(item.isNew() ? View.VISIBLE : View.GONE); - isFavorite.setVisibility(item.isTagged(FeedItem.TAG_FAVORITE) ? View.VISIBLE : View.GONE); - isInQueue.setVisibility(item.isTagged(FeedItem.TAG_QUEUE) ? View.VISIBLE : View.GONE); - container.setAlpha(item.isPlayed() ? 0.5f : 1.0f); - - ItemActionButton actionButton = ItemActionButton.forItem(item); - actionButton.configure(secondaryActionButton, secondaryActionIcon, activity); - secondaryActionButton.setFocusable(false); - - if (item.getMedia() != null) { - bind(item.getMedia()); - } else { - secondaryActionProgress.setPercentage(0, item); - secondaryActionProgress.setIndeterminate(false); - isVideo.setVisibility(View.GONE); - progressBar.setVisibility(View.GONE); - duration.setVisibility(View.GONE); - position.setVisibility(View.GONE); - itemView.setBackgroundResource(ThemeUtils.getDrawableFromAttr(activity, R.attr.selectableItemBackground)); - } - - if (coverHolder.getVisibility() == View.VISIBLE) { - new CoverLoader() - .withUri(ImageResourceUtils.getEpisodeListImageLocation(item)) - .withFallbackUri(item.getFeed().getImageUrl()) - .withPlaceholderView(placeholder) - .withCoverView(cover) - .load(); - } - } - - private void bind(FeedMedia media) { - isVideo.setVisibility(media.getMediaType() == MediaType.VIDEO ? View.VISIBLE : View.GONE); - duration.setVisibility(media.getDuration() > 0 ? View.VISIBLE : View.GONE); - - if (PlaybackStatus.isCurrentlyPlaying(media)) { - float density = activity.getResources().getDisplayMetrics().density; - itemView.setBackgroundColor(SurfaceColors.getColorForElevation(activity, 8 * density)); - } else { - itemView.setBackgroundResource(ThemeUtils.getDrawableFromAttr(activity, R.attr.selectableItemBackground)); - } - - if (DownloadServiceInterface.get().isDownloadingEpisode(media.getDownloadUrl())) { - float percent = 0.01f * DownloadServiceInterface.get().getProgress(media.getDownloadUrl()); - secondaryActionProgress.setPercentage(Math.max(percent, 0.01f), item); - secondaryActionProgress.setIndeterminate( - DownloadServiceInterface.get().isEpisodeQueued(media.getDownloadUrl())); - } else if (media.isDownloaded()) { - secondaryActionProgress.setPercentage(1, item); // Do not animate 100% -> 0% - secondaryActionProgress.setIndeterminate(false); - } else { - secondaryActionProgress.setPercentage(0, item); // Animate X% -> 0% - secondaryActionProgress.setIndeterminate(false); - } - - duration.setText(Converter.getDurationStringLong(media.getDuration())); - duration.setContentDescription(activity.getString(R.string.chapter_duration, - Converter.getDurationStringLocalized(activity, media.getDuration()))); - if (PlaybackStatus.isPlaying(item.getMedia()) || item.isInProgress()) { - int progress = (int) (100.0 * media.getPosition() / media.getDuration()); - int remainingTime = Math.max(media.getDuration() - media.getPosition(), 0); - progressBar.setProgress(progress); - position.setText(Converter.getDurationStringLong(media.getPosition())); - position.setContentDescription(activity.getString(R.string.position, - Converter.getDurationStringLocalized(activity, media.getPosition()))); - progressBar.setVisibility(View.VISIBLE); - position.setVisibility(View.VISIBLE); - if (UserPreferences.shouldShowRemainingTime()) { - duration.setText(((remainingTime > 0) ? "-" : "") + Converter.getDurationStringLong(remainingTime)); - duration.setContentDescription(activity.getString(R.string.chapter_duration, - Converter.getDurationStringLocalized(activity, (media.getDuration() - media.getPosition())))); - } - } else { - progressBar.setVisibility(View.GONE); - position.setVisibility(View.GONE); - } - - if (media.getSize() > 0) { - size.setText(Formatter.formatShortFileSize(activity, media.getSize())); - } else if (NetworkUtils.isEpisodeHeadDownloadAllowed() && !media.checkedOnSizeButUnknown()) { - size.setText(""); - MediaSizeLoader.getFeedMediaSizeObservable(media).subscribe( - sizeValue -> { - if (sizeValue > 0) { - size.setText(Formatter.formatShortFileSize(activity, sizeValue)); - } else { - size.setText(""); - } - }, error -> { - size.setText(""); - Log.e(TAG, Log.getStackTraceString(error)); - }); - } else { - size.setText(""); - } - } - - public void bindDummy() { - item = new FeedItem(); - container.setAlpha(0.1f); - secondaryActionIcon.setImageDrawable(null); - isInbox.setVisibility(View.VISIBLE); - isVideo.setVisibility(View.GONE); - isFavorite.setVisibility(View.GONE); - isInQueue.setVisibility(View.GONE); - title.setText("███████"); - pubDate.setText("████"); - duration.setText("████"); - secondaryActionProgress.setPercentage(0, null); - secondaryActionProgress.setIndeterminate(false); - progressBar.setVisibility(View.GONE); - position.setVisibility(View.GONE); - dragHandle.setVisibility(View.GONE); - size.setText(""); - itemView.setBackgroundResource(ThemeUtils.getDrawableFromAttr(activity, R.attr.selectableItemBackground)); - placeholder.setText(""); - if (coverHolder.getVisibility() == View.VISIBLE) { - new CoverLoader() - .withResource(R.color.medium_gray) - .withPlaceholderView(placeholder) - .withCoverView(cover) - .load(); - } - } - - private void updateDuration(PlaybackPositionEvent event) { - if (getFeedItem().getMedia() != null) { - getFeedItem().getMedia().setPosition(event.getPosition()); - getFeedItem().getMedia().setDuration(event.getDuration()); - } - int currentPosition = event.getPosition(); - int timeDuration = event.getDuration(); - int remainingTime = Math.max(timeDuration - currentPosition, 0); - Log.d(TAG, "currentPosition " + Converter.getDurationStringLong(currentPosition)); - if (currentPosition == Playable.INVALID_TIME || timeDuration == Playable.INVALID_TIME) { - Log.w(TAG, "Could not react to position observer update because of invalid time"); - return; - } - if (UserPreferences.shouldShowRemainingTime()) { - duration.setText(((remainingTime > 0) ? "-" : "") + Converter.getDurationStringLong(remainingTime)); - } else { - duration.setText(Converter.getDurationStringLong(timeDuration)); - } - } - - public FeedItem getFeedItem() { - return item; - } - - public boolean isCurrentlyPlayingItem() { - return item.getMedia() != null && PlaybackStatus.isCurrentlyPlaying(item.getMedia()); - } - - public void notifyPlaybackPositionUpdated(PlaybackPositionEvent event) { - progressBar.setProgress((int) (100.0 * event.getPosition() / event.getDuration())); - position.setText(Converter.getDurationStringLong(event.getPosition())); - updateDuration(event); - duration.setVisibility(View.VISIBLE); // Even if the duration was previously unknown, it is now known - } - - /** - * Hides the separator dot between icons and text if there are no icons. - */ - public void hideSeparatorIfNecessary() { - boolean hasIcons = isInbox.getVisibility() == View.VISIBLE - || isInQueue.getVisibility() == View.VISIBLE - || isVideo.getVisibility() == View.VISIBLE - || isFavorite.getVisibility() == View.VISIBLE - || isInbox.getVisibility() == View.VISIBLE; - separatorIcons.setVisibility(hasIcons ? View.VISIBLE : View.GONE); - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/view/viewholder/HorizontalItemViewHolder.java b/app/src/main/java/de/danoeh/antennapod/view/viewholder/HorizontalItemViewHolder.java deleted file mode 100644 index ee642c041..000000000 --- a/app/src/main/java/de/danoeh/antennapod/view/viewholder/HorizontalItemViewHolder.java +++ /dev/null @@ -1,130 +0,0 @@ -package de.danoeh.antennapod.view.viewholder; - -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.ProgressBar; -import android.widget.TextView; -import androidx.cardview.widget.CardView; -import androidx.recyclerview.widget.RecyclerView; -import com.google.android.material.elevation.SurfaceColors; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.activity.MainActivity; -import de.danoeh.antennapod.adapter.CoverLoader; -import de.danoeh.antennapod.adapter.actionbutton.ItemActionButton; -import de.danoeh.antennapod.ui.common.DateFormatter; -import de.danoeh.antennapod.playback.service.PlaybackStatus; -import de.danoeh.antennapod.event.playback.PlaybackPositionEvent; -import de.danoeh.antennapod.model.feed.FeedItem; -import de.danoeh.antennapod.model.feed.FeedMedia; -import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterface; -import de.danoeh.antennapod.ui.common.CircularProgressBar; -import de.danoeh.antennapod.ui.common.SquareImageView; -import de.danoeh.antennapod.ui.common.ThemeUtils; -import de.danoeh.antennapod.ui.episodes.ImageResourceUtils; - -public class HorizontalItemViewHolder extends RecyclerView.ViewHolder { - public final CardView card; - public final ImageView secondaryActionIcon; - private final SquareImageView cover; - private final TextView title; - private final TextView date; - private final ProgressBar progressBar; - private final CircularProgressBar circularProgressBar; - private final View progressBarReplacementSpacer; - - private final MainActivity activity; - private FeedItem item; - - public HorizontalItemViewHolder(MainActivity activity, ViewGroup parent) { - super(LayoutInflater.from(activity).inflate(R.layout.horizontal_itemlist_item, parent, false)); - this.activity = activity; - - card = itemView.findViewById(R.id.card); - cover = itemView.findViewById(R.id.cover); - title = itemView.findViewById(R.id.titleLabel); - date = itemView.findViewById(R.id.dateLabel); - secondaryActionIcon = itemView.findViewById(R.id.secondaryActionIcon); - circularProgressBar = itemView.findViewById(R.id.circularProgressBar); - progressBar = itemView.findViewById(R.id.progressBar); - progressBarReplacementSpacer = itemView.findViewById(R.id.progressBarReplacementSpacer); - itemView.setTag(this); - } - - public void bind(FeedItem item) { - this.item = item; - - card.setAlpha(1.0f); - float density = activity.getResources().getDisplayMetrics().density; - card.setCardBackgroundColor(SurfaceColors.getColorForElevation(activity, 1 * density)); - new CoverLoader() - .withUri(ImageResourceUtils.getEpisodeListImageLocation(item)) - .withFallbackUri(item.getFeed().getImageUrl()) - .withCoverView(cover) - .load(); - title.setText(item.getTitle()); - date.setText(DateFormatter.formatAbbrev(activity, item.getPubDate())); - date.setContentDescription(DateFormatter.formatForAccessibility(item.getPubDate())); - ItemActionButton actionButton = ItemActionButton.forItem(item); - actionButton.configure(secondaryActionIcon, secondaryActionIcon, activity); - secondaryActionIcon.setFocusable(false); - - FeedMedia media = item.getMedia(); - if (media == null) { - circularProgressBar.setPercentage(0, item); - setProgressBar(false, 0); - } else { - if (PlaybackStatus.isCurrentlyPlaying(media)) { - card.setCardBackgroundColor(ThemeUtils.getColorFromAttr(activity, R.attr.colorSurfaceVariant)); - } - - if (item.getMedia().getDuration() > 0 && item.getMedia().getPosition() > 0) { - setProgressBar(true, 100.0f * item.getMedia().getPosition() / item.getMedia().getDuration()); - } else { - setProgressBar(false, 0); - } - - if (DownloadServiceInterface.get().isDownloadingEpisode(media.getDownloadUrl())) { - float percent = 0.01f * DownloadServiceInterface.get().getProgress(media.getDownloadUrl()); - circularProgressBar.setPercentage(Math.max(percent, 0.01f), item); - circularProgressBar.setIndeterminate( - DownloadServiceInterface.get().isEpisodeQueued(media.getDownloadUrl())); - } else if (media.isDownloaded()) { - circularProgressBar.setPercentage(1, item); // Do not animate 100% -> 0% - circularProgressBar.setIndeterminate(false); - } else { - circularProgressBar.setPercentage(0, item); // Animate X% -> 0% - circularProgressBar.setIndeterminate(false); - } - } - } - - public void bindDummy() { - card.setAlpha(0.1f); - new CoverLoader() - .withResource(android.R.color.transparent) - .withCoverView(cover) - .load(); - title.setText("████ █████"); - date.setText("███"); - secondaryActionIcon.setImageDrawable(null); - circularProgressBar.setPercentage(0, null); - circularProgressBar.setIndeterminate(false); - setProgressBar(true, 50); - } - - public boolean isCurrentlyPlayingItem() { - return item != null && item.getMedia() != null && PlaybackStatus.isCurrentlyPlaying(item.getMedia()); - } - - public void notifyPlaybackPositionUpdated(PlaybackPositionEvent event) { - setProgressBar(true, 100.0f * event.getPosition() / event.getDuration()); - } - - private void setProgressBar(boolean visible, float progress) { - progressBar.setVisibility(visible ? ViewGroup.VISIBLE : ViewGroup.GONE); - progressBarReplacementSpacer.setVisibility(visible ? View.GONE : ViewGroup.VISIBLE); - progressBar.setProgress(Math.max(5, (int) progress)); // otherwise invisible below the edge radius - } -} diff --git a/app/src/main/res/layout-sw720dp/main.xml b/app/src/main/res/layout-sw720dp/main.xml index d2b231992..e8edc260f 100644 --- a/app/src/main/res/layout-sw720dp/main.xml +++ b/app/src/main/res/layout-sw720dp/main.xml @@ -40,7 +40,7 @@ android:background="?android:attr/colorBackground" android:elevation="8dp" android:visibility="gone" - app:layout_behavior="de.danoeh.antennapod.view.LockableBottomSheetBehavior" /> + app:layout_behavior="de.danoeh.antennapod.ui.view.LockableBottomSheetBehavior" /> diff --git a/app/src/main/res/layout/audioplayer_fragment.xml b/app/src/main/res/layout/audioplayer_fragment.xml index 1a6794db9..5f4775141 100644 --- a/app/src/main/res/layout/audioplayer_fragment.xml +++ b/app/src/main/res/layout/audioplayer_fragment.xml @@ -86,7 +86,7 @@ android:layoutDirection="ltr" android:orientation="vertical"> - - - - - - - - - - + - + android:nestedScrollingEnabled="true" + app:preferVertical="10"> - + android:layout_height="match_parent" /> - \ No newline at end of file + diff --git a/app/src/main/res/layout/main.xml b/app/src/main/res/layout/main.xml index 3a368e90e..e205707d5 100644 --- a/app/src/main/res/layout/main.xml +++ b/app/src/main/res/layout/main.xml @@ -32,7 +32,7 @@ android:background="?android:attr/colorBackground" android:elevation="8dp" android:visibility="gone" - app:layout_behavior="de.danoeh.antennapod.view.LockableBottomSheetBehavior" /> + app:layout_behavior="de.danoeh.antennapod.ui.view.LockableBottomSheetBehavior" /> diff --git a/app/src/main/res/layout/playback_speed_feed_setting_dialog.xml b/app/src/main/res/layout/playback_speed_feed_setting_dialog.xml index 50eb4f84f..317f86093 100644 --- a/app/src/main/res/layout/playback_speed_feed_setting_dialog.xml +++ b/app/src/main/res/layout/playback_speed_feed_setting_dialog.xml @@ -19,7 +19,7 @@ android:orientation="horizontal" android:gravity="center_vertical"> - - - - - - + android:orientation="vertical"> - - + android:layout_gravity="center" /> - "; + ShownotesCleaner t = new ShownotesCleaner(context, shownotes, Integer.MAX_VALUE); + String res = t.processShownotes(); + checkLinkCorrect(res, new long[]{time}, new String[]{timeStr}); + } + + @Test + public void testProcessShownotesAddTimecodeHhmmssMoreThen24HoursNoChapters() { + final String timeStr = "25:00:00"; + final long time = 25 * 60 * 60 * 1000; + + String shownotes = "

Some test text with a timecode " + timeStr + " here.

"; + ShownotesCleaner t = new ShownotesCleaner(context, shownotes, Integer.MAX_VALUE); + String res = t.processShownotes(); + checkLinkCorrect(res, new long[]{time}, new String[]{timeStr}); + } + + @Test + public void testProcessShownotesAddTimecodeHhmmNoChapters() { + final String timeStr = "10:11"; + final long time = 3600 * 1000 * 10 + 60 * 1000 * 11; + + String shownotes = "

Some test text with a timecode " + timeStr + " here.

"; + ShownotesCleaner t = new ShownotesCleaner(context, shownotes, Integer.MAX_VALUE); + String res = t.processShownotes(); + checkLinkCorrect(res, new long[]{time}, new String[]{timeStr}); + } + + @Test + public void testProcessShownotesAddTimecodeMmssNoChapters() { + final String timeStr = "10:11"; + final long time = 10 * 60 * 1000 + 11 * 1000; + + String shownotes = "

Some test text with a timecode " + timeStr + " here.

"; + ShownotesCleaner t = new ShownotesCleaner(context, shownotes, 11 * 60 * 1000); + String res = t.processShownotes(); + checkLinkCorrect(res, new long[]{time}, new String[]{timeStr}); + } + + @Test + public void testProcessShownotesAddTimecodeHmmssNoChapters() { + final String timeStr = "2:11:12"; + final long time = 2 * 60 * 60 * 1000 + 11 * 60 * 1000 + 12 * 1000; + + String shownotes = "

Some test text with a timecode " + timeStr + " here.

"; + ShownotesCleaner t = new ShownotesCleaner(context, shownotes, Integer.MAX_VALUE); + String res = t.processShownotes(); + checkLinkCorrect(res, new long[]{time}, new String[]{timeStr}); + } + + @Test + public void testProcessShownotesAddTimecodeMssNoChapters() { + final String timeStr = "1:12"; + final long time = 60 * 1000 + 12 * 1000; + + String shownotes = "

Some test text with a timecode " + timeStr + " here.

"; + ShownotesCleaner t = new ShownotesCleaner(context, shownotes, 2 * 60 * 1000); + String res = t.processShownotes(); + checkLinkCorrect(res, new long[]{time}, new String[]{timeStr}); + } + + @Test + public void testProcessShownotesAddNoTimecodeDuration() { + final String timeStr = "2:11:12"; + final int time = 2 * 60 * 60 * 1000 + 11 * 60 * 1000 + 12 * 1000; + + String shownotes = "

Some test text with a timecode " + timeStr + " here.

"; + ShownotesCleaner t = new ShownotesCleaner(context, shownotes, time); + String res = t.processShownotes(); + Document d = Jsoup.parse(res); + assertEquals("Should not parse time codes that equal duration", 0, d.body().getElementsByTag("a").size()); + } + + @Test + public void testProcessShownotesAddTimecodeMultipleFormatsNoChapters() { + final String[] timeStrings = new String[]{ "10:12", "1:10:12" }; + + String shownotes = "

Some test text with a timecode " + timeStrings[0] + + " here. Hey look another one " + timeStrings[1] + " here!

"; + ShownotesCleaner t = new ShownotesCleaner(context, shownotes, 2 * 60 * 60 * 1000); + String res = t.processShownotes(); + checkLinkCorrect(res, new long[]{10 * 60 * 1000 + 12 * 1000, + 60 * 60 * 1000 + 10 * 60 * 1000 + 12 * 1000}, timeStrings); + } + + @Test + public void testProcessShownotesAddTimecodeMultipleShortFormatNoChapters() { + + // One of these timecodes fits as HH:MM and one does not so both should be parsed as MM:SS. + final String[] timeStrings = new String[]{ "10:12", "2:12" }; + + String shownotes = "

Some test text with a timecode " + timeStrings[0] + + " here. Hey look another one " + timeStrings[1] + " here!

"; + ShownotesCleaner t = new ShownotesCleaner(context, shownotes, 3 * 60 * 60 * 1000); + String res = t.processShownotes(); + checkLinkCorrect(res, new long[]{10 * 60 * 1000 + 12 * 1000, 2 * 60 * 1000 + 12 * 1000}, timeStrings); + } + + @Test + public void testProcessShownotesAddTimecodeParentheses() { + final String timeStr = "10:11"; + final long time = 3600 * 1000 * 10 + 60 * 1000 * 11; + + String shownotes = "

Some test text with a timecode (" + timeStr + ") here.

"; + ShownotesCleaner t = new ShownotesCleaner(context, shownotes, Integer.MAX_VALUE); + String res = t.processShownotes(); + checkLinkCorrect(res, new long[]{time}, new String[]{timeStr}); + } + + @Test + public void testProcessShownotesAddTimecodeBrackets() { + final String timeStr = "10:11"; + final long time = 3600 * 1000 * 10 + 60 * 1000 * 11; + + String shownotes = "

Some test text with a timecode [" + timeStr + "] here.

"; + ShownotesCleaner t = new ShownotesCleaner(context, shownotes, Integer.MAX_VALUE); + String res = t.processShownotes(); + checkLinkCorrect(res, new long[]{time}, new String[]{timeStr}); + } + + @Test + public void testProcessShownotesAddTimecodeAngleBrackets() { + final String timeStr = "10:11"; + final long time = 3600 * 1000 * 10 + 60 * 1000 * 11; + + String shownotes = "

Some test text with a timecode <" + timeStr + "> here.

"; + ShownotesCleaner t = new ShownotesCleaner(context, shownotes, Integer.MAX_VALUE); + String res = t.processShownotes(); + checkLinkCorrect(res, new long[]{time}, new String[]{timeStr}); + } + + @Test + public void testProcessShownotesAndInvalidTimecode() { + final String[] timeStrs = new String[] {"2:1", "0:0", "000", "00", "00:000"}; + + StringBuilder shownotes = new StringBuilder("

Some test text with timecodes "); + for (String timeStr : timeStrs) { + shownotes.append(timeStr).append(" "); + } + shownotes.append("here.

"); + + ShownotesCleaner t = new ShownotesCleaner(context, shownotes.toString(), Integer.MAX_VALUE); + String res = t.processShownotes(); + checkLinkCorrect(res, new long[0], new String[0]); + } + + private void checkLinkCorrect(String res, long[] timecodes, String[] timecodeStr) { + assertNotNull(res); + Document d = Jsoup.parse(res); + Elements links = d.body().getElementsByTag("a"); + int countedLinks = 0; + for (Element link : links) { + String href = link.attributes().get("href"); + String text = link.text(); + if (href.startsWith("antennapod://")) { + assertTrue(href.endsWith(String.valueOf(timecodes[countedLinks]))); + assertEquals(timecodeStr[countedLinks], text); + countedLinks++; + assertTrue("Contains too many links: " + countedLinks + " > " + + timecodes.length, countedLinks <= timecodes.length); + } + } + assertEquals(timecodes.length, countedLinks); + } + + @Test + public void testIsTimecodeLink() { + assertFalse(ShownotesCleaner.isTimecodeLink(null)); + assertFalse(ShownotesCleaner.isTimecodeLink("http://antennapod/timecode/123123")); + assertFalse(ShownotesCleaner.isTimecodeLink("antennapod://timecode/")); + assertFalse(ShownotesCleaner.isTimecodeLink("antennapod://123123")); + assertFalse(ShownotesCleaner.isTimecodeLink("antennapod://timecode/123123a")); + assertTrue(ShownotesCleaner.isTimecodeLink("antennapod://timecode/123")); + assertTrue(ShownotesCleaner.isTimecodeLink("antennapod://timecode/1")); + } + + @Test + public void testGetTimecodeLinkTime() { + assertEquals(-1, ShownotesCleaner.getTimecodeLinkTime(null)); + assertEquals(-1, ShownotesCleaner.getTimecodeLinkTime("http://timecode/123")); + assertEquals(123, ShownotesCleaner.getTimecodeLinkTime("antennapod://timecode/123")); + } + + @Test + public void testCleanupColors() { + final String input = "/* /* */ .foo { text-decoration: underline;color:#f00;font-weight:bold;}" + + "#bar { text-decoration: underline;color:#f00;font-weight:bold; }" + + "div {text-decoration: underline; color /* */ : /* */ #f00 /* */; font-weight:bold; }" + + "#foobar { /* color: */ text-decoration: underline; /* color: */font-weight:bold /* ; */; }" + + "baz { background-color:#f00;border: solid 2px;border-color:#0f0;text-decoration: underline; }"; + final String expected = " .foo { text-decoration: underline;font-weight:bold;}" + + "#bar { text-decoration: underline;font-weight:bold; }" + + "div {text-decoration: underline; font-weight:bold; }" + + "#foobar { text-decoration: underline; font-weight:bold ; }" + + "baz { background-color:#f00;border: solid 2px;border-color:#0f0;text-decoration: underline; }"; + assertEquals(expected, ShownotesCleaner.cleanStyleTag(input)); + } +} diff --git a/app/src/test/java/de/danoeh/antennapod/ui/screen/onlinefeedview/FeedDiscovererTest.java b/app/src/test/java/de/danoeh/antennapod/ui/screen/onlinefeedview/FeedDiscovererTest.java new file mode 100644 index 000000000..ee9f8a6d5 --- /dev/null +++ b/app/src/test/java/de/danoeh/antennapod/ui/screen/onlinefeedview/FeedDiscovererTest.java @@ -0,0 +1,128 @@ +package de.danoeh.antennapod.ui.screen.onlinefeedview; + +import androidx.test.platform.app.InstrumentationRegistry; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +import java.io.File; +import java.io.FileOutputStream; +import java.nio.charset.StandardCharsets; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +/** + * Test class for {@link FeedDiscoverer} + */ +@RunWith(RobolectricTestRunner.class) +public class FeedDiscovererTest { + + private FeedDiscoverer fd; + + private File testDir; + + @Before + public void setUp() { + fd = new FeedDiscoverer(); + testDir = new File(InstrumentationRegistry + .getInstrumentation().getTargetContext().getFilesDir(), "FeedDiscovererTest"); + //noinspection ResultOfMethodCallIgnored + testDir.mkdir(); + assertTrue(testDir.exists()); + } + + @After + public void tearDown() throws Exception { + FileUtils.deleteDirectory(testDir); + } + + @SuppressWarnings("SameParameterValue") + private String createTestHtmlString(String rel, String type, String href, String title) { + return String.format("Test", + rel, type, href, title); + } + + private String createTestHtmlString(String rel, String type, String href) { + return String.format("Test", + rel, type, href); + } + + private void checkFindUrls(boolean isAlternate, boolean isRss, boolean withTitle, boolean isAbsolute, boolean fromString) throws Exception { + final String title = "Test title"; + final String hrefAbs = "http://example.com/feed"; + final String hrefRel = "/feed"; + final String base = "http://example.com"; + + final String rel = (isAlternate) ? "alternate" : "feed"; + final String type = (isRss) ? "application/rss+xml" : "application/atom+xml"; + final String href = (isAbsolute) ? hrefAbs : hrefRel; + + Map res; + String html = (withTitle) ? createTestHtmlString(rel, type, href, title) + : createTestHtmlString(rel, type, href); + if (fromString) { + res = fd.findLinks(html, base); + } else { + File testFile = new File(testDir, "feed"); + FileOutputStream out = new FileOutputStream(testFile); + IOUtils.write(html, out, StandardCharsets.UTF_8); + out.close(); + res = fd.findLinks(testFile, base); + } + + assertNotNull(res); + assertEquals(1, res.size()); + for (String key : res.keySet()) { + assertEquals(hrefAbs, key); + } + assertTrue(res.containsKey(hrefAbs)); + if (withTitle) { + assertEquals(title, res.get(hrefAbs)); + } else { + assertEquals(href, res.get(hrefAbs)); + } + } + + @Test + public void testAlternateRSSWithTitleAbsolute() throws Exception { + checkFindUrls(true, true, true, true, true); + } + + @Test + public void testAlternateRSSWithTitleRelative() throws Exception { + checkFindUrls(true, true, true, false, true); + } + + @Test + public void testAlternateRSSNoTitleAbsolute() throws Exception { + checkFindUrls(true, true, false, true, true); + } + + @Test + public void testAlternateRSSNoTitleRelative() throws Exception { + checkFindUrls(true, true, false, false, true); + } + + @Test + public void testAlternateAtomWithTitleAbsolute() throws Exception { + checkFindUrls(true, false, true, true, true); + } + + @Test + public void testFeedAtomWithTitleAbsolute() throws Exception { + checkFindUrls(false, false, true, true, true); + } + + @Test + public void testAlternateRSSWithTitleAbsoluteFromFile() throws Exception { + checkFindUrls(true, true, true, true, false); + } +} diff --git a/config/spotbugs/exclude.xml b/config/spotbugs/exclude.xml index 5eedd1775..35f0881b7 100644 --- a/config/spotbugs/exclude.xml +++ b/config/spotbugs/exclude.xml @@ -2,7 +2,7 @@ - + @@ -34,7 +34,7 @@ - + @@ -58,7 +58,7 @@ - + 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 deleted file mode 100644 index b964c7508..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/dialog/ConfirmationDialog.java +++ /dev/null @@ -1,56 +0,0 @@ -package de.danoeh.antennapod.core.dialog; - -import android.content.Context; -import android.content.DialogInterface; -import androidx.appcompat.app.AlertDialog; -import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import android.util.Log; - -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.class.getSimpleName(); - - private final Context context; - private final int titleId; - private final String message; - - private int positiveText; - - public ConfirmationDialog(Context context, int titleId, int messageId) { - this(context, titleId, context.getString(messageId)); - } - - public ConfirmationDialog(Context context, int titleId, String message) { - this.context = context; - this.titleId = titleId; - this.message = message; - } - - private void onCancelButtonPressed(DialogInterface dialog) { - Log.d(TAG, "Dialog was cancelled"); - dialog.dismiss(); - } - - public void setPositiveText(int id) { - this.positiveText = id; - } - - public abstract void onConfirmButtonPressed(DialogInterface dialog); - - public final AlertDialog createNewDialog() { - MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(context); - builder.setTitle(titleId); - builder.setMessage(message); - builder.setPositiveButton(positiveText != 0 ? positiveText : R.string.confirm_label, - (dialog, which) -> onConfirmButtonPressed(dialog)); - builder.setNegativeButton(R.string.cancel_label, (dialog, which) -> onCancelButtonPressed(dialog)); - builder.setOnCancelListener(ConfirmationDialog.this::onCancelButtonPressed); - return builder.create(); - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/ChapterMerger.java b/core/src/main/java/de/danoeh/antennapod/core/feed/ChapterMerger.java deleted file mode 100644 index 9bbab7251..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/feed/ChapterMerger.java +++ /dev/null @@ -1,70 +0,0 @@ -package de.danoeh.antennapod.core.feed; - -import android.text.TextUtils; -import android.util.Log; -import androidx.annotation.Nullable; -import de.danoeh.antennapod.model.feed.Chapter; - -import java.util.List; - -public class ChapterMerger { - private static final String TAG = "ChapterMerger"; - - private ChapterMerger() { - - } - - /** - * This method might modify the input data. - */ - @Nullable - public static List merge(@Nullable List chapters1, @Nullable List chapters2) { - Log.d(TAG, "Merging chapters"); - if (chapters1 == null) { - return chapters2; - } else if (chapters2 == null) { - return chapters1; - } else if (chapters2.size() > chapters1.size()) { - return chapters2; - } else if (chapters2.size() < chapters1.size()) { - return chapters1; - } else { - // Merge chapter lists of same length. Store in chapters2 array. - // In case the lists can not be merged, return chapters1 array. - for (int i = 0; i < chapters2.size(); i++) { - Chapter chapterTarget = chapters2.get(i); - Chapter chapterOther = chapters1.get(i); - - if (Math.abs(chapterTarget.getStart() - chapterOther.getStart()) > 1000) { - Log.e(TAG, "Chapter lists are too different. Cancelling merge."); - return score(chapters1) > score(chapters2) ? chapters1 : chapters2; - } - - if (TextUtils.isEmpty(chapterTarget.getImageUrl())) { - chapterTarget.setImageUrl(chapterOther.getImageUrl()); - } - if (TextUtils.isEmpty(chapterTarget.getLink())) { - chapterTarget.setLink(chapterOther.getLink()); - } - if (TextUtils.isEmpty(chapterTarget.getTitle())) { - chapterTarget.setTitle(chapterOther.getTitle()); - } - } - return chapters2; - } - } - - /** - * Tries to give a score that can determine which list of chapters a user might want to see. - */ - private static int score(List chapters) { - int score = 0; - for (Chapter chapter : chapters) { - score = score - + (TextUtils.isEmpty(chapter.getTitle()) ? 0 : 1) - + (TextUtils.isEmpty(chapter.getLink()) ? 0 : 1) - + (TextUtils.isEmpty(chapter.getImageUrl()) ? 0 : 1); - } - return score; - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItemFilterGroup.java b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItemFilterGroup.java deleted file mode 100644 index fbdf6b3a9..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItemFilterGroup.java +++ /dev/null @@ -1,37 +0,0 @@ -package de.danoeh.antennapod.core.feed; - -import de.danoeh.antennapod.core.R; -import de.danoeh.antennapod.model.feed.FeedItemFilter; - -public enum FeedItemFilterGroup { - PLAYED(new ItemProperties(R.string.hide_played_episodes_label, FeedItemFilter.PLAYED), - new ItemProperties(R.string.not_played, FeedItemFilter.UNPLAYED)), - PAUSED(new ItemProperties(R.string.hide_paused_episodes_label, FeedItemFilter.PAUSED), - new ItemProperties(R.string.not_paused, FeedItemFilter.NOT_PAUSED)), - FAVORITE(new ItemProperties(R.string.hide_is_favorite_label, FeedItemFilter.IS_FAVORITE), - new ItemProperties(R.string.not_favorite, FeedItemFilter.NOT_FAVORITE)), - MEDIA(new ItemProperties(R.string.has_media, FeedItemFilter.HAS_MEDIA), - new ItemProperties(R.string.no_media, FeedItemFilter.NO_MEDIA)), - QUEUED(new ItemProperties(R.string.queued_label, FeedItemFilter.QUEUED), - new ItemProperties(R.string.not_queued_label, FeedItemFilter.NOT_QUEUED)), - DOWNLOADED(new ItemProperties(R.string.hide_downloaded_episodes_label, FeedItemFilter.DOWNLOADED), - new ItemProperties(R.string.hide_not_downloaded_episodes_label, FeedItemFilter.NOT_DOWNLOADED)); - - public final ItemProperties[] values; - - FeedItemFilterGroup(ItemProperties... values) { - this.values = values; - } - - public static class ItemProperties { - - public final int displayName; - public final String filterId; - - public ItemProperties(int displayName, String filterId) { - this.displayName = displayName; - this.filterId = filterId; - } - - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/SubscriptionsFilterGroup.java b/core/src/main/java/de/danoeh/antennapod/core/feed/SubscriptionsFilterGroup.java deleted file mode 100644 index cea5d96ef..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/feed/SubscriptionsFilterGroup.java +++ /dev/null @@ -1,33 +0,0 @@ -package de.danoeh.antennapod.core.feed; - -import de.danoeh.antennapod.core.R; - -public enum SubscriptionsFilterGroup { - COUNTER_GREATER_ZERO(new ItemProperties(R.string.subscriptions_counter_greater_zero, "counter_greater_zero")), - AUTO_DOWNLOAD(new ItemProperties(R.string.auto_downloaded, "enabled_auto_download"), - new ItemProperties(R.string.not_auto_downloaded, "disabled_auto_download")), - UPDATED(new ItemProperties(R.string.kept_updated, "enabled_updates"), - new ItemProperties(R.string.not_kept_updated, "disabled_updates")), - NEW_EPISODE_NOTIFICATION(new ItemProperties(R.string.new_episode_notification_enabled, - "episode_notification_enabled"), - new ItemProperties(R.string.new_episode_notification_disabled, "episode_notification_disabled")); - - - public final ItemProperties[] values; - - SubscriptionsFilterGroup(ItemProperties... values) { - this.values = values; - } - - public static class ItemProperties { - - public final int displayName; - public final String filterId; - - public ItemProperties(int displayName, String filterId) { - this.displayName = displayName; - this.filterId = filterId; - } - - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/menuhandler/MenuItemUtils.java b/core/src/main/java/de/danoeh/antennapod/core/menuhandler/MenuItemUtils.java deleted file mode 100644 index 829add126..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/menuhandler/MenuItemUtils.java +++ /dev/null @@ -1,28 +0,0 @@ -package de.danoeh.antennapod.core.menuhandler; - -import android.view.Menu; -import android.view.MenuItem; - -/** - * Utilities for menu items - */ -public class MenuItemUtils { - - /** - * When pressing a context menu item, Android calls onContextItemSelected - * for ALL fragments in arbitrary order, not just for the fragment that the - * context menu was created from. This assigns the listener to every menu item, - * so that the correct fragment is always called first and can consume the click. - *

- * Note that Android still calls the onContextItemSelected methods of all fragments - * when the passed listener returns false. - */ - public static void setOnClickListeners(Menu menu, MenuItem.OnMenuItemClickListener listener) { - for (int i = 0; i < menu.size(); i++) { - if (menu.getItem(i).getSubMenu() != null) { - setOnClickListeners(menu.getItem(i).getSubMenu(), listener); - } - menu.getItem(i).setOnMenuItemClickListener(listener); - } - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/AutomaticDownloadAlgorithm.java b/core/src/main/java/de/danoeh/antennapod/core/storage/AutomaticDownloadAlgorithm.java index 93f7d578a..dbcc899ba 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/AutomaticDownloadAlgorithm.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/AutomaticDownloadAlgorithm.java @@ -1,6 +1,9 @@ package de.danoeh.antennapod.core.storage; import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.BatteryManager; import android.util.Log; import java.util.ArrayList; @@ -15,7 +18,6 @@ import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterfa import de.danoeh.antennapod.storage.database.DBReader; import de.danoeh.antennapod.storage.preferences.UserPreferences; import de.danoeh.antennapod.net.common.NetworkUtils; -import de.danoeh.antennapod.core.util.PowerUtils; /** * Implements the automatic download algorithm used by AntennaPod. This class assumes that @@ -42,8 +44,7 @@ public class AutomaticDownloadAlgorithm { && UserPreferences.isEnableAutodownload(); // true if we should auto download based on power status - boolean powerShouldAutoDl = PowerUtils.deviceCharging(context) - || UserPreferences.isEnableAutodownloadOnBattery(); + boolean powerShouldAutoDl = deviceCharging(context) || UserPreferences.isEnableAutodownloadOnBattery(); // we should only auto download if both network AND power are happy if (networkShouldAutoDl && powerShouldAutoDl) { @@ -103,4 +104,18 @@ public class AutomaticDownloadAlgorithm { } }; } + + /** + * @return true if the device is charging + */ + public static boolean deviceCharging(Context context) { + // from http://developer.android.com/training/monitoring-device-state/battery-monitoring.html + IntentFilter intentFilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED); + Intent batteryStatus = context.registerReceiver(null, intentFilter); + + int status = batteryStatus.getIntExtra(BatteryManager.EXTRA_STATUS, -1); + return (status == BatteryManager.BATTERY_STATUS_CHARGING + || status == BatteryManager.BATTERY_STATUS_FULL); + + } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/ChapterMerger.java b/core/src/main/java/de/danoeh/antennapod/core/util/ChapterMerger.java new file mode 100644 index 000000000..794b2322d --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/ChapterMerger.java @@ -0,0 +1,70 @@ +package de.danoeh.antennapod.core.util; + +import android.text.TextUtils; +import android.util.Log; +import androidx.annotation.Nullable; +import de.danoeh.antennapod.model.feed.Chapter; + +import java.util.List; + +public class ChapterMerger { + private static final String TAG = "ChapterMerger"; + + private ChapterMerger() { + + } + + /** + * This method might modify the input data. + */ + @Nullable + public static List merge(@Nullable List chapters1, @Nullable List chapters2) { + Log.d(TAG, "Merging chapters"); + if (chapters1 == null) { + return chapters2; + } else if (chapters2 == null) { + return chapters1; + } else if (chapters2.size() > chapters1.size()) { + return chapters2; + } else if (chapters2.size() < chapters1.size()) { + return chapters1; + } else { + // Merge chapter lists of same length. Store in chapters2 array. + // In case the lists can not be merged, return chapters1 array. + for (int i = 0; i < chapters2.size(); i++) { + Chapter chapterTarget = chapters2.get(i); + Chapter chapterOther = chapters1.get(i); + + if (Math.abs(chapterTarget.getStart() - chapterOther.getStart()) > 1000) { + Log.e(TAG, "Chapter lists are too different. Cancelling merge."); + return score(chapters1) > score(chapters2) ? chapters1 : chapters2; + } + + if (TextUtils.isEmpty(chapterTarget.getImageUrl())) { + chapterTarget.setImageUrl(chapterOther.getImageUrl()); + } + if (TextUtils.isEmpty(chapterTarget.getLink())) { + chapterTarget.setLink(chapterOther.getLink()); + } + if (TextUtils.isEmpty(chapterTarget.getTitle())) { + chapterTarget.setTitle(chapterOther.getTitle()); + } + } + return chapters2; + } + } + + /** + * Tries to give a score that can determine which list of chapters a user might want to see. + */ + private static int score(List chapters) { + int score = 0; + for (Chapter chapter : chapters) { + score = score + + (TextUtils.isEmpty(chapter.getTitle()) ? 0 : 1) + + (TextUtils.isEmpty(chapter.getLink()) ? 0 : 1) + + (TextUtils.isEmpty(chapter.getImageUrl()) ? 0 : 1); + } + return score; + } +} 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 index 1af81802b..b32b05236 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/ChapterUtils.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/ChapterUtils.java @@ -7,7 +7,6 @@ import android.text.TextUtils; import android.util.Log; import androidx.annotation.NonNull; import de.danoeh.antennapod.model.feed.Chapter; -import de.danoeh.antennapod.core.feed.ChapterMerger; import de.danoeh.antennapod.model.feed.FeedMedia; import de.danoeh.antennapod.net.common.AntennapodHttpClient; import de.danoeh.antennapod.storage.database.DBReader; diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/ConfirmationDialog.java b/core/src/main/java/de/danoeh/antennapod/core/util/ConfirmationDialog.java new file mode 100644 index 000000000..ff5e56f5d --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/ConfirmationDialog.java @@ -0,0 +1,56 @@ +package de.danoeh.antennapod.core.util; + +import android.content.Context; +import android.content.DialogInterface; +import androidx.appcompat.app.AlertDialog; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import android.util.Log; + +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.class.getSimpleName(); + + private final Context context; + private final int titleId; + private final String message; + + private int positiveText; + + public ConfirmationDialog(Context context, int titleId, int messageId) { + this(context, titleId, context.getString(messageId)); + } + + public ConfirmationDialog(Context context, int titleId, String message) { + this.context = context; + this.titleId = titleId; + this.message = message; + } + + private void onCancelButtonPressed(DialogInterface dialog) { + Log.d(TAG, "Dialog was cancelled"); + dialog.dismiss(); + } + + public void setPositiveText(int id) { + this.positiveText = id; + } + + public abstract void onConfirmButtonPressed(DialogInterface dialog); + + public final AlertDialog createNewDialog() { + MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(context); + builder.setTitle(titleId); + builder.setMessage(message); + builder.setPositiveButton(positiveText != 0 ? positiveText : R.string.confirm_label, + (dialog, which) -> onConfirmButtonPressed(dialog)); + builder.setNegativeButton(R.string.cancel_label, (dialog, which) -> onCancelButtonPressed(dialog)); + builder.setOnCancelListener(ConfirmationDialog.this::onCancelButtonPressed); + return builder.create(); + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/DownloadErrorLabel.java b/core/src/main/java/de/danoeh/antennapod/core/util/DownloadErrorLabel.java deleted file mode 100644 index 3d2558a9f..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/util/DownloadErrorLabel.java +++ /dev/null @@ -1,46 +0,0 @@ -package de.danoeh.antennapod.core.util; - -import androidx.annotation.StringRes; -import de.danoeh.antennapod.core.BuildConfig; -import de.danoeh.antennapod.core.R; -import de.danoeh.antennapod.model.download.DownloadError; - -/** - * Provides user-visible labels for download errors. - */ -public class DownloadErrorLabel { - - @StringRes - public static int from(DownloadError error) { - switch (error) { - case SUCCESS: return R.string.download_successful; - case ERROR_PARSER_EXCEPTION: return R.string.download_error_parser_exception; - case ERROR_UNSUPPORTED_TYPE: return R.string.download_error_unsupported_type; - case ERROR_CONNECTION_ERROR: return R.string.download_error_connection_error; - case ERROR_MALFORMED_URL: return R.string.download_error_error_unknown; - case ERROR_IO_ERROR: return R.string.download_error_io_error; - case ERROR_FILE_EXISTS: return R.string.download_error_error_unknown; - case ERROR_DOWNLOAD_CANCELLED: return R.string.download_canceled_msg; - case ERROR_DEVICE_NOT_FOUND: return R.string.download_error_device_not_found; - case ERROR_HTTP_DATA_ERROR: return R.string.download_error_http_data_error; - case ERROR_NOT_ENOUGH_SPACE: return R.string.download_error_insufficient_space; - case ERROR_UNKNOWN_HOST: return R.string.download_error_unknown_host; - case ERROR_REQUEST_ERROR: return R.string.download_error_request_error; - case ERROR_DB_ACCESS_ERROR: return R.string.download_error_db_access; - case ERROR_UNAUTHORIZED: return R.string.download_error_unauthorized; - case ERROR_FILE_TYPE: return R.string.download_error_file_type_type; - case ERROR_FORBIDDEN: return R.string.download_error_forbidden; - case ERROR_IO_WRONG_SIZE: return R.string.download_error_wrong_size; - case ERROR_IO_BLOCKED: return R.string.download_error_blocked; - case ERROR_UNSUPPORTED_TYPE_HTML: return R.string.download_error_unsupported_type_html; - case ERROR_NOT_FOUND: return R.string.download_error_not_found; - case ERROR_CERTIFICATE: return R.string.download_error_certificate; - case ERROR_PARSER_EXCEPTION_DUPLICATE: return R.string.download_error_parser_exception; - default: - if (BuildConfig.DEBUG) { - throw new IllegalArgumentException("No mapping from download error to label"); - } - return R.string.download_error_error_unknown; - } - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/PowerUtils.java b/core/src/main/java/de/danoeh/antennapod/core/util/PowerUtils.java deleted file mode 100644 index a1fadb4dc..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/util/PowerUtils.java +++ /dev/null @@ -1,30 +0,0 @@ -package de.danoeh.antennapod.core.util; - -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.os.BatteryManager; - -/** - * Created by Tom on 1/5/15. - */ -public class PowerUtils { - - private PowerUtils() { - - } - - /** - * @return true if the device is charging - */ - public static boolean deviceCharging(Context context) { - // from http://developer.android.com/training/monitoring-device-state/battery-monitoring.html - IntentFilter iFilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED); - Intent batteryStatus = context.registerReceiver(null, iFilter); - - int status = batteryStatus.getIntExtra(BatteryManager.EXTRA_STATUS, -1); - return (status == BatteryManager.BATTERY_STATUS_CHARGING || - status == BatteryManager.BATTERY_STATUS_FULL); - - } -} 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 deleted file mode 100644 index 316771123..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/util/ShareUtils.java +++ /dev/null @@ -1,91 +0,0 @@ -package de.danoeh.antennapod.core.util; - -import android.content.Context; -import android.content.Intent; -import android.net.Uri; -import android.util.Log; - -import androidx.annotation.NonNull; -import androidx.core.app.ShareCompat; -import androidx.core.content.FileProvider; - -import de.danoeh.antennapod.ui.common.Converter; -import java.io.File; -import java.net.URLEncoder; - -import de.danoeh.antennapod.core.R; -import de.danoeh.antennapod.model.feed.Feed; -import de.danoeh.antennapod.model.feed.FeedItem; -import de.danoeh.antennapod.model.feed.FeedMedia; - -/** Utility methods for sharing data */ -public class ShareUtils { - private static final String TAG = "ShareUtils"; - - private ShareUtils() { - } - - public static void shareLink(@NonNull Context context, @NonNull String text) { - Intent intent = new ShareCompat.IntentBuilder(context) - .setType("text/plain") - .setText(text) - .setChooserTitle(R.string.share_url_label) - .createChooserIntent(); - context.startActivity(intent); - } - - public static void shareFeedLink(Context context, Feed feed) { - String text = feed.getTitle() - + "\n\n" - + "https://antennapod.org/deeplink/subscribe/?url=" - + URLEncoder.encode(feed.getDownloadUrl()) - + "&title=" - + URLEncoder.encode(feed.getTitle()); - shareLink(context, text); - } - - public static boolean hasLinkToShare(FeedItem item) { - return FeedItemUtil.getLinkWithFallback(item) != null; - } - - public static void shareMediaDownloadLink(Context context, FeedMedia media) { - shareLink(context, media.getDownloadUrl()); - } - - public static void shareFeedItemLinkWithDownloadLink(Context context, FeedItem item, boolean withPosition) { - String text = item.getFeed().getTitle() + ": " + item.getTitle(); - int pos = 0; - if (item.getMedia() != null && withPosition) { - text += "\n" + context.getResources().getString(R.string.share_starting_position_label) + ": "; - pos = item.getMedia().getPosition(); - text += Converter.getDurationStringLong(pos); - } - - if (hasLinkToShare(item)) { - text += "\n\n" + context.getResources().getString(R.string.share_dialog_episode_website_label) + ": "; - text += FeedItemUtil.getLinkWithFallback(item); - } - - if (item.getMedia() != null && item.getMedia().getDownloadUrl() != null) { - text += "\n\n" + context.getResources().getString(R.string.share_dialog_media_file_label) + ": "; - text += item.getMedia().getDownloadUrl(); - if (withPosition) { - text += "#t=" + pos / 1000; - } - } - shareLink(context, text); - } - - public static void shareFeedItemFile(Context context, FeedMedia media) { - Uri fileUri = FileProvider.getUriForFile(context, context.getString(R.string.provider_authority), - new File(media.getLocalFileUrl())); - - new ShareCompat.IntentBuilder(context) - .setType(media.getMimeType()) - .addStream(fileUri) - .setChooserTitle(R.string.share_file_label) - .startChooser(); - - Log.e(TAG, "shareFeedItemFile called"); - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/download/MediaSizeLoader.java b/core/src/main/java/de/danoeh/antennapod/core/util/download/MediaSizeLoader.java deleted file mode 100644 index 779f3b947..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/util/download/MediaSizeLoader.java +++ /dev/null @@ -1,78 +0,0 @@ -package de.danoeh.antennapod.core.util.download; - -import android.text.TextUtils; -import de.danoeh.antennapod.net.common.AntennapodHttpClient; -import de.danoeh.antennapod.storage.database.DBWriter; -import de.danoeh.antennapod.net.common.NetworkUtils; -import de.danoeh.antennapod.model.feed.FeedMedia; -import io.reactivex.Single; -import io.reactivex.SingleOnSubscribe; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.schedulers.Schedulers; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import android.util.Log; -import okhttp3.Response; - -import java.io.File; -import java.io.IOException; - -public abstract class MediaSizeLoader { - private static final String TAG = "MediaSizeLoader"; - - public static Single getFeedMediaSizeObservable(FeedMedia media) { - return Single.create((SingleOnSubscribe) emitter -> { - if (!NetworkUtils.isEpisodeHeadDownloadAllowed()) { - emitter.onSuccess(0L); - return; - } - long size = Integer.MIN_VALUE; - if (media.isDownloaded()) { - File mediaFile = new File(media.getLocalFileUrl()); - if (mediaFile.exists()) { - size = mediaFile.length(); - } - } else if (!media.checkedOnSizeButUnknown()) { - // only query the network if we haven't already checked - - String url = media.getDownloadUrl(); - if (TextUtils.isEmpty(url)) { - emitter.onSuccess(0L); - return; - } - - OkHttpClient client = AntennapodHttpClient.getHttpClient(); - Request.Builder httpReq = new Request.Builder() - .url(url) - .header("Accept-Encoding", "identity") - .head(); - try { - Response response = client.newCall(httpReq.build()).execute(); - if (response.isSuccessful()) { - String contentLength = response.header("Content-Length"); - try { - size = Integer.parseInt(contentLength); - } catch (NumberFormatException e) { - Log.e(TAG, Log.getStackTraceString(e)); - } - } - } catch (IOException e) { - emitter.onSuccess(0L); - Log.e(TAG, Log.getStackTraceString(e)); - return; // better luck next time - } - } - Log.d(TAG, "new size: " + size); - if (size <= 0) { - // they didn't tell us the size, but we don't want to keep querying on it - media.setCheckedOnSizeButUnknown(); - } else { - media.setSize(size); - } - emitter.onSuccess(size); - DBWriter.setFeedMedia(media); - }) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()); - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/download/NetworkConnectionChangeHandler.java b/core/src/main/java/de/danoeh/antennapod/core/util/download/NetworkConnectionChangeHandler.java deleted file mode 100644 index 79c6e76e1..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/util/download/NetworkConnectionChangeHandler.java +++ /dev/null @@ -1,29 +0,0 @@ -package de.danoeh.antennapod.core.util.download; - -import android.content.Context; -import android.util.Log; -import de.danoeh.antennapod.net.download.serviceinterface.AutoDownloadManager; -import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterface; -import de.danoeh.antennapod.net.common.NetworkUtils; - -public abstract class NetworkConnectionChangeHandler { - private static final String TAG = "NetConnChangeHandler"; - private static Context context; - - public static void init(Context context) { - NetworkConnectionChangeHandler.context = context; - } - - public static void networkChangedDetected() { - if (NetworkUtils.isAutoDownloadAllowed()) { - Log.d(TAG, "auto-dl network available, starting auto-download"); - AutoDownloadManager.getInstance().autodownloadUndownloadedItems(context); - } else { // if new network is Wi-Fi, finish ongoing downloads, - // otherwise cancel all downloads - if (NetworkUtils.isNetworkRestricted()) { - Log.i(TAG, "Device is no longer connected to Wi-Fi. Cancelling ongoing downloads"); - DownloadServiceInterface.get().cancelAll(context); - } - } - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/gui/MoreContentListFooterUtil.java b/core/src/main/java/de/danoeh/antennapod/core/util/gui/MoreContentListFooterUtil.java deleted file mode 100644 index 6e5c3e84b..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/util/gui/MoreContentListFooterUtil.java +++ /dev/null @@ -1,53 +0,0 @@ -package de.danoeh.antennapod.core.util.gui; - -import android.view.View; -import android.widget.ImageView; -import android.widget.ProgressBar; - -import de.danoeh.antennapod.core.R; - -/** - * Utility methods for the more_content_list_footer layout. - */ -public class MoreContentListFooterUtil { - - private final View root; - - private boolean loading; - - private Listener listener; - - public MoreContentListFooterUtil(View root) { - this.root = root; - root.setOnClickListener(v -> { - if (listener != null && !loading) { - listener.onClick(); - } - }); - } - - public void setLoadingState(boolean newState) { - final ImageView imageView = root.findViewById(R.id.imgExpand); - final ProgressBar progressBar = root.findViewById(R.id.progBar); - if (newState) { - imageView.setVisibility(View.GONE); - progressBar.setVisibility(View.VISIBLE); - } else { - imageView.setVisibility(View.VISIBLE); - progressBar.setVisibility(View.GONE); - } - loading = newState; - } - - public void setClickListener(Listener l) { - listener = l; - } - - public interface Listener { - void onClick(); - } - - public View getRoot() { - return root; - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/gui/PictureInPictureUtil.java b/core/src/main/java/de/danoeh/antennapod/core/util/gui/PictureInPictureUtil.java deleted file mode 100644 index f763653a1..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/util/gui/PictureInPictureUtil.java +++ /dev/null @@ -1,27 +0,0 @@ -package de.danoeh.antennapod.core.util.gui; - -import android.app.Activity; -import android.content.pm.PackageManager; -import android.os.Build; - -public class PictureInPictureUtil { - private PictureInPictureUtil() { - } - - public static boolean supportsPictureInPicture(Activity activity) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - PackageManager packageManager = activity.getPackageManager(); - return packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE); - } else { - return false; - } - } - - public static boolean isInPictureInPictureMode(Activity activity) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && supportsPictureInPicture(activity)) { - return activity.isInPictureInPictureMode(); - } else { - return false; - } - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/gui/ShownotesCleaner.java b/core/src/main/java/de/danoeh/antennapod/core/util/gui/ShownotesCleaner.java deleted file mode 100644 index 7bf9257a1..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/util/gui/ShownotesCleaner.java +++ /dev/null @@ -1,208 +0,0 @@ -package de.danoeh.antennapod.core.util.gui; - -import android.content.Context; -import android.content.res.TypedArray; -import android.graphics.Color; -import androidx.annotation.ColorInt; -import androidx.annotation.NonNull; -import android.text.TextUtils; -import android.util.Log; -import android.util.TypedValue; - -import androidx.annotation.Nullable; -import org.apache.commons.io.IOUtils; -import org.jsoup.Jsoup; -import org.jsoup.nodes.Document; -import org.jsoup.nodes.Element; -import org.jsoup.select.Elements; - -import java.io.IOException; -import java.io.InputStream; -import java.util.Locale; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import de.danoeh.antennapod.core.R; -import de.danoeh.antennapod.ui.common.Converter; - -/** - * Cleans up and prepares shownotes: - * - Guesses time stamps to make them clickable - * - Removes some formatting - */ -public class ShownotesCleaner { - private static final String TAG = "Timeline"; - - 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((\\d+):)?(\\d+):(\\d{2})\\b"); - private static final Pattern LINE_BREAK_REGEX = Pattern.compile("
"); - private static final String CSS_COLOR = "(?<=(\\s|;|^))color\\s*:([^;])*;"; - private static final String CSS_COMMENT = "/\\*.*?\\*/"; - - private final String rawShownotes; - private final String noShownotesLabel; - private final int playableDuration; - private final String webviewStyle; - - public ShownotesCleaner(Context context, @Nullable String rawShownotes, int playableDuration) { - this.rawShownotes = rawShownotes; - - noShownotesLabel = context.getString(R.string.no_shownotes_label); - this.playableDuration = playableDuration; - final String colorPrimary = colorToHtml(context, android.R.attr.textColorPrimary); - final String colorAccent = colorToHtml(context, R.attr.colorAccent); - final int margin = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8, - context.getResources().getDisplayMetrics()); - String styleString = ""; - try { - InputStream templateStream = context.getAssets().open("shownotes-style.css"); - styleString = IOUtils.toString(templateStream, "UTF-8"); - } catch (IOException e) { - e.printStackTrace(); - } - webviewStyle = String.format(Locale.US, styleString, colorPrimary, colorAccent, - margin, margin, margin, margin); - } - - private String colorToHtml(Context context, int colorAttr) { - TypedArray res = context.getTheme().obtainStyledAttributes(new int[]{colorAttr}); - @ColorInt int col = res.getColor(0, 0); - final String color = "rgba(" + Color.red(col) + "," + Color.green(col) + "," - + Color.blue(col) + "," + (Color.alpha(col) / 255.0) + ")"; - res.recycle(); - return color; - } - - /** - * 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. - * - * @return The processed HTML string. - */ - @NonNull - public String processShownotes() { - String shownotes = rawShownotes; - - if (TextUtils.isEmpty(shownotes)) { - Log.d(TAG, "shownotesProvider contained no shownotes. Returning 'no shownotes' message"); - shownotes = "

" + noShownotesLabel + "

"; - } - - // replace ASCII line breaks with HTML ones if shownotes don't contain HTML line breaks already - if (!LINE_BREAK_REGEX.matcher(shownotes).find() && !shownotes.contains("

")) { - shownotes = shownotes.replace("\n", "
"); - } - - Document document = Jsoup.parse(shownotes); - cleanCss(document); - document.head().appendElement("style").attr("type", "text/css").text(webviewStyle); - addTimecodes(document); - 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.parseInt(m.group(1)); - } - } catch (NumberFormatException e) { - e.printStackTrace(); - } - } - return -1; - } - - private void addTimecodes(Document document) { - Elements elementsWithTimeCodes = document.body().getElementsMatchingOwnText(TIMECODE_REGEX); - Log.d(TAG, "Recognized " + elementsWithTimeCodes.size() + " timecodes"); - - if (elementsWithTimeCodes.size() == 0) { - // No elements with timecodes - return; - } - boolean useHourFormat = true; - - if (playableDuration != Integer.MAX_VALUE) { - - // We need to decide if we are going to treat short timecodes as HH:MM or MM:SS. To do - // so we will parse all the short timecodes and see if they fit in the duration. If one - // does not we will use MM:SS, otherwise all will be parsed as HH:MM. - for (Element element : elementsWithTimeCodes) { - Matcher matcherForElement = TIMECODE_REGEX.matcher(element.html()); - while (matcherForElement.find()) { - - // We only want short timecodes right now. - if (matcherForElement.group(1) == null) { - int time = Converter.durationStringShortToMs(matcherForElement.group(0), true); - - // If the parsed timecode is greater then the duration then we know we need to - // use the minute format so we are done. - if (time > playableDuration) { - useHourFormat = false; - break; - } - } - } - - if (!useHourFormat) { - break; - } - } - } - - for (Element element : elementsWithTimeCodes) { - - Matcher matcherForElement = TIMECODE_REGEX.matcher(element.html()); - StringBuffer buffer = new StringBuffer(); - - while (matcherForElement.find()) { - String group = matcherForElement.group(0); - - int time = matcherForElement.group(1) != null - ? Converter.durationStringLongToMs(group) - : Converter.durationStringShortToMs(group, useHourFormat); - - String replacementText = group; - if (time < playableDuration) { - replacementText = String.format(Locale.US, TIMECODE_LINK, time, group); - } - - matcherForElement.appendReplacement(buffer, replacementText); - } - - matcherForElement.appendTail(buffer); - element.html(buffer.toString()); - } - } - - private void cleanCss(Document document) { - for (Element element : document.getAllElements()) { - if (element.hasAttr("style")) { - element.attr("style", element.attr("style").replaceAll(CSS_COLOR, "")); - } else if (element.tagName().equals("style")) { - element.html(cleanStyleTag(element.html())); - } - } - } - - public static String cleanStyleTag(String oldCss) { - return oldCss.replaceAll(CSS_COMMENT, "").replaceAll(CSS_COLOR, ""); - } -} 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 deleted file mode 100644 index 316608f2f..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/util/syndication/FeedDiscoverer.java +++ /dev/null @@ -1,79 +0,0 @@ -package de.danoeh.antennapod.core.util.syndication; - -import android.net.Uri; -import androidx.collection.ArrayMap; -import android.text.TextUtils; - -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.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), 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) { - return findLinks(Jsoup.parse(in), baseUrl); - } - - private Map findLinks(Document document, String baseUrl) { - Map res = new ArrayMap<>(); - Elements links = document.head().getElementsByTag("link"); - for (Element link : links) { - String rel = link.attr("rel"); - String href = link.attr("href"); - if (!TextUtils.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, - (TextUtils.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/syndication/HtmlToPlainText.java b/core/src/main/java/de/danoeh/antennapod/core/util/syndication/HtmlToPlainText.java deleted file mode 100644 index c5f0727c2..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/util/syndication/HtmlToPlainText.java +++ /dev/null @@ -1,123 +0,0 @@ -package de.danoeh.antennapod.core.util.syndication; - -import android.text.TextUtils; - -import org.apache.commons.lang3.StringUtils; -import org.jsoup.Jsoup; -import org.jsoup.internal.StringUtil; -import org.jsoup.nodes.Document; -import org.jsoup.nodes.Element; -import org.jsoup.nodes.Node; -import org.jsoup.nodes.TextNode; -import org.jsoup.select.NodeTraversor; -import org.jsoup.select.NodeVisitor; - -import java.util.regex.Pattern; - -/** - * This class is based on HtmlToPlainText from jsoup's examples package. - * - * HTML to plain-text. This example program demonstrates the use of jsoup to convert HTML input to lightly-formatted - * plain-text. That is divergent from the general goal of jsoup's .text() methods, which is to get clean data from a - * scrape. - *

- * Note that this is a fairly simplistic formatter -- for real world use you'll want to embrace and extend. - *

- *

- * To invoke from the command line, assuming you've downloaded the jsoup jar to your current directory:

- *

java -cp jsoup.jar org.jsoup.examples.HtmlToPlainText url [selector]

- * where url is the URL to fetch, and selector is an optional CSS selector. - * - * @author Jonathan Hedley, jonathan@hedley.net - * @author AntennaPod open source community - */ -public class HtmlToPlainText { - - /** - * Use this method to strip off HTML encoding from given text. - * Replaces bullet points with *, ignores colors/bold/... - * - * @param str String with any encoding - * @return Human readable text with minimal HTML formatting - */ - public static String getPlainText(String str) { - if (!TextUtils.isEmpty(str) && isHtml(str)) { - HtmlToPlainText formatter = new HtmlToPlainText(); - Document feedDescription = Jsoup.parse(str); - str = StringUtils.trim(formatter.getPlainText(feedDescription)); - } else if (TextUtils.isEmpty(str)) { - str = ""; - } - - return str; - } - - /** - * Use this method to determine if a given text has any HTML tag - * - * @param str String to be tested for presence of HTML content - * @return True if text contains any HTML tags
False is no HTML tag is found - */ - private static boolean isHtml(String str) { - final String htmlTagPattern = "<(\"[^\"]*\"|'[^']*'|[^'\">])*>"; - return Pattern.compile(htmlTagPattern).matcher(str).find(); - } - - /** - * Format an Element to plain-text - * @param element the root element to format - * @return formatted text - */ - public String getPlainText(Element element) { - FormattingVisitor formatter = new FormattingVisitor(); - // walk the DOM, and call .head() and .tail() for each node - NodeTraversor.traverse(formatter, element); - - return formatter.toString(); - } - - // the formatting rules, implemented in a breadth-first DOM traverse - private static class FormattingVisitor implements NodeVisitor { - - private final StringBuilder accum = new StringBuilder(); // holds the accumulated text - - // hit when the node is first seen - public void head(Node node, int depth) { - String name = node.nodeName(); - if (node instanceof TextNode) { - append(((TextNode) node).text()); // TextNodes carry all user-readable text in the DOM. - } else if (name.equals("li")) { - append("\n * "); - } else if (name.equals("dt")) { - append(" "); - } else if (StringUtil.in(name, "p", "h1", "h2", "h3", "h4", "h5", "tr")) { - append("\n"); - } - } - - // hit when all of the node's children (if any) have been visited - public void tail(Node node, int depth) { - String name = node.nodeName(); - if (StringUtil.in(name, "br", "dd", "dt", "p", "h1", "h2", "h3", "h4", "h5")) { - append("\n"); - } else if (name.equals("a")) { - append(String.format(" <%s>", node.absUrl("href"))); - } - } - - // appends text to the string builder with a simple word wrap method - private void append(String text) { - if (text.equals(" ") && - (accum.length() == 0 || StringUtil.in(accum.substring(accum.length() - 1), " ", "\n"))) { - return; // don't accumulate long runs of empty spaces - } - - accum.append(text); - } - - @Override - public String toString() { - return accum.toString(); - } - } -} diff --git a/core/src/test/java/android/text/TextUtils.java b/core/src/test/java/android/text/TextUtils.java deleted file mode 100644 index 709cb9e93..000000000 --- a/core/src/test/java/android/text/TextUtils.java +++ /dev/null @@ -1,42 +0,0 @@ -package android.text; - -/** - * A slim-down version of standard {@link android.text.TextUtils} to be used in unit tests. - */ -public class TextUtils { - - /** - * Returns true if a and b are equal, including if they are both null. - *

Note: In platform versions 1.1 and earlier, this method only worked well if - * both the arguments were instances of String.

- * @param a first CharSequence to check - * @param b second CharSequence to check - * @return true if a and b are equal - */ - @SuppressWarnings("unused") - public static boolean equals(CharSequence a, CharSequence b) { - if (a == b) return true; - int length; - if (a != null && b != null && (length = a.length()) == b.length()) { - if (a instanceof String && b instanceof String) { - return a.equals(b); - } else { - for (int i = 0; i < length; i++) { - if (a.charAt(i) != b.charAt(i)) return false; - } - return true; - } - } - return false; - } - - /** - * Returns true if the string is null or has zero length. - * - * @param str The string to be examined, can be null. - * @return true if the string is null or has zero length. - */ - public static boolean isEmpty(CharSequence str) { - return str == null || str.length() == 0; - } -} diff --git a/core/src/test/java/de/danoeh/antennapod/core/feed/VolumeAdaptionSettingTest.java b/core/src/test/java/de/danoeh/antennapod/core/feed/VolumeAdaptionSettingTest.java deleted file mode 100644 index 966351a5e..000000000 --- a/core/src/test/java/de/danoeh/antennapod/core/feed/VolumeAdaptionSettingTest.java +++ /dev/null @@ -1,129 +0,0 @@ -package de.danoeh.antennapod.core.feed; - -import de.danoeh.antennapod.model.feed.VolumeAdaptionSetting; - -import org.junit.After; -import org.junit.Before; -import org.junit.Test; - -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.is; -import static org.junit.Assert.assertEquals; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.Assert.assertTrue; - -public class VolumeAdaptionSettingTest { - - @Before - public void setUp() throws Exception { - VolumeAdaptionSetting.setBoostSupported(false); - } - - @After - public void tearDown() throws Exception { - VolumeAdaptionSetting.setBoostSupported(null); - } - - @Test - public void mapOffToInteger() { - VolumeAdaptionSetting setting = VolumeAdaptionSetting.OFF; - assertThat(setting.toInteger(), is(equalTo(0))); - } - - @Test - public void mapLightReductionToInteger() { - VolumeAdaptionSetting setting = VolumeAdaptionSetting.LIGHT_REDUCTION; - - assertThat(setting.toInteger(), is(equalTo(1))); - } - - @Test - public void mapHeavyReductionToInteger() { - VolumeAdaptionSetting setting = VolumeAdaptionSetting.HEAVY_REDUCTION; - - assertThat(setting.toInteger(), is(equalTo(2))); - } - - @Test - public void mapLightBoostToInteger() { - VolumeAdaptionSetting setting = VolumeAdaptionSetting.LIGHT_BOOST; - - assertThat(setting.toInteger(), is(equalTo(3))); - } - - @Test - public void mapMediumBoostToInteger() { - VolumeAdaptionSetting setting = VolumeAdaptionSetting.MEDIUM_BOOST; - - assertThat(setting.toInteger(), is(equalTo(4))); - } - - @Test - public void mapHeavyBoostToInteger() { - VolumeAdaptionSetting setting = VolumeAdaptionSetting.HEAVY_BOOST; - - assertThat(setting.toInteger(), is(equalTo(5))); - } - - @Test - public void mapIntegerToVolumeAdaptionSetting() { - assertThat(VolumeAdaptionSetting.fromInteger(0), is(equalTo(VolumeAdaptionSetting.OFF))); - assertThat(VolumeAdaptionSetting.fromInteger(1), is(equalTo(VolumeAdaptionSetting.LIGHT_REDUCTION))); - assertThat(VolumeAdaptionSetting.fromInteger(2), is(equalTo(VolumeAdaptionSetting.HEAVY_REDUCTION))); - assertThat(VolumeAdaptionSetting.fromInteger(3), is(equalTo(VolumeAdaptionSetting.LIGHT_BOOST))); - assertThat(VolumeAdaptionSetting.fromInteger(4), is(equalTo(VolumeAdaptionSetting.MEDIUM_BOOST))); - assertThat(VolumeAdaptionSetting.fromInteger(5), is(equalTo(VolumeAdaptionSetting.HEAVY_BOOST))); - } - - @Test(expected = IllegalArgumentException.class) - public void cannotMapNegativeValues() { - VolumeAdaptionSetting.fromInteger(-1); - } - - @Test(expected = IllegalArgumentException.class) - public void cannotMapValuesOutOfRange() { - VolumeAdaptionSetting.fromInteger(6); - } - - @Test - public void noAdaptionIfTurnedOff() { - float adaptionFactor = VolumeAdaptionSetting.OFF.getAdaptionFactor(); - assertEquals(1.0f, adaptionFactor, 0.01f); - } - - @Test - public void lightReductionYieldsHigherValueThanHeavyReduction() { - float lightReductionFactor = VolumeAdaptionSetting.LIGHT_REDUCTION.getAdaptionFactor(); - - float heavyReductionFactor = VolumeAdaptionSetting.HEAVY_REDUCTION.getAdaptionFactor(); - - assertTrue("Light reduction must have higher factor than heavy reduction", lightReductionFactor > heavyReductionFactor); - } - - @Test - public void lightBoostYieldsHigherValueThanLightReduction() { - float lightReductionFactor = VolumeAdaptionSetting.LIGHT_REDUCTION.getAdaptionFactor(); - - float lightBoostFactor = VolumeAdaptionSetting.LIGHT_BOOST.getAdaptionFactor(); - - assertTrue("Light boost must have higher factor than light reduction", lightBoostFactor > lightReductionFactor); - } - - @Test - public void mediumBoostYieldsHigherValueThanLightBoost() { - float lightBoostFactor = VolumeAdaptionSetting.LIGHT_BOOST.getAdaptionFactor(); - - float mediumBoostFactor = VolumeAdaptionSetting.MEDIUM_BOOST.getAdaptionFactor(); - - assertTrue("Medium boost must have higher factor than light boost", mediumBoostFactor > lightBoostFactor); - } - - @Test - public void heavyBoostYieldsHigherValueThanMediumBoost() { - float mediumBoostFactor = VolumeAdaptionSetting.MEDIUM_BOOST.getAdaptionFactor(); - - float heavyBoostFactor = VolumeAdaptionSetting.HEAVY_BOOST.getAdaptionFactor(); - - assertTrue("Heavy boost must have higher factor than medium boost", heavyBoostFactor > mediumBoostFactor); - } -} diff --git a/core/src/test/java/de/danoeh/antennapod/core/storage/mapper/FeedCursorMapperTest.java b/core/src/test/java/de/danoeh/antennapod/core/storage/mapper/FeedCursorMapperTest.java deleted file mode 100644 index 87ade0c6f..000000000 --- a/core/src/test/java/de/danoeh/antennapod/core/storage/mapper/FeedCursorMapperTest.java +++ /dev/null @@ -1,101 +0,0 @@ -package de.danoeh.antennapod.core.storage.mapper; - -import android.content.ContentValues; -import android.content.Context; -import android.database.Cursor; - -import androidx.test.platform.app.InstrumentationRegistry; - -import de.danoeh.antennapod.storage.database.PodDBAdapter; -import de.danoeh.antennapod.storage.database.mapper.FeedCursorMapper; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.robolectric.RobolectricTestRunner; - -import de.danoeh.antennapod.model.feed.Feed; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; - -@RunWith(RobolectricTestRunner.class) -public class FeedCursorMapperTest { - private PodDBAdapter adapter; - - @Before - public void setUp() { - Context context = InstrumentationRegistry.getInstrumentation().getContext(); - - PodDBAdapter.init(context); - adapter = PodDBAdapter.getInstance(); - - writeFeedToDatabase(); - } - - @After - public void tearDown() { - PodDBAdapter.tearDownTests(); - } - - @SuppressWarnings("ConstantConditions") - @Test - public void testFromCursor() { - try (Cursor cursor = adapter.getAllFeedsCursor()) { - cursor.moveToNext(); - Feed feed = FeedCursorMapper.convert(cursor); - assertTrue(feed.getId() >= 0); - assertEquals("feed custom title", feed.getTitle()); - assertEquals("feed custom title", feed.getCustomTitle()); - assertEquals("feed link", feed.getLink()); - assertEquals("feed description", feed.getDescription()); - assertEquals("feed payment link", feed.getPaymentLinks().get(0).url); - assertEquals("feed author", feed.getAuthor()); - assertEquals("feed language", feed.getLanguage()); - assertEquals("feed image url", feed.getImageUrl()); - assertEquals("feed file url", feed.getLocalFileUrl()); - assertEquals("feed download url", feed.getDownloadUrl()); - assertEquals(42, feed.getLastRefreshAttempt()); - assertEquals("feed last update", feed.getLastModified()); - assertEquals("feed type", feed.getType()); - assertEquals("feed identifier", feed.getFeedIdentifier()); - assertTrue(feed.isPaged()); - assertEquals("feed next page link", feed.getNextPageLink()); - assertTrue(feed.getItemFilter().showUnplayed); - assertEquals(1, feed.getSortOrder().code); - assertTrue(feed.hasLastUpdateFailed()); - } - } - - /** - * Insert test data to the database. - * Uses raw database insert instead of adapter.setCompleteFeed() to avoid testing the Feed class - * against itself. - */ - private void writeFeedToDatabase() { - ContentValues values = new ContentValues(); - values.put(PodDBAdapter.KEY_TITLE, "feed title"); - values.put(PodDBAdapter.KEY_CUSTOM_TITLE, "feed custom title"); - values.put(PodDBAdapter.KEY_LINK, "feed link"); - values.put(PodDBAdapter.KEY_DESCRIPTION, "feed description"); - values.put(PodDBAdapter.KEY_PAYMENT_LINK, "feed payment link"); - values.put(PodDBAdapter.KEY_AUTHOR, "feed author"); - values.put(PodDBAdapter.KEY_LANGUAGE, "feed language"); - values.put(PodDBAdapter.KEY_IMAGE_URL, "feed image url"); - - values.put(PodDBAdapter.KEY_FILE_URL, "feed file url"); - values.put(PodDBAdapter.KEY_DOWNLOAD_URL, "feed download url"); - values.put(PodDBAdapter.KEY_LAST_REFRESH_ATTEMPT, 42); - values.put(PodDBAdapter.KEY_LASTUPDATE, "feed last update"); - values.put(PodDBAdapter.KEY_TYPE, "feed type"); - values.put(PodDBAdapter.KEY_FEED_IDENTIFIER, "feed identifier"); - - values.put(PodDBAdapter.KEY_IS_PAGED, true); - values.put(PodDBAdapter.KEY_NEXT_PAGE_LINK, "feed next page link"); - values.put(PodDBAdapter.KEY_HIDE, "unplayed"); - values.put(PodDBAdapter.KEY_SORT_ORDER, "1"); - values.put(PodDBAdapter.KEY_LAST_UPDATE_FAILED, true); - - adapter.insertTestData(PodDBAdapter.TABLE_NAME_FEEDS, values); - } -} diff --git a/core/src/test/java/de/danoeh/antennapod/core/util/ConverterTest.java b/core/src/test/java/de/danoeh/antennapod/core/util/ConverterTest.java deleted file mode 100644 index 2e4ead5e6..000000000 --- a/core/src/test/java/de/danoeh/antennapod/core/util/ConverterTest.java +++ /dev/null @@ -1,40 +0,0 @@ -package de.danoeh.antennapod.core.util; - -import de.danoeh.antennapod.ui.common.Converter; -import org.junit.Test; - -import static org.junit.Assert.assertEquals; - -/** - * Test class for converter - */ -public class ConverterTest { - - @Test - public void testGetDurationStringLong() { - String expected = "13:05:10"; - int input = 47110000; - assertEquals(expected, Converter.getDurationStringLong(input)); - } - - @Test - public void testGetDurationStringShort() { - String expected = "13:05"; - assertEquals(expected, Converter.getDurationStringShort(47110000, true)); - assertEquals(expected, Converter.getDurationStringShort(785000, false)); - } - - @Test - public void testDurationStringLongToMs() { - String input = "01:20:30"; - long expected = 4830000; - assertEquals(expected, Converter.durationStringLongToMs(input)); - } - - @Test - public void testDurationStringShortToMs() { - String input = "8:30"; - assertEquals(30600000, Converter.durationStringShortToMs(input, true)); - assertEquals(510000, Converter.durationStringShortToMs(input, false)); - } -} diff --git a/core/src/test/java/de/danoeh/antennapod/core/util/FeedItemPermutorsTest.java b/core/src/test/java/de/danoeh/antennapod/core/util/FeedItemPermutorsTest.java deleted file mode 100644 index 4ebff066e..000000000 --- a/core/src/test/java/de/danoeh/antennapod/core/util/FeedItemPermutorsTest.java +++ /dev/null @@ -1,231 +0,0 @@ -package de.danoeh.antennapod.core.util; - -import de.danoeh.antennapod.model.feed.SortOrder; -import de.danoeh.antennapod.storage.database.FeedItemPermutors; -import de.danoeh.antennapod.storage.database.Permutor; -import org.junit.Test; - -import java.util.ArrayList; -import java.util.Calendar; -import java.util.List; - -import de.danoeh.antennapod.model.feed.Feed; -import de.danoeh.antennapod.model.feed.FeedItem; -import de.danoeh.antennapod.model.feed.FeedMedia; - -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; - -/** - * Test class for FeedItemPermutors. - */ -public class FeedItemPermutorsTest { - - @Test - public void testEnsureNonNullPermutors() { - for (SortOrder sortOrder : SortOrder.values()) { - assertNotNull("The permutor for SortOrder " + sortOrder + " is unexpectedly null", - FeedItemPermutors.getPermutor(sortOrder)); - } - } - - @Test - public void testPermutorForRule_EPISODE_TITLE_ASC() { - Permutor permutor = FeedItemPermutors.getPermutor(SortOrder.EPISODE_TITLE_A_Z); - - List itemList = getTestList(); - assertTrue(checkIdOrder(itemList, 1, 3, 2)); // before sorting - permutor.reorder(itemList); - assertTrue(checkIdOrder(itemList, 1, 2, 3)); // after sorting - } - - @Test - public void testPermutorForRule_EPISODE_TITLE_ASC_NullTitle() { - Permutor permutor = FeedItemPermutors.getPermutor(SortOrder.EPISODE_TITLE_A_Z); - - List itemList = getTestList(); - itemList.get(2) // itemId 2 - .setTitle(null); - assertTrue(checkIdOrder(itemList, 1, 3, 2)); // before sorting - permutor.reorder(itemList); - assertTrue(checkIdOrder(itemList, 2, 1, 3)); // after sorting - } - - - @Test - public void testPermutorForRule_EPISODE_TITLE_DESC() { - Permutor permutor = FeedItemPermutors.getPermutor(SortOrder.EPISODE_TITLE_Z_A); - - List itemList = getTestList(); - assertTrue(checkIdOrder(itemList, 1, 3, 2)); // before sorting - permutor.reorder(itemList); - assertTrue(checkIdOrder(itemList, 3, 2, 1)); // after sorting - } - - @Test - public void testPermutorForRule_DATE_ASC() { - Permutor permutor = FeedItemPermutors.getPermutor(SortOrder.DATE_OLD_NEW); - - List itemList = getTestList(); - assertTrue(checkIdOrder(itemList, 1, 3, 2)); // before sorting - permutor.reorder(itemList); - assertTrue(checkIdOrder(itemList, 1, 2, 3)); // after sorting - } - - @Test - public void testPermutorForRule_DATE_ASC_NulPubDatel() { - Permutor permutor = FeedItemPermutors.getPermutor(SortOrder.DATE_OLD_NEW); - - List itemList = getTestList(); - itemList.get(2) // itemId 2 - .setPubDate(null); - assertTrue(checkIdOrder(itemList, 1, 3, 2)); // before sorting - permutor.reorder(itemList); - assertTrue(checkIdOrder(itemList, 2, 1, 3)); // after sorting - } - - @Test - public void testPermutorForRule_DATE_DESC() { - Permutor permutor = FeedItemPermutors.getPermutor(SortOrder.DATE_NEW_OLD); - - List itemList = getTestList(); - assertTrue(checkIdOrder(itemList, 1, 3, 2)); // before sorting - permutor.reorder(itemList); - assertTrue(checkIdOrder(itemList, 3, 2, 1)); // after sorting - } - - @Test - public void testPermutorForRule_DURATION_ASC() { - Permutor permutor = FeedItemPermutors.getPermutor(SortOrder.DURATION_SHORT_LONG); - - List itemList = getTestList(); - assertTrue(checkIdOrder(itemList, 1, 3, 2)); // before sorting - permutor.reorder(itemList); - assertTrue(checkIdOrder(itemList, 1, 2, 3)); // after sorting - } - - @Test - public void testPermutorForRule_DURATION_DESC() { - Permutor permutor = FeedItemPermutors.getPermutor(SortOrder.DURATION_LONG_SHORT); - - List itemList = getTestList(); - assertTrue(checkIdOrder(itemList, 1, 3, 2)); // before sorting - permutor.reorder(itemList); - assertTrue(checkIdOrder(itemList, 3, 2, 1)); // after sorting - } - - @Test - public void testPermutorForRule_size_asc() { - Permutor permutor = FeedItemPermutors.getPermutor(SortOrder.SIZE_SMALL_LARGE); - - List itemList = getTestList(); - assertTrue(checkIdOrder(itemList, 1, 3, 2)); // before sorting - permutor.reorder(itemList); - assertTrue(checkIdOrder(itemList, 1, 2, 3)); // after sorting - } - - @Test - public void testPermutorForRule_size_desc() { - Permutor permutor = FeedItemPermutors.getPermutor(SortOrder.SIZE_LARGE_SMALL); - - List itemList = getTestList(); - assertTrue(checkIdOrder(itemList, 1, 3, 2)); // before sorting - permutor.reorder(itemList); - assertTrue(checkIdOrder(itemList, 3, 2, 1)); // after sorting - } - - @Test - public void testPermutorForRule_DURATION_DESC_NullMedia() { - Permutor permutor = FeedItemPermutors.getPermutor(SortOrder.DURATION_LONG_SHORT); - - List itemList = getTestList(); - itemList.get(1) // itemId 3 - .setMedia(null); - assertTrue(checkIdOrder(itemList, 1, 3, 2)); // before sorting - permutor.reorder(itemList); - assertTrue(checkIdOrder(itemList, 2, 1, 3)); // after sorting - } - - @Test - public void testPermutorForRule_FEED_TITLE_ASC() { - Permutor permutor = FeedItemPermutors.getPermutor(SortOrder.FEED_TITLE_A_Z); - - List itemList = getTestList(); - assertTrue(checkIdOrder(itemList, 1, 3, 2)); // before sorting - permutor.reorder(itemList); - assertTrue(checkIdOrder(itemList, 1, 2, 3)); // after sorting - } - - @Test - public void testPermutorForRule_FEED_TITLE_DESC() { - Permutor permutor = FeedItemPermutors.getPermutor(SortOrder.FEED_TITLE_Z_A); - - List itemList = getTestList(); - assertTrue(checkIdOrder(itemList, 1, 3, 2)); // before sorting - permutor.reorder(itemList); - assertTrue(checkIdOrder(itemList, 3, 2, 1)); // after sorting - } - - @Test - public void testPermutorForRule_FEED_TITLE_DESC_NullTitle() { - Permutor permutor = FeedItemPermutors.getPermutor(SortOrder.FEED_TITLE_Z_A); - - List itemList = getTestList(); - itemList.get(1) // itemId 3 - .getFeed().setTitle(null); - assertTrue(checkIdOrder(itemList, 1, 3, 2)); // before sorting - permutor.reorder(itemList); - assertTrue(checkIdOrder(itemList, 2, 1, 3)); // after sorting - } - - /** - * Generates a list with test data. - */ - private List getTestList() { - List itemList = new ArrayList<>(); - - Calendar calendar = Calendar.getInstance(); - calendar.set(2019, 0, 1); // January 1st - Feed feed1 = new Feed(null, null, "Feed title 1"); - FeedItem feedItem1 = new FeedItem(1, "Title 1", null, null, calendar.getTime(), 0, feed1); - FeedMedia feedMedia1 = new FeedMedia(0, feedItem1, 1000, 0, 100, null, null, null, true, null, 0, 0); - feedItem1.setMedia(feedMedia1); - itemList.add(feedItem1); - - calendar.set(2019, 2, 1); // March 1st - Feed feed2 = new Feed(null, null, "Feed title 3"); - FeedItem feedItem2 = new FeedItem(3, "Title 3", null, null, calendar.getTime(), 0, feed2); - FeedMedia feedMedia2 = new FeedMedia(0, feedItem2, 3000, 0, 300, null, null, null, true, null, 0, 0); - feedItem2.setMedia(feedMedia2); - itemList.add(feedItem2); - - calendar.set(2019, 1, 1); // February 1st - Feed feed3 = new Feed(null, null, "Feed title 2"); - FeedItem feedItem3 = new FeedItem(2, "Title 2", null, null, calendar.getTime(), 0, feed3); - FeedMedia feedMedia3 = new FeedMedia(0, feedItem3, 2000, 0, 200, null, null, null, true, null, 0, 0); - feedItem3.setMedia(feedMedia3); - itemList.add(feedItem3); - - return itemList; - } - - /** - * Checks if both lists have the same size and the same ID order. - * - * @param itemList Item list. - * @param ids List of IDs. - * @return true if both lists have the same size and the same ID order. - */ - private boolean checkIdOrder(List itemList, long... ids) { - if (itemList.size() != ids.length) { - return false; - } - - for (int i = 0; i < ids.length; i++) { - if (itemList.get(i).getId() != ids[i]) { - return false; - } - } - return true; - } -} diff --git a/core/src/test/java/de/danoeh/antennapod/core/util/FilenameGeneratorTest.java b/core/src/test/java/de/danoeh/antennapod/core/util/FilenameGeneratorTest.java deleted file mode 100644 index 4c225322a..000000000 --- a/core/src/test/java/de/danoeh/antennapod/core/util/FilenameGeneratorTest.java +++ /dev/null @@ -1,99 +0,0 @@ -package de.danoeh.antennapod.core.util; - -import androidx.test.platform.app.InstrumentationRegistry; -import android.text.TextUtils; - -import java.io.File; - -import de.danoeh.antennapod.net.download.serviceinterface.FileNameGenerator; -import org.apache.commons.lang3.StringUtils; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.robolectric.RobolectricTestRunner; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotEquals; -import static org.junit.Assert.assertTrue; - -@RunWith(RobolectricTestRunner.class) -public class FilenameGeneratorTest { - - public FilenameGeneratorTest() { - super(); - } - - @Test - public void testGenerateFileName() throws Exception { - String result = FileNameGenerator.generateFileName("abc abc"); - assertEquals(result, "abc abc"); - createFiles(result); - } - - @Test - public void testGenerateFileName1() throws Exception { - String result = FileNameGenerator.generateFileName("ab/c: "; - ShownotesCleaner t = new ShownotesCleaner(context, shownotes, Integer.MAX_VALUE); - String res = t.processShownotes(); - checkLinkCorrect(res, new long[]{time}, new String[]{timeStr}); - } - - @Test - public void testProcessShownotesAddTimecodeHhmmssMoreThen24HoursNoChapters() { - final String timeStr = "25:00:00"; - final long time = 25 * 60 * 60 * 1000; - - String shownotes = "

Some test text with a timecode " + timeStr + " here.

"; - ShownotesCleaner t = new ShownotesCleaner(context, shownotes, Integer.MAX_VALUE); - String res = t.processShownotes(); - checkLinkCorrect(res, new long[]{time}, new String[]{timeStr}); - } - - @Test - public void testProcessShownotesAddTimecodeHhmmNoChapters() { - final String timeStr = "10:11"; - final long time = 3600 * 1000 * 10 + 60 * 1000 * 11; - - String shownotes = "

Some test text with a timecode " + timeStr + " here.

"; - ShownotesCleaner t = new ShownotesCleaner(context, shownotes, Integer.MAX_VALUE); - String res = t.processShownotes(); - checkLinkCorrect(res, new long[]{time}, new String[]{timeStr}); - } - - @Test - public void testProcessShownotesAddTimecodeMmssNoChapters() { - final String timeStr = "10:11"; - final long time = 10 * 60 * 1000 + 11 * 1000; - - String shownotes = "

Some test text with a timecode " + timeStr + " here.

"; - ShownotesCleaner t = new ShownotesCleaner(context, shownotes, 11 * 60 * 1000); - String res = t.processShownotes(); - checkLinkCorrect(res, new long[]{time}, new String[]{timeStr}); - } - - @Test - public void testProcessShownotesAddTimecodeHmmssNoChapters() { - final String timeStr = "2:11:12"; - final long time = 2 * 60 * 60 * 1000 + 11 * 60 * 1000 + 12 * 1000; - - String shownotes = "

Some test text with a timecode " + timeStr + " here.

"; - ShownotesCleaner t = new ShownotesCleaner(context, shownotes, Integer.MAX_VALUE); - String res = t.processShownotes(); - checkLinkCorrect(res, new long[]{time}, new String[]{timeStr}); - } - - @Test - public void testProcessShownotesAddTimecodeMssNoChapters() { - final String timeStr = "1:12"; - final long time = 60 * 1000 + 12 * 1000; - - String shownotes = "

Some test text with a timecode " + timeStr + " here.

"; - ShownotesCleaner t = new ShownotesCleaner(context, shownotes, 2 * 60 * 1000); - String res = t.processShownotes(); - checkLinkCorrect(res, new long[]{time}, new String[]{timeStr}); - } - - @Test - public void testProcessShownotesAddNoTimecodeDuration() { - final String timeStr = "2:11:12"; - final int time = 2 * 60 * 60 * 1000 + 11 * 60 * 1000 + 12 * 1000; - - String shownotes = "

Some test text with a timecode " + timeStr + " here.

"; - ShownotesCleaner t = new ShownotesCleaner(context, shownotes, time); - String res = t.processShownotes(); - Document d = Jsoup.parse(res); - assertEquals("Should not parse time codes that equal duration", 0, d.body().getElementsByTag("a").size()); - } - - @Test - public void testProcessShownotesAddTimecodeMultipleFormatsNoChapters() { - final String[] timeStrings = new String[]{ "10:12", "1:10:12" }; - - String shownotes = "

Some test text with a timecode " + timeStrings[0] - + " here. Hey look another one " + timeStrings[1] + " here!

"; - ShownotesCleaner t = new ShownotesCleaner(context, shownotes, 2 * 60 * 60 * 1000); - String res = t.processShownotes(); - checkLinkCorrect(res, new long[]{10 * 60 * 1000 + 12 * 1000, - 60 * 60 * 1000 + 10 * 60 * 1000 + 12 * 1000}, timeStrings); - } - - @Test - public void testProcessShownotesAddTimecodeMultipleShortFormatNoChapters() { - - // One of these timecodes fits as HH:MM and one does not so both should be parsed as MM:SS. - final String[] timeStrings = new String[]{ "10:12", "2:12" }; - - String shownotes = "

Some test text with a timecode " + timeStrings[0] - + " here. Hey look another one " + timeStrings[1] + " here!

"; - ShownotesCleaner t = new ShownotesCleaner(context, shownotes, 3 * 60 * 60 * 1000); - String res = t.processShownotes(); - checkLinkCorrect(res, new long[]{10 * 60 * 1000 + 12 * 1000, 2 * 60 * 1000 + 12 * 1000}, timeStrings); - } - - @Test - public void testProcessShownotesAddTimecodeParentheses() { - final String timeStr = "10:11"; - final long time = 3600 * 1000 * 10 + 60 * 1000 * 11; - - String shownotes = "

Some test text with a timecode (" + timeStr + ") here.

"; - ShownotesCleaner t = new ShownotesCleaner(context, shownotes, Integer.MAX_VALUE); - String res = t.processShownotes(); - checkLinkCorrect(res, new long[]{time}, new String[]{timeStr}); - } - - @Test - public void testProcessShownotesAddTimecodeBrackets() { - final String timeStr = "10:11"; - final long time = 3600 * 1000 * 10 + 60 * 1000 * 11; - - String shownotes = "

Some test text with a timecode [" + timeStr + "] here.

"; - ShownotesCleaner t = new ShownotesCleaner(context, shownotes, Integer.MAX_VALUE); - String res = t.processShownotes(); - checkLinkCorrect(res, new long[]{time}, new String[]{timeStr}); - } - - @Test - public void testProcessShownotesAddTimecodeAngleBrackets() { - final String timeStr = "10:11"; - final long time = 3600 * 1000 * 10 + 60 * 1000 * 11; - - String shownotes = "

Some test text with a timecode <" + timeStr + "> here.

"; - ShownotesCleaner t = new ShownotesCleaner(context, shownotes, Integer.MAX_VALUE); - String res = t.processShownotes(); - checkLinkCorrect(res, new long[]{time}, new String[]{timeStr}); - } - - @Test - public void testProcessShownotesAndInvalidTimecode() { - final String[] timeStrs = new String[] {"2:1", "0:0", "000", "00", "00:000"}; - - StringBuilder shownotes = new StringBuilder("

Some test text with timecodes "); - for (String timeStr : timeStrs) { - shownotes.append(timeStr).append(" "); - } - shownotes.append("here.

"); - - ShownotesCleaner t = new ShownotesCleaner(context, shownotes.toString(), Integer.MAX_VALUE); - String res = t.processShownotes(); - checkLinkCorrect(res, new long[0], new String[0]); - } - - private void checkLinkCorrect(String res, long[] timecodes, String[] timecodeStr) { - assertNotNull(res); - Document d = Jsoup.parse(res); - Elements links = d.body().getElementsByTag("a"); - int countedLinks = 0; - for (Element link : links) { - String href = link.attributes().get("href"); - String text = link.text(); - if (href.startsWith("antennapod://")) { - assertTrue(href.endsWith(String.valueOf(timecodes[countedLinks]))); - assertEquals(timecodeStr[countedLinks], text); - countedLinks++; - assertTrue("Contains too many links: " + countedLinks + " > " - + timecodes.length, countedLinks <= timecodes.length); - } - } - assertEquals(timecodes.length, countedLinks); - } - - @Test - public void testIsTimecodeLink() { - assertFalse(ShownotesCleaner.isTimecodeLink(null)); - assertFalse(ShownotesCleaner.isTimecodeLink("http://antennapod/timecode/123123")); - assertFalse(ShownotesCleaner.isTimecodeLink("antennapod://timecode/")); - assertFalse(ShownotesCleaner.isTimecodeLink("antennapod://123123")); - assertFalse(ShownotesCleaner.isTimecodeLink("antennapod://timecode/123123a")); - assertTrue(ShownotesCleaner.isTimecodeLink("antennapod://timecode/123")); - assertTrue(ShownotesCleaner.isTimecodeLink("antennapod://timecode/1")); - } - - @Test - public void testGetTimecodeLinkTime() { - assertEquals(-1, ShownotesCleaner.getTimecodeLinkTime(null)); - assertEquals(-1, ShownotesCleaner.getTimecodeLinkTime("http://timecode/123")); - assertEquals(123, ShownotesCleaner.getTimecodeLinkTime("antennapod://timecode/123")); - } - - @Test - public void testCleanupColors() { - final String input = "/* /* */ .foo { text-decoration: underline;color:#f00;font-weight:bold;}" - + "#bar { text-decoration: underline;color:#f00;font-weight:bold; }" - + "div {text-decoration: underline; color /* */ : /* */ #f00 /* */; font-weight:bold; }" - + "#foobar { /* color: */ text-decoration: underline; /* color: */font-weight:bold /* ; */; }" - + "baz { background-color:#f00;border: solid 2px;border-color:#0f0;text-decoration: underline; }"; - final String expected = " .foo { text-decoration: underline;font-weight:bold;}" - + "#bar { text-decoration: underline;font-weight:bold; }" - + "div {text-decoration: underline; font-weight:bold; }" - + "#foobar { text-decoration: underline; font-weight:bold ; }" - + "baz { background-color:#f00;border: solid 2px;border-color:#0f0;text-decoration: underline; }"; - assertEquals(expected, ShownotesCleaner.cleanStyleTag(input)); - } -} diff --git a/core/src/test/java/de/danoeh/antennapod/core/util/syndication/FeedDiscovererTest.java b/core/src/test/java/de/danoeh/antennapod/core/util/syndication/FeedDiscovererTest.java deleted file mode 100644 index 3df5230cc..000000000 --- a/core/src/test/java/de/danoeh/antennapod/core/util/syndication/FeedDiscovererTest.java +++ /dev/null @@ -1,128 +0,0 @@ -package de.danoeh.antennapod.core.util.syndication; - -import androidx.test.platform.app.InstrumentationRegistry; - -import org.apache.commons.io.FileUtils; -import org.apache.commons.io.IOUtils; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.robolectric.RobolectricTestRunner; - -import java.io.File; -import java.io.FileOutputStream; -import java.nio.charset.StandardCharsets; -import java.util.Map; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; - -/** - * Test class for {@link FeedDiscoverer} - */ -@RunWith(RobolectricTestRunner.class) -public class FeedDiscovererTest { - - private FeedDiscoverer fd; - - private File testDir; - - @Before - public void setUp() { - fd = new FeedDiscoverer(); - testDir = new File(InstrumentationRegistry - .getInstrumentation().getTargetContext().getFilesDir(), "FeedDiscovererTest"); - //noinspection ResultOfMethodCallIgnored - testDir.mkdir(); - assertTrue(testDir.exists()); - } - - @After - public void tearDown() throws Exception { - FileUtils.deleteDirectory(testDir); - } - - @SuppressWarnings("SameParameterValue") - private String createTestHtmlString(String rel, String type, String href, String title) { - return String.format("Test", - rel, type, href, title); - } - - private String createTestHtmlString(String rel, String type, String href) { - return String.format("Test", - rel, type, href); - } - - private void checkFindUrls(boolean isAlternate, boolean isRss, boolean withTitle, boolean isAbsolute, boolean fromString) throws Exception { - final String title = "Test title"; - final String hrefAbs = "http://example.com/feed"; - final String hrefRel = "/feed"; - final String base = "http://example.com"; - - final String rel = (isAlternate) ? "alternate" : "feed"; - final String type = (isRss) ? "application/rss+xml" : "application/atom+xml"; - final String href = (isAbsolute) ? hrefAbs : hrefRel; - - Map res; - String html = (withTitle) ? createTestHtmlString(rel, type, href, title) - : createTestHtmlString(rel, type, href); - if (fromString) { - res = fd.findLinks(html, base); - } else { - File testFile = new File(testDir, "feed"); - FileOutputStream out = new FileOutputStream(testFile); - IOUtils.write(html, out, StandardCharsets.UTF_8); - out.close(); - res = fd.findLinks(testFile, base); - } - - assertNotNull(res); - assertEquals(1, res.size()); - for (String key : res.keySet()) { - assertEquals(hrefAbs, key); - } - assertTrue(res.containsKey(hrefAbs)); - if (withTitle) { - assertEquals(title, res.get(hrefAbs)); - } else { - assertEquals(href, res.get(hrefAbs)); - } - } - - @Test - public void testAlternateRSSWithTitleAbsolute() throws Exception { - checkFindUrls(true, true, true, true, true); - } - - @Test - public void testAlternateRSSWithTitleRelative() throws Exception { - checkFindUrls(true, true, true, false, true); - } - - @Test - public void testAlternateRSSNoTitleAbsolute() throws Exception { - checkFindUrls(true, true, false, true, true); - } - - @Test - public void testAlternateRSSNoTitleRelative() throws Exception { - checkFindUrls(true, true, false, false, true); - } - - @Test - public void testAlternateAtomWithTitleAbsolute() throws Exception { - checkFindUrls(true, false, true, true, true); - } - - @Test - public void testFeedAtomWithTitleAbsolute() throws Exception { - checkFindUrls(false, false, true, true, true); - } - - @Test - public void testAlternateRSSWithTitleAbsoluteFromFile() throws Exception { - checkFindUrls(true, true, true, true, false); - } -} diff --git a/model/build.gradle b/model/build.gradle index 5afbdf109..fa0f3679c 100644 --- a/model/build.gradle +++ b/model/build.gradle @@ -14,6 +14,8 @@ android { dependencies { annotationProcessor "androidx.annotation:annotation:$annotationVersion" implementation "androidx.media:media:$mediaVersion" - implementation "org.apache.commons:commons-lang3:$commonslangVersion" + + testImplementation "junit:junit:$junitVersion" + testImplementation "androidx.test:core:$testCoreVersion" } diff --git a/model/src/main/java/de/danoeh/antennapod/model/feed/Feed.java b/model/src/main/java/de/danoeh/antennapod/model/feed/Feed.java index 8d36d24a3..15d256c24 100644 --- a/model/src/main/java/de/danoeh/antennapod/model/feed/Feed.java +++ b/model/src/main/java/de/danoeh/antennapod/model/feed/Feed.java @@ -1,12 +1,11 @@ package de.danoeh.antennapod.model.feed; -import android.text.TextUtils; - import androidx.annotation.Nullable; import java.util.ArrayList; import java.util.Date; import java.util.List; +import org.apache.commons.lang3.StringUtils; /** * Data Object for a whole feed. @@ -205,9 +204,9 @@ public class Feed { } public String getHumanReadableIdentifier() { - if (!TextUtils.isEmpty(customTitle)) { + if (!StringUtils.isEmpty(customTitle)) { return customTitle; - } else if (!TextUtils.isEmpty(feedTitle)) { + } else if (!StringUtils.isEmpty(feedTitle)) { return feedTitle; } else { return downloadUrl; @@ -266,7 +265,7 @@ public class Feed { } public String getTitle() { - return !TextUtils.isEmpty(customTitle) ? customTitle : feedTitle; + return !StringUtils.isEmpty(customTitle) ? customTitle : feedTitle; } public void setTitle(String title) { diff --git a/model/src/main/java/de/danoeh/antennapod/model/playback/RemoteMedia.java b/model/src/main/java/de/danoeh/antennapod/model/playback/RemoteMedia.java index b963f3de1..c979488a6 100644 --- a/model/src/main/java/de/danoeh/antennapod/model/playback/RemoteMedia.java +++ b/model/src/main/java/de/danoeh/antennapod/model/playback/RemoteMedia.java @@ -3,7 +3,6 @@ package de.danoeh.antennapod.model.playback; import android.content.Context; import android.os.Parcel; import android.os.Parcelable; -import android.text.TextUtils; import androidx.annotation.Nullable; import de.danoeh.antennapod.model.feed.Chapter; import de.danoeh.antennapod.model.feed.Feed; @@ -13,6 +12,7 @@ import de.danoeh.antennapod.model.feed.FeedMedia; import java.util.Date; import java.util.List; +import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.builder.HashCodeBuilder; /** @@ -68,7 +68,7 @@ public class RemoteMedia implements Playable { this.episodeTitle = item.getTitle(); this.episodeLink = item.getLink(); this.feedAuthor = item.getFeed().getAuthor(); - if (!TextUtils.isEmpty(item.getImageUrl())) { + if (!StringUtils.isEmpty(item.getImageUrl())) { this.imageUrl = item.getImageUrl(); } else { this.imageUrl = item.getFeed().getImageUrl(); @@ -280,21 +280,21 @@ public class RemoteMedia implements Playable { public boolean equals(Object other) { if (other instanceof RemoteMedia) { RemoteMedia rm = (RemoteMedia) other; - return TextUtils.equals(downloadUrl, rm.downloadUrl) - && TextUtils.equals(feedUrl, rm.feedUrl) - && TextUtils.equals(itemIdentifier, rm.itemIdentifier); + return StringUtils.equals(downloadUrl, rm.downloadUrl) + && StringUtils.equals(feedUrl, rm.feedUrl) + && StringUtils.equals(itemIdentifier, rm.itemIdentifier); } if (other instanceof FeedMedia) { FeedMedia fm = (FeedMedia) other; - if (!TextUtils.equals(downloadUrl, fm.getStreamUrl())) { + if (!StringUtils.equals(downloadUrl, fm.getStreamUrl())) { return false; } FeedItem fi = fm.getItem(); - if (fi == null || !TextUtils.equals(itemIdentifier, fi.getItemIdentifier())) { + if (fi == null || !StringUtils.equals(itemIdentifier, fi.getItemIdentifier())) { return false; } Feed feed = fi.getFeed(); - return feed != null && TextUtils.equals(feedUrl, feed.getDownloadUrl()); + return feed != null && StringUtils.equals(feedUrl, feed.getDownloadUrl()); } return false; } diff --git a/model/src/test/java/de/danoeh/antennapod/model/VolumeAdaptionSettingTest.java b/model/src/test/java/de/danoeh/antennapod/model/VolumeAdaptionSettingTest.java new file mode 100644 index 000000000..265b425e8 --- /dev/null +++ b/model/src/test/java/de/danoeh/antennapod/model/VolumeAdaptionSettingTest.java @@ -0,0 +1,129 @@ +package de.danoeh.antennapod.model; + +import de.danoeh.antennapod.model.feed.VolumeAdaptionSetting; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertEquals; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertTrue; + +public class VolumeAdaptionSettingTest { + + @Before + public void setUp() throws Exception { + VolumeAdaptionSetting.setBoostSupported(false); + } + + @After + public void tearDown() throws Exception { + VolumeAdaptionSetting.setBoostSupported(null); + } + + @Test + public void mapOffToInteger() { + VolumeAdaptionSetting setting = VolumeAdaptionSetting.OFF; + assertThat(setting.toInteger(), is(equalTo(0))); + } + + @Test + public void mapLightReductionToInteger() { + VolumeAdaptionSetting setting = VolumeAdaptionSetting.LIGHT_REDUCTION; + + assertThat(setting.toInteger(), is(equalTo(1))); + } + + @Test + public void mapHeavyReductionToInteger() { + VolumeAdaptionSetting setting = VolumeAdaptionSetting.HEAVY_REDUCTION; + + assertThat(setting.toInteger(), is(equalTo(2))); + } + + @Test + public void mapLightBoostToInteger() { + VolumeAdaptionSetting setting = VolumeAdaptionSetting.LIGHT_BOOST; + + assertThat(setting.toInteger(), is(equalTo(3))); + } + + @Test + public void mapMediumBoostToInteger() { + VolumeAdaptionSetting setting = VolumeAdaptionSetting.MEDIUM_BOOST; + + assertThat(setting.toInteger(), is(equalTo(4))); + } + + @Test + public void mapHeavyBoostToInteger() { + VolumeAdaptionSetting setting = VolumeAdaptionSetting.HEAVY_BOOST; + + assertThat(setting.toInteger(), is(equalTo(5))); + } + + @Test + public void mapIntegerToVolumeAdaptionSetting() { + assertThat(VolumeAdaptionSetting.fromInteger(0), is(equalTo(VolumeAdaptionSetting.OFF))); + assertThat(VolumeAdaptionSetting.fromInteger(1), is(equalTo(VolumeAdaptionSetting.LIGHT_REDUCTION))); + assertThat(VolumeAdaptionSetting.fromInteger(2), is(equalTo(VolumeAdaptionSetting.HEAVY_REDUCTION))); + assertThat(VolumeAdaptionSetting.fromInteger(3), is(equalTo(VolumeAdaptionSetting.LIGHT_BOOST))); + assertThat(VolumeAdaptionSetting.fromInteger(4), is(equalTo(VolumeAdaptionSetting.MEDIUM_BOOST))); + assertThat(VolumeAdaptionSetting.fromInteger(5), is(equalTo(VolumeAdaptionSetting.HEAVY_BOOST))); + } + + @Test(expected = IllegalArgumentException.class) + public void cannotMapNegativeValues() { + VolumeAdaptionSetting.fromInteger(-1); + } + + @Test(expected = IllegalArgumentException.class) + public void cannotMapValuesOutOfRange() { + VolumeAdaptionSetting.fromInteger(6); + } + + @Test + public void noAdaptionIfTurnedOff() { + float adaptionFactor = VolumeAdaptionSetting.OFF.getAdaptionFactor(); + assertEquals(1.0f, adaptionFactor, 0.01f); + } + + @Test + public void lightReductionYieldsHigherValueThanHeavyReduction() { + float lightReductionFactor = VolumeAdaptionSetting.LIGHT_REDUCTION.getAdaptionFactor(); + + float heavyReductionFactor = VolumeAdaptionSetting.HEAVY_REDUCTION.getAdaptionFactor(); + + assertTrue("Light reduction must have higher factor than heavy reduction", lightReductionFactor > heavyReductionFactor); + } + + @Test + public void lightBoostYieldsHigherValueThanLightReduction() { + float lightReductionFactor = VolumeAdaptionSetting.LIGHT_REDUCTION.getAdaptionFactor(); + + float lightBoostFactor = VolumeAdaptionSetting.LIGHT_BOOST.getAdaptionFactor(); + + assertTrue("Light boost must have higher factor than light reduction", lightBoostFactor > lightReductionFactor); + } + + @Test + public void mediumBoostYieldsHigherValueThanLightBoost() { + float lightBoostFactor = VolumeAdaptionSetting.LIGHT_BOOST.getAdaptionFactor(); + + float mediumBoostFactor = VolumeAdaptionSetting.MEDIUM_BOOST.getAdaptionFactor(); + + assertTrue("Medium boost must have higher factor than light boost", mediumBoostFactor > lightBoostFactor); + } + + @Test + public void heavyBoostYieldsHigherValueThanMediumBoost() { + float mediumBoostFactor = VolumeAdaptionSetting.MEDIUM_BOOST.getAdaptionFactor(); + + float heavyBoostFactor = VolumeAdaptionSetting.HEAVY_BOOST.getAdaptionFactor(); + + assertTrue("Heavy boost must have higher factor than medium boost", heavyBoostFactor > mediumBoostFactor); + } +} diff --git a/net/download/service-interface/src/test/java/de/danoeh/antennapod/net/download/serviceinterface/FilenameGeneratorTest.java b/net/download/service-interface/src/test/java/de/danoeh/antennapod/net/download/serviceinterface/FilenameGeneratorTest.java new file mode 100644 index 000000000..521fc2d36 --- /dev/null +++ b/net/download/service-interface/src/test/java/de/danoeh/antennapod/net/download/serviceinterface/FilenameGeneratorTest.java @@ -0,0 +1,98 @@ +package de.danoeh.antennapod.net.download.serviceinterface; + +import androidx.test.platform.app.InstrumentationRegistry; +import android.text.TextUtils; + +import java.io.File; + +import org.apache.commons.lang3.StringUtils; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; + +@RunWith(RobolectricTestRunner.class) +public class FilenameGeneratorTest { + + public FilenameGeneratorTest() { + super(); + } + + @Test + public void testGenerateFileName() throws Exception { + String result = FileNameGenerator.generateFileName("abc abc"); + assertEquals(result, "abc abc"); + createFiles(result); + } + + @Test + public void testGenerateFileName1() throws Exception { + String result = FileNameGenerator.generateFileName("ab/c: + + + + + + + + + + + + + + diff --git a/net/download/service/src/main/java/de/danoeh/antennapod/net/download/service/ConnectivityActionReceiver.java b/net/download/service/src/main/java/de/danoeh/antennapod/net/download/service/ConnectivityActionReceiver.java new file mode 100644 index 000000000..3b733a482 --- /dev/null +++ b/net/download/service/src/main/java/de/danoeh/antennapod/net/download/service/ConnectivityActionReceiver.java @@ -0,0 +1,34 @@ +package de.danoeh.antennapod.net.download.service; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.net.ConnectivityManager; +import android.text.TextUtils; +import android.util.Log; + +import de.danoeh.antennapod.net.common.NetworkUtils; +import de.danoeh.antennapod.net.download.serviceinterface.AutoDownloadManager; +import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterface; + +public class ConnectivityActionReceiver extends BroadcastReceiver { + private static final String TAG = "ConnectivityActionRecvr"; + + @Override + public void onReceive(final Context context, Intent intent) { + if (TextUtils.equals(intent.getAction(), ConnectivityManager.CONNECTIVITY_ACTION)) { + Log.d(TAG, "Received intent"); + + if (NetworkUtils.isAutoDownloadAllowed()) { + Log.d(TAG, "auto-dl network available, starting auto-download"); + AutoDownloadManager.getInstance().autodownloadUndownloadedItems(context); + } else { // if new network is Wi-Fi, finish ongoing downloads, + // otherwise cancel all downloads + if (NetworkUtils.isNetworkRestricted()) { + Log.i(TAG, "Device is no longer connected to Wi-Fi. Cancelling ongoing downloads"); + DownloadServiceInterface.get().cancelAll(context); + } + } + } + } +} diff --git a/net/download/service/src/main/java/de/danoeh/antennapod/net/download/service/PowerConnectionReceiver.java b/net/download/service/src/main/java/de/danoeh/antennapod/net/download/service/PowerConnectionReceiver.java new file mode 100644 index 000000000..12a3f82e0 --- /dev/null +++ b/net/download/service/src/main/java/de/danoeh/antennapod/net/download/service/PowerConnectionReceiver.java @@ -0,0 +1,46 @@ +package de.danoeh.antennapod.net.download.service; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.util.Log; + +import de.danoeh.antennapod.net.download.serviceinterface.AutoDownloadManager; +import de.danoeh.antennapod.storage.preferences.UserPreferences; +import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterface; + +// modified from http://developer.android.com/training/monitoring-device-state/battery-monitoring.html +// and ConnectivityActionReceiver.java +// Updated based on http://stackoverflow.com/questions/20833241/android-charge-intent-has-no-extra-data +// Since the intent doesn't have the EXTRA_STATUS like the android.com article says it does +// (though it used to) +public class PowerConnectionReceiver extends BroadcastReceiver { + private static final String TAG = "PowerConnectionReceiver"; + + @Override + public void onReceive(Context context, Intent intent) { + final String action = intent.getAction(); + + Log.d(TAG, "charging intent: " + action); + + if (Intent.ACTION_POWER_CONNECTED.equals(action)) { + Log.d(TAG, "charging, starting auto-download"); + // we're plugged in, this is a great time to auto-download if everything else is + // right. So, even if the user allows auto-dl on battery, let's still start + // downloading now. They shouldn't mind. + // autodownloadUndownloadedItems will make sure we're on the right wifi networks, + // etc... so we don't have to worry about it. + AutoDownloadManager.getInstance().autodownloadUndownloadedItems(context); + } else { + // if we're not supposed to be auto-downloading when we're not charging, stop it + if (!UserPreferences.isEnableAutodownloadOnBattery()) { + Log.d(TAG, "not charging anymore, canceling auto-download"); + DownloadServiceInterface.get().cancelAll(context); + } else { + Log.d(TAG, "not charging anymore, but the user allows auto-download " + + "when on battery so we'll keep going"); + } + } + + } +} diff --git a/storage/database/build.gradle b/storage/database/build.gradle index 63f9eeaec..a78afbf79 100644 --- a/storage/database/build.gradle +++ b/storage/database/build.gradle @@ -28,6 +28,9 @@ dependencies { implementation "commons-io:commons-io:$commonsioVersion" implementation "org.greenrobot:eventbus:$eventbusVersion" implementation "com.google.guava:guava:31.0.1-android" + implementation "org.apache.commons:commons-lang3:$commonslangVersion" testImplementation "junit:junit:$junitVersion" + testImplementation "androidx.test:core:$testCoreVersion" + testImplementation "org.robolectric:robolectric:$robolectricVersion" } diff --git a/storage/database/src/main/java/de/danoeh/antennapod/storage/database/FeedItemDuplicateGuesser.java b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/FeedItemDuplicateGuesser.java index bbaedb519..19a2bd5f1 100644 --- a/storage/database/src/main/java/de/danoeh/antennapod/storage/database/FeedItemDuplicateGuesser.java +++ b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/FeedItemDuplicateGuesser.java @@ -1,11 +1,11 @@ package de.danoeh.antennapod.storage.database; -import android.text.TextUtils; import de.danoeh.antennapod.model.feed.FeedItem; import de.danoeh.antennapod.model.feed.FeedMedia; import java.text.DateFormat; import java.util.Locale; +import org.apache.commons.lang3.StringUtils; /** * Publishers sometimes mess up their feed by adding episodes twice or by changing the ID of existing episodes. @@ -32,7 +32,7 @@ public class FeedItemDuplicateGuesser { } public static boolean sameAndNotEmpty(String string1, String string2) { - if (TextUtils.isEmpty(string1) || TextUtils.isEmpty(string2)) { + if (StringUtils.isEmpty(string1) || StringUtils.isEmpty(string2)) { return false; } return string1.equals(string2); @@ -45,7 +45,7 @@ public class FeedItemDuplicateGuesser { DateFormat dateFormat = DateFormat.getDateInstance(DateFormat.SHORT, Locale.US); // MM/DD/YY String dateOriginal = dateFormat.format(item2.getPubDate()); String dateNew = dateFormat.format(item1.getPubDate()); - return TextUtils.equals(dateOriginal, dateNew); // Same date; time is ignored. + return StringUtils.equals(dateOriginal, dateNew); // Same date; time is ignored. } private static boolean durationsLookSimilar(FeedMedia media1, FeedMedia media2) { @@ -62,7 +62,7 @@ public class FeedItemDuplicateGuesser { mimeType1 = mimeType1.substring(0, mimeType1.indexOf("/")); mimeType2 = mimeType2.substring(0, mimeType2.indexOf("/")); } - return TextUtils.equals(mimeType1, mimeType2); + return StringUtils.equals(mimeType1, mimeType2); } private static boolean titlesLookSimilar(FeedItem item1, FeedItem item2) { diff --git a/storage/database/src/test/java/de/danoeh/antennapod/storage/database/FeedItemPermutorsTest.java b/storage/database/src/test/java/de/danoeh/antennapod/storage/database/FeedItemPermutorsTest.java new file mode 100644 index 000000000..9e0f106fd --- /dev/null +++ b/storage/database/src/test/java/de/danoeh/antennapod/storage/database/FeedItemPermutorsTest.java @@ -0,0 +1,231 @@ +package de.danoeh.antennapod.storage.database; + +import de.danoeh.antennapod.model.feed.SortOrder; +import de.danoeh.antennapod.storage.database.FeedItemPermutors; +import de.danoeh.antennapod.storage.database.Permutor; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.List; + +import de.danoeh.antennapod.model.feed.Feed; +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedMedia; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +/** + * Test class for FeedItemPermutors. + */ +public class FeedItemPermutorsTest { + + @Test + public void testEnsureNonNullPermutors() { + for (SortOrder sortOrder : SortOrder.values()) { + assertNotNull("The permutor for SortOrder " + sortOrder + " is unexpectedly null", + FeedItemPermutors.getPermutor(sortOrder)); + } + } + + @Test + public void testPermutorForRule_EPISODE_TITLE_ASC() { + Permutor permutor = FeedItemPermutors.getPermutor(SortOrder.EPISODE_TITLE_A_Z); + + List itemList = getTestList(); + assertTrue(checkIdOrder(itemList, 1, 3, 2)); // before sorting + permutor.reorder(itemList); + assertTrue(checkIdOrder(itemList, 1, 2, 3)); // after sorting + } + + @Test + public void testPermutorForRule_EPISODE_TITLE_ASC_NullTitle() { + Permutor permutor = FeedItemPermutors.getPermutor(SortOrder.EPISODE_TITLE_A_Z); + + List itemList = getTestList(); + itemList.get(2) // itemId 2 + .setTitle(null); + assertTrue(checkIdOrder(itemList, 1, 3, 2)); // before sorting + permutor.reorder(itemList); + assertTrue(checkIdOrder(itemList, 2, 1, 3)); // after sorting + } + + + @Test + public void testPermutorForRule_EPISODE_TITLE_DESC() { + Permutor permutor = FeedItemPermutors.getPermutor(SortOrder.EPISODE_TITLE_Z_A); + + List itemList = getTestList(); + assertTrue(checkIdOrder(itemList, 1, 3, 2)); // before sorting + permutor.reorder(itemList); + assertTrue(checkIdOrder(itemList, 3, 2, 1)); // after sorting + } + + @Test + public void testPermutorForRule_DATE_ASC() { + Permutor permutor = FeedItemPermutors.getPermutor(SortOrder.DATE_OLD_NEW); + + List itemList = getTestList(); + assertTrue(checkIdOrder(itemList, 1, 3, 2)); // before sorting + permutor.reorder(itemList); + assertTrue(checkIdOrder(itemList, 1, 2, 3)); // after sorting + } + + @Test + public void testPermutorForRule_DATE_ASC_NulPubDatel() { + Permutor permutor = FeedItemPermutors.getPermutor(SortOrder.DATE_OLD_NEW); + + List itemList = getTestList(); + itemList.get(2) // itemId 2 + .setPubDate(null); + assertTrue(checkIdOrder(itemList, 1, 3, 2)); // before sorting + permutor.reorder(itemList); + assertTrue(checkIdOrder(itemList, 2, 1, 3)); // after sorting + } + + @Test + public void testPermutorForRule_DATE_DESC() { + Permutor permutor = FeedItemPermutors.getPermutor(SortOrder.DATE_NEW_OLD); + + List itemList = getTestList(); + assertTrue(checkIdOrder(itemList, 1, 3, 2)); // before sorting + permutor.reorder(itemList); + assertTrue(checkIdOrder(itemList, 3, 2, 1)); // after sorting + } + + @Test + public void testPermutorForRule_DURATION_ASC() { + Permutor permutor = FeedItemPermutors.getPermutor(SortOrder.DURATION_SHORT_LONG); + + List itemList = getTestList(); + assertTrue(checkIdOrder(itemList, 1, 3, 2)); // before sorting + permutor.reorder(itemList); + assertTrue(checkIdOrder(itemList, 1, 2, 3)); // after sorting + } + + @Test + public void testPermutorForRule_DURATION_DESC() { + Permutor permutor = FeedItemPermutors.getPermutor(SortOrder.DURATION_LONG_SHORT); + + List itemList = getTestList(); + assertTrue(checkIdOrder(itemList, 1, 3, 2)); // before sorting + permutor.reorder(itemList); + assertTrue(checkIdOrder(itemList, 3, 2, 1)); // after sorting + } + + @Test + public void testPermutorForRule_size_asc() { + Permutor permutor = FeedItemPermutors.getPermutor(SortOrder.SIZE_SMALL_LARGE); + + List itemList = getTestList(); + assertTrue(checkIdOrder(itemList, 1, 3, 2)); // before sorting + permutor.reorder(itemList); + assertTrue(checkIdOrder(itemList, 1, 2, 3)); // after sorting + } + + @Test + public void testPermutorForRule_size_desc() { + Permutor permutor = FeedItemPermutors.getPermutor(SortOrder.SIZE_LARGE_SMALL); + + List itemList = getTestList(); + assertTrue(checkIdOrder(itemList, 1, 3, 2)); // before sorting + permutor.reorder(itemList); + assertTrue(checkIdOrder(itemList, 3, 2, 1)); // after sorting + } + + @Test + public void testPermutorForRule_DURATION_DESC_NullMedia() { + Permutor permutor = FeedItemPermutors.getPermutor(SortOrder.DURATION_LONG_SHORT); + + List itemList = getTestList(); + itemList.get(1) // itemId 3 + .setMedia(null); + assertTrue(checkIdOrder(itemList, 1, 3, 2)); // before sorting + permutor.reorder(itemList); + assertTrue(checkIdOrder(itemList, 2, 1, 3)); // after sorting + } + + @Test + public void testPermutorForRule_FEED_TITLE_ASC() { + Permutor permutor = FeedItemPermutors.getPermutor(SortOrder.FEED_TITLE_A_Z); + + List itemList = getTestList(); + assertTrue(checkIdOrder(itemList, 1, 3, 2)); // before sorting + permutor.reorder(itemList); + assertTrue(checkIdOrder(itemList, 1, 2, 3)); // after sorting + } + + @Test + public void testPermutorForRule_FEED_TITLE_DESC() { + Permutor permutor = FeedItemPermutors.getPermutor(SortOrder.FEED_TITLE_Z_A); + + List itemList = getTestList(); + assertTrue(checkIdOrder(itemList, 1, 3, 2)); // before sorting + permutor.reorder(itemList); + assertTrue(checkIdOrder(itemList, 3, 2, 1)); // after sorting + } + + @Test + public void testPermutorForRule_FEED_TITLE_DESC_NullTitle() { + Permutor permutor = FeedItemPermutors.getPermutor(SortOrder.FEED_TITLE_Z_A); + + List itemList = getTestList(); + itemList.get(1) // itemId 3 + .getFeed().setTitle(null); + assertTrue(checkIdOrder(itemList, 1, 3, 2)); // before sorting + permutor.reorder(itemList); + assertTrue(checkIdOrder(itemList, 2, 1, 3)); // after sorting + } + + /** + * Generates a list with test data. + */ + private List getTestList() { + List itemList = new ArrayList<>(); + + Calendar calendar = Calendar.getInstance(); + calendar.set(2019, 0, 1); // January 1st + Feed feed1 = new Feed(null, null, "Feed title 1"); + FeedItem feedItem1 = new FeedItem(1, "Title 1", null, null, calendar.getTime(), 0, feed1); + FeedMedia feedMedia1 = new FeedMedia(0, feedItem1, 1000, 0, 100, null, null, null, true, null, 0, 0); + feedItem1.setMedia(feedMedia1); + itemList.add(feedItem1); + + calendar.set(2019, 2, 1); // March 1st + Feed feed2 = new Feed(null, null, "Feed title 3"); + FeedItem feedItem2 = new FeedItem(3, "Title 3", null, null, calendar.getTime(), 0, feed2); + FeedMedia feedMedia2 = new FeedMedia(0, feedItem2, 3000, 0, 300, null, null, null, true, null, 0, 0); + feedItem2.setMedia(feedMedia2); + itemList.add(feedItem2); + + calendar.set(2019, 1, 1); // February 1st + Feed feed3 = new Feed(null, null, "Feed title 2"); + FeedItem feedItem3 = new FeedItem(2, "Title 2", null, null, calendar.getTime(), 0, feed3); + FeedMedia feedMedia3 = new FeedMedia(0, feedItem3, 2000, 0, 200, null, null, null, true, null, 0, 0); + feedItem3.setMedia(feedMedia3); + itemList.add(feedItem3); + + return itemList; + } + + /** + * Checks if both lists have the same size and the same ID order. + * + * @param itemList Item list. + * @param ids List of IDs. + * @return true if both lists have the same size and the same ID order. + */ + private boolean checkIdOrder(List itemList, long... ids) { + if (itemList.size() != ids.length) { + return false; + } + + for (int i = 0; i < ids.length; i++) { + if (itemList.get(i).getId() != ids[i]) { + return false; + } + } + return true; + } +} diff --git a/storage/database/src/test/java/de/danoeh/antennapod/storage/database/mapper/FeedCursorMapperTest.java b/storage/database/src/test/java/de/danoeh/antennapod/storage/database/mapper/FeedCursorMapperTest.java new file mode 100644 index 000000000..5e7a4843e --- /dev/null +++ b/storage/database/src/test/java/de/danoeh/antennapod/storage/database/mapper/FeedCursorMapperTest.java @@ -0,0 +1,100 @@ +package de.danoeh.antennapod.storage.database.mapper; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; + +import androidx.test.platform.app.InstrumentationRegistry; + +import de.danoeh.antennapod.storage.database.PodDBAdapter; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +import de.danoeh.antennapod.model.feed.Feed; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +@RunWith(RobolectricTestRunner.class) +public class FeedCursorMapperTest { + private PodDBAdapter adapter; + + @Before + public void setUp() { + Context context = InstrumentationRegistry.getInstrumentation().getContext(); + + PodDBAdapter.init(context); + adapter = PodDBAdapter.getInstance(); + + writeFeedToDatabase(); + } + + @After + public void tearDown() { + PodDBAdapter.tearDownTests(); + } + + @SuppressWarnings("ConstantConditions") + @Test + public void testFromCursor() { + try (Cursor cursor = adapter.getAllFeedsCursor()) { + cursor.moveToNext(); + Feed feed = FeedCursorMapper.convert(cursor); + assertTrue(feed.getId() >= 0); + assertEquals("feed custom title", feed.getTitle()); + assertEquals("feed custom title", feed.getCustomTitle()); + assertEquals("feed link", feed.getLink()); + assertEquals("feed description", feed.getDescription()); + assertEquals("feed payment link", feed.getPaymentLinks().get(0).url); + assertEquals("feed author", feed.getAuthor()); + assertEquals("feed language", feed.getLanguage()); + assertEquals("feed image url", feed.getImageUrl()); + assertEquals("feed file url", feed.getLocalFileUrl()); + assertEquals("feed download url", feed.getDownloadUrl()); + assertEquals(42, feed.getLastRefreshAttempt()); + assertEquals("feed last update", feed.getLastModified()); + assertEquals("feed type", feed.getType()); + assertEquals("feed identifier", feed.getFeedIdentifier()); + assertTrue(feed.isPaged()); + assertEquals("feed next page link", feed.getNextPageLink()); + assertTrue(feed.getItemFilter().showUnplayed); + assertEquals(1, feed.getSortOrder().code); + assertTrue(feed.hasLastUpdateFailed()); + } + } + + /** + * Insert test data to the database. + * Uses raw database insert instead of adapter.setCompleteFeed() to avoid testing the Feed class + * against itself. + */ + private void writeFeedToDatabase() { + ContentValues values = new ContentValues(); + values.put(PodDBAdapter.KEY_TITLE, "feed title"); + values.put(PodDBAdapter.KEY_CUSTOM_TITLE, "feed custom title"); + values.put(PodDBAdapter.KEY_LINK, "feed link"); + values.put(PodDBAdapter.KEY_DESCRIPTION, "feed description"); + values.put(PodDBAdapter.KEY_PAYMENT_LINK, "feed payment link"); + values.put(PodDBAdapter.KEY_AUTHOR, "feed author"); + values.put(PodDBAdapter.KEY_LANGUAGE, "feed language"); + values.put(PodDBAdapter.KEY_IMAGE_URL, "feed image url"); + + values.put(PodDBAdapter.KEY_FILE_URL, "feed file url"); + values.put(PodDBAdapter.KEY_DOWNLOAD_URL, "feed download url"); + values.put(PodDBAdapter.KEY_LAST_REFRESH_ATTEMPT, 42); + values.put(PodDBAdapter.KEY_LASTUPDATE, "feed last update"); + values.put(PodDBAdapter.KEY_TYPE, "feed type"); + values.put(PodDBAdapter.KEY_FEED_IDENTIFIER, "feed identifier"); + + values.put(PodDBAdapter.KEY_IS_PAGED, true); + values.put(PodDBAdapter.KEY_NEXT_PAGE_LINK, "feed next page link"); + values.put(PodDBAdapter.KEY_HIDE, "unplayed"); + values.put(PodDBAdapter.KEY_SORT_ORDER, "1"); + values.put(PodDBAdapter.KEY_LAST_UPDATE_FAILED, true); + + adapter.insertTestData(PodDBAdapter.TABLE_NAME_FEEDS, values); + } +} diff --git a/ui/common/build.gradle b/ui/common/build.gradle index 26db9f9e4..1325761d3 100644 --- a/ui/common/build.gradle +++ b/ui/common/build.gradle @@ -16,4 +16,6 @@ dependencies { implementation "androidx.viewpager2:viewpager2:$viewPager2Version" implementation "com.google.android.material:material:$googleMaterialVersion" implementation "androidx.core:core-splashscreen:1.0.0" + + testImplementation "junit:junit:$junitVersion" } diff --git a/ui/common/src/test/java/de/danoeh/antennapod/ui/common/ConverterTest.java b/ui/common/src/test/java/de/danoeh/antennapod/ui/common/ConverterTest.java new file mode 100644 index 000000000..516490ff3 --- /dev/null +++ b/ui/common/src/test/java/de/danoeh/antennapod/ui/common/ConverterTest.java @@ -0,0 +1,40 @@ +package de.danoeh.antennapod.ui.common; + +import de.danoeh.antennapod.ui.common.Converter; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +/** + * Test class for converter + */ +public class ConverterTest { + + @Test + public void testGetDurationStringLong() { + String expected = "13:05:10"; + int input = 47110000; + assertEquals(expected, Converter.getDurationStringLong(input)); + } + + @Test + public void testGetDurationStringShort() { + String expected = "13:05"; + assertEquals(expected, Converter.getDurationStringShort(47110000, true)); + assertEquals(expected, Converter.getDurationStringShort(785000, false)); + } + + @Test + public void testDurationStringLongToMs() { + String input = "01:20:30"; + long expected = 4830000; + assertEquals(expected, Converter.durationStringLongToMs(input)); + } + + @Test + public void testDurationStringShortToMs() { + String input = "8:30"; + assertEquals(30600000, Converter.durationStringShortToMs(input, true)); + assertEquals(510000, Converter.durationStringShortToMs(input, false)); + } +} diff --git a/ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/StatisticsFragment.java b/ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/StatisticsFragment.java index c5ec4cb59..a2f9e8146 100644 --- a/ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/StatisticsFragment.java +++ b/ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/StatisticsFragment.java @@ -18,7 +18,7 @@ import androidx.viewpager2.widget.ViewPager2; import com.google.android.material.tabs.TabLayout; import com.google.android.material.tabs.TabLayoutMediator; -import de.danoeh.antennapod.core.dialog.ConfirmationDialog; +import de.danoeh.antennapod.core.util.ConfirmationDialog; import de.danoeh.antennapod.storage.database.DBWriter; import de.danoeh.antennapod.event.StatisticsEvent; import de.danoeh.antennapod.ui.common.PagedToolbarFragment; -- cgit v1.2.3