summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/ISSUE_TEMPLATE/bug_report.yml2
-rw-r--r--.github/ISSUE_TEMPLATE/feature_request.yml2
-rw-r--r--.github/workflows/checks.yml29
-rw-r--r--.github/workflows/close-if-no-reply.yml12
-rw-r--r--.tx/config74
-rw-r--r--CONTRIBUTING.md4
-rw-r--r--README.md2
-rw-r--r--app/build.gradle18
-rw-r--r--app/src/androidTest/java/de/danoeh/antennapod/core/service/download/StubDownloader.java1
-rw-r--r--app/src/androidTest/java/de/test/antennapod/EspressoTestUtils.java2
-rw-r--r--app/src/androidTest/java/de/test/antennapod/dialogs/ShareDialogTest.java4
-rw-r--r--app/src/androidTest/java/de/test/antennapod/service/download/DownloadServiceTest.java2
-rw-r--r--app/src/androidTest/java/de/test/antennapod/service/download/HttpDownloaderTest.java4
-rw-r--r--app/src/androidTest/java/de/test/antennapod/service/playback/PlaybackServiceMediaPlayerTest.java2
-rw-r--r--app/src/androidTest/java/de/test/antennapod/service/playback/PlaybackServiceTaskManagerTest.java2
-rw-r--r--app/src/androidTest/java/de/test/antennapod/storage/AutoDownloadTest.java2
-rw-r--r--app/src/androidTest/java/de/test/antennapod/ui/MainActivityTest.java2
-rw-r--r--app/src/androidTest/java/de/test/antennapod/ui/PreferencesTest.java11
-rw-r--r--app/src/androidTest/java/de/test/antennapod/ui/UITestUtils.java2
-rw-r--r--app/src/androidTest/java/de/test/antennapod/util/syndication/feedgenerator/Rss2Generator.java6
-rw-r--r--app/src/main/AndroidManifest.xml18
-rw-r--r--app/src/main/java/de/danoeh/antennapod/PodcastApp.java4
-rw-r--r--app/src/main/java/de/danoeh/antennapod/activity/BugReportActivity.java28
-rw-r--r--app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java5
-rw-r--r--app/src/main/java/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java21
-rw-r--r--app/src/main/java/de/danoeh/antennapod/activity/PlaybackSpeedDialogActivity.java29
-rw-r--r--app/src/main/java/de/danoeh/antennapod/activity/PreferenceActivity.java7
-rw-r--r--app/src/main/java/de/danoeh/antennapod/activity/SplashActivity.java2
-rw-r--r--app/src/main/java/de/danoeh/antennapod/activity/VideoplayerActivity.java18
-rw-r--r--app/src/main/java/de/danoeh/antennapod/activity/WidgetConfigActivity.java17
-rw-r--r--app/src/main/java/de/danoeh/antennapod/adapter/DownloadLogAdapter.java20
-rw-r--r--app/src/main/java/de/danoeh/antennapod/adapter/FeedDiscoverAdapter.java2
-rw-r--r--app/src/main/java/de/danoeh/antennapod/adapter/FeedItemlistDescriptionAdapter.java2
-rw-r--r--app/src/main/java/de/danoeh/antennapod/adapter/PlaybackStatisticsListAdapter.java69
-rw-r--r--app/src/main/java/de/danoeh/antennapod/adapter/SubscriptionsRecyclerAdapter.java3
-rw-r--r--app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/PlayActionButton.java2
-rw-r--r--app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/PlayLocalActionButton.java2
-rw-r--r--app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/StreamActionButton.java2
-rw-r--r--app/src/main/java/de/danoeh/antennapod/adapter/itunes/ItunesAdapter.java4
-rw-r--r--app/src/main/java/de/danoeh/antennapod/asynctask/DocumentFileExportWorker.java2
-rw-r--r--app/src/main/java/de/danoeh/antennapod/dialog/AllEpisodesFilterDialog.java31
-rw-r--r--app/src/main/java/de/danoeh/antennapod/dialog/DownloadLogDetailsDialog.java59
-rw-r--r--app/src/main/java/de/danoeh/antennapod/dialog/FeedItemFilterDialog.java26
-rw-r--r--app/src/main/java/de/danoeh/antennapod/dialog/FeedRefreshIntervalDialog.java6
-rw-r--r--app/src/main/java/de/danoeh/antennapod/dialog/FilterDialog.java83
-rw-r--r--app/src/main/java/de/danoeh/antennapod/dialog/ItemFilterDialog.java75
-rw-r--r--app/src/main/java/de/danoeh/antennapod/dialog/PlaybackControlsDialog.java28
-rw-r--r--app/src/main/java/de/danoeh/antennapod/dialog/ProxyDialog.java6
-rw-r--r--app/src/main/java/de/danoeh/antennapod/dialog/ShareDialog.java60
-rw-r--r--app/src/main/java/de/danoeh/antennapod/dialog/StreamingConfirmationDialog.java2
-rw-r--r--app/src/main/java/de/danoeh/antennapod/dialog/SubscriptionsFilterDialog.java2
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/AddFeedFragment.java18
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/AllEpisodesFragment.java42
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/ChaptersFragment.java12
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/CompletedDownloadsFragment.java1
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/DiscoveryFragment.java4
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/DownloadLogFragment.java63
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/DownloadsFragment.java1
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/EpisodesFragment.java1
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/EpisodesListFragment.java2
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/FeedInfoFragment.java13
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/FeedItemlistFragment.java275
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/ItemFragment.java14
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/OnlineSearchFragment.java6
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/QuickFeedDiscoveryFragment.java13
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/SearchFragment.java4
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/SubscriptionFragment.java4
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/actions/FeedMultiSelectActionHandler.java5
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/GpodnetMainFragment.java2
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/TagFragment.java2
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/preferences/ImportExportPreferencesFragment.java75
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/preferences/MainPreferencesFragment.java9
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/preferences/PlaybackPreferencesFragment.java9
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/preferences/PlaybackStatisticsFragment.java197
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/preferences/StatisticsFragment.java106
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/preferences/StoragePreferencesFragment.java1
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/preferences/SwipePreferencesFragment.java2
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/preferences/about/ContributorsPagerFragment.java7
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/preferences/synchronization/GpodderAuthenticationFragment.java12
-rw-r--r--app/src/main/java/de/danoeh/antennapod/menuhandler/FeedMenuHandler.java34
-rw-r--r--app/src/main/java/de/danoeh/antennapod/preferences/PreferenceUpgrader.java6
-rw-r--r--app/src/main/java/de/danoeh/antennapod/view/TimePicker.java31
-rw-r--r--app/src/main/java/de/danoeh/antennapod/view/viewholder/EpisodeItemViewHolder.java2
-rw-r--r--app/src/main/res/layout-sw720dp/main.xml6
-rw-r--r--app/src/main/res/layout/activity_widget_config.xml144
-rw-r--r--app/src/main/res/layout/audio_controls.xml25
-rw-r--r--app/src/main/res/layout/audioplayer_fragment.xml443
-rw-r--r--app/src/main/res/layout/feed_item_list_fragment.xml10
-rw-r--r--app/src/main/res/layout/feed_refresh_dialog.xml5
-rw-r--r--app/src/main/res/layout/feedinfo.xml9
-rw-r--r--app/src/main/res/layout/feeditemlist_header.xml261
-rw-r--r--app/src/main/res/layout/filter_dialog_row.xml21
-rw-r--r--app/src/main/res/layout/main.xml6
-rw-r--r--app/src/main/res/layout/share_episode_dialog.xml20
-rw-r--r--app/src/main/res/layout/statistics_mode_select_dialog.xml25
-rw-r--r--app/src/main/res/menu/feedinfo.xml17
-rw-r--r--app/src/main/res/menu/feeditem_options.xml2
-rw-r--r--app/src/main/res/menu/feedlist.xml24
-rw-r--r--app/src/main/res/menu/mediaplayer.xml10
-rw-r--r--app/src/main/res/menu/subscriptions.xml5
-rw-r--r--app/src/main/res/xml/preferences.xml5
-rw-r--r--app/src/main/res/xml/preferences_playback.xml7
-rw-r--r--build.gradle2
-rw-r--r--common.gradle14
-rw-r--r--core/build.gradle11
-rw-r--r--core/lint.xml8
-rw-r--r--core/src/main/AndroidManifest.xml10
-rw-r--r--core/src/main/java/androidx/core/app/SafeJobIntentService.java118
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/ClientConfig.java3
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/dialog/ConfirmationDialog.java63
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/feed/LocalFeedUpdater.java86
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/glide/ApGlideModule.java1
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/glide/AudioCoverFetcher.java3
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/glide/GenerativePlaceholderImageModelLoader.java139
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/preferences/PlaybackPreferences.java12
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/preferences/UsageStatistics.java21
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/preferences/UserPreferences.java101
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/receiver/PlayerWidget.java17
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/BasicAuthorizationInterceptor.java17
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/download/AntennapodHttpClient.java56
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadService.java15
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadServiceNotification.java61
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadStatus.java171
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/download/Downloader.java7
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/download/HttpCredentialEncoder.java18
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/download/HttpDownloader.java184
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/download/NewEpisodesNotification.java16
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedParserTask.java19
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedSyncTask.java2
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/download/handler/MediaDownloadedHandler.java7
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/playback/ExoPlayerWrapper.java4
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/playback/LocalPSMP.java13
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java100
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceStateManager.java4
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/storage/AutomaticDownloadAlgorithm.java2
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java110
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java58
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java10
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/storage/DatabaseExporter.java3
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/storage/EpisodeCleanupAlgorithmFactory.java22
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/storage/FeedSearcher.java9
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/storage/NavDrawerData.java6
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/storage/StatisticsItem.java16
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/storage/mapper/ChapterCursorMapper.java51
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/ChapterUtils.java38
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/DateFormatter.java2
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/DownloadError.java60
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/DownloadErrorLabel.java46
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/LongIntMap.java263
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/NetworkUtils.java13
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/PowerUtils.java2
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/ShareUtils.java46
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/comparator/DownloadStatusComparator.java2
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java7
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackServiceStarter.java37
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/widget/WidgetUpdater.java19
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/widget/WidgetUpdaterJobService.java35
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/widget/WidgetUpdaterWorker.java57
-rw-r--r--core/src/main/res/drawable-xxxhdpi/chart_box_outline.xml7
-rw-r--r--core/src/main/res/drawable/ic_filter_white.xml7
-rw-r--r--core/src/main/res/drawable/ic_rounded_corner_left.xml7
-rw-r--r--core/src/main/res/drawable/ic_rounded_corner_right.xml7
-rw-r--r--core/src/main/res/drawable/ic_sort.xml5
-rw-r--r--core/src/main/res/drawable/ic_statistics.xml5
-rw-r--r--core/src/main/res/layout/player_widget.xml10
-rw-r--r--core/src/main/res/layout/popup_bubble_view.xml4
-rw-r--r--core/src/main/res/raw/local_feed_default_icon.pngbin1240 -> 0 bytes
-rw-r--r--core/src/main/res/values/arrays.xml22
-rw-r--r--core/src/main/res/values/attrs.xml1
-rw-r--r--core/src/main/res/values/styles.xml14
-rw-r--r--core/src/play/java/de/danoeh/antennapod/core/service/playback/WearMediaSession.java2
-rw-r--r--core/src/test/java/de/danoeh/antennapod/core/feed/LocalFeedUpdaterTest.java32
-rw-r--r--core/src/test/java/de/danoeh/antennapod/core/storage/DbCleanupTests.java1
-rw-r--r--core/src/test/java/de/danoeh/antennapod/core/storage/DbNullCleanupAlgorithmTest.java1
-rw-r--r--core/src/test/java/de/danoeh/antennapod/core/storage/DbReaderTest.java1
-rw-r--r--core/src/test/java/de/danoeh/antennapod/core/storage/DbTasksTest.java1
-rw-r--r--core/src/test/java/de/danoeh/antennapod/core/storage/DbTestUtils.java4
-rw-r--r--core/src/test/java/de/danoeh/antennapod/core/storage/DbWriterTest.java1
-rw-r--r--core/src/test/java/de/danoeh/antennapod/core/storage/mapper/FeedCursorMapperTest.java3
-rw-r--r--core/src/test/java/de/danoeh/antennapod/core/util/LongLongMapTest.java67
-rw-r--r--event/build.gradle4
-rw-r--r--event/src/main/java/de/danoeh/antennapod/event/FavoritesEvent.java21
-rw-r--r--event/src/main/java/de/danoeh/antennapod/event/FeedItemEvent.java22
-rw-r--r--event/src/main/java/de/danoeh/antennapod/event/StatisticsEvent.java7
-rw-r--r--model/build.gradle4
-rw-r--r--model/src/main/java/de/danoeh/antennapod/model/download/DownloadError.java49
-rw-r--r--model/src/main/java/de/danoeh/antennapod/model/download/DownloadStatus.java153
-rw-r--r--model/src/main/java/de/danoeh/antennapod/model/download/ProxyConfig.java (renamed from core/src/main/java/de/danoeh/antennapod/core/service/download/ProxyConfig.java)2
-rw-r--r--model/src/main/java/de/danoeh/antennapod/model/feed/Chapter.java32
-rw-r--r--model/src/main/java/de/danoeh/antennapod/model/feed/Feed.java1
-rw-r--r--model/src/main/java/de/danoeh/antennapod/model/feed/FeedCounter.java24
-rw-r--r--model/src/main/java/de/danoeh/antennapod/model/feed/FeedItem.java15
-rw-r--r--model/src/main/java/de/danoeh/antennapod/model/feed/FeedItemFilter.java4
-rw-r--r--model/src/main/java/de/danoeh/antennapod/model/feed/FeedMedia.java4
-rw-r--r--model/src/main/java/de/danoeh/antennapod/model/feed/SubscriptionsFilter.java (renamed from core/src/main/java/de/danoeh/antennapod/core/feed/SubscriptionsFilter.java)12
-rw-r--r--model/src/main/java/de/danoeh/antennapod/model/playback/MediaType.java26
-rw-r--r--net/discovery/README.md3
-rw-r--r--net/discovery/build.gradle37
-rw-r--r--net/discovery/src/main/AndroidManifest.xml1
-rw-r--r--net/discovery/src/main/java/de/danoeh/antennapod/net/discovery/CombinedSearcher.java (renamed from app/src/main/java/de/danoeh/antennapod/discovery/CombinedSearcher.java)2
-rw-r--r--net/discovery/src/main/java/de/danoeh/antennapod/net/discovery/FyydPodcastSearcher.java (renamed from app/src/main/java/de/danoeh/antennapod/discovery/FyydPodcastSearcher.java)2
-rw-r--r--net/discovery/src/main/java/de/danoeh/antennapod/net/discovery/GpodnetPodcastSearcher.java (renamed from app/src/main/java/de/danoeh/antennapod/discovery/GpodnetPodcastSearcher.java)2
-rw-r--r--net/discovery/src/main/java/de/danoeh/antennapod/net/discovery/ItunesPodcastSearcher.java (renamed from app/src/main/java/de/danoeh/antennapod/discovery/ItunesPodcastSearcher.java)2
-rw-r--r--net/discovery/src/main/java/de/danoeh/antennapod/net/discovery/ItunesTopListLoader.java (renamed from app/src/main/java/de/danoeh/antennapod/discovery/ItunesTopListLoader.java)3
-rw-r--r--net/discovery/src/main/java/de/danoeh/antennapod/net/discovery/PodcastIndexPodcastSearcher.java (renamed from app/src/main/java/de/danoeh/antennapod/discovery/PodcastIndexPodcastSearcher.java)4
-rw-r--r--net/discovery/src/main/java/de/danoeh/antennapod/net/discovery/PodcastSearchResult.java (renamed from app/src/main/java/de/danoeh/antennapod/discovery/PodcastSearchResult.java)6
-rw-r--r--net/discovery/src/main/java/de/danoeh/antennapod/net/discovery/PodcastSearcher.java (renamed from app/src/main/java/de/danoeh/antennapod/discovery/PodcastSearcher.java)2
-rw-r--r--net/discovery/src/main/java/de/danoeh/antennapod/net/discovery/PodcastSearcherRegistry.java (renamed from app/src/main/java/de/danoeh/antennapod/discovery/PodcastSearcherRegistry.java)3
-rw-r--r--net/ssl/build.gradle4
-rw-r--r--net/sync/gpoddernet/build.gradle4
-rw-r--r--net/sync/gpoddernet/src/main/java/de/danoeh/antennapod/net/sync/gpoddernet/GpodnetService.java73
-rw-r--r--net/sync/model/build.gradle4
-rw-r--r--parser/feed/build.gradle4
-rw-r--r--parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/PodcastIndexChapterParser.java31
-rw-r--r--parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/element/SimpleChapter.java16
-rw-r--r--parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/namespace/Atom.java12
-rw-r--r--parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/namespace/DublinCore.java1
-rw-r--r--parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/namespace/Itunes.java2
-rw-r--r--parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/namespace/Media.java25
-rw-r--r--parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/namespace/Namespace.java5
-rw-r--r--parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/namespace/PodcastIndex.java10
-rw-r--r--parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/namespace/Rss20.java14
-rw-r--r--parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/namespace/SimpleChapters.java4
-rw-r--r--parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/util/MimeTypeUtils.java83
-rw-r--r--parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/util/SyndTypeUtils.java44
-rw-r--r--parser/media/build.gradle4
-rw-r--r--parser/media/src/main/java/de/danoeh/antennapod/parser/media/id3/ChapterReader.java5
-rw-r--r--parser/media/src/main/java/de/danoeh/antennapod/parser/media/id3/ID3Chapter.java38
-rw-r--r--parser/media/src/main/java/de/danoeh/antennapod/parser/media/id3/Id3MetadataReader.java39
-rw-r--r--parser/media/src/main/java/de/danoeh/antennapod/parser/media/vorbis/VorbisCommentChapter.java88
-rw-r--r--parser/media/src/main/java/de/danoeh/antennapod/parser/media/vorbis/VorbisCommentChapterReader.java93
-rw-r--r--parser/media/src/main/java/de/danoeh/antennapod/parser/media/vorbis/VorbisCommentMetadataReader.java32
-rw-r--r--parser/media/src/main/java/de/danoeh/antennapod/parser/media/vorbis/VorbisCommentReader.java84
-rw-r--r--parser/media/src/main/java/de/danoeh/antennapod/parser/media/vorbis/VorbisCommentReaderException.java8
-rw-r--r--parser/media/src/test/java/de/danoeh/antennapod/parser/media/id3/ChapterReaderTest.java2
-rw-r--r--parser/media/src/test/java/de/danoeh/antennapod/parser/media/id3/MetadataReaderTest.java50
-rw-r--r--parser/media/src/test/java/de/danoeh/antennapod/parser/media/vorbis/VorbisCommentChapterReaderTest.java4
-rw-r--r--parser/media/src/test/java/de/danoeh/antennapod/parser/media/vorbis/VorbisCommentMetadataReaderTest.java28
-rw-r--r--playback/base/build.gradle4
-rw-r--r--playback/base/src/main/java/de/danoeh/antennapod/playback/base/PlaybackServiceMediaPlayer.java6
-rw-r--r--playback/base/src/main/java/de/danoeh/antennapod/playback/base/PlayerStatus.java4
-rw-r--r--playback/cast/build.gradle4
-rw-r--r--playback/cast/src/play/java/de/danoeh/antennapod/playback/cast/CastEnabledActivity.java1
-rw-r--r--playback/cast/src/play/java/de/danoeh/antennapod/playback/cast/CastPsmp.java5
-rw-r--r--settings.gradle5
-rw-r--r--storage/README.md3
-rw-r--r--storage/database/README.md3
-rw-r--r--storage/database/build.gradle17
-rw-r--r--storage/database/src/main/AndroidManifest.xml1
-rw-r--r--storage/database/src/main/java/de/danoeh/antennapod/storage/database/DBUpgrader.java (renamed from core/src/main/java/de/danoeh/antennapod/core/storage/DBUpgrader.java)10
-rw-r--r--storage/database/src/main/java/de/danoeh/antennapod/storage/database/PodDBAdapter.java (renamed from core/src/main/java/de/danoeh/antennapod/core/storage/PodDBAdapter.java)64
-rw-r--r--storage/database/src/main/java/de/danoeh/antennapod/storage/database/mapper/ChapterCursorMapper.java32
-rw-r--r--storage/database/src/main/java/de/danoeh/antennapod/storage/database/mapper/DownloadStatusCursorMapper.java35
-rw-r--r--storage/database/src/main/java/de/danoeh/antennapod/storage/database/mapper/FeedCursorMapper.java (renamed from core/src/main/java/de/danoeh/antennapod/core/storage/mapper/FeedCursorMapper.java)4
-rw-r--r--storage/database/src/main/java/de/danoeh/antennapod/storage/database/mapper/FeedItemCursorMapper.java (renamed from core/src/main/java/de/danoeh/antennapod/core/storage/mapper/FeedItemCursorMapper.java)8
-rw-r--r--storage/database/src/main/java/de/danoeh/antennapod/storage/database/mapper/FeedItemFilterQuery.java (renamed from core/src/main/java/de/danoeh/antennapod/core/storage/mapper/FeedItemFilterQuery.java)4
-rw-r--r--storage/database/src/main/java/de/danoeh/antennapod/storage/database/mapper/FeedMediaCursorMapper.java (renamed from core/src/main/java/de/danoeh/antennapod/core/storage/mapper/FeedMediaCursorMapper.java)4
-rw-r--r--storage/database/src/main/java/de/danoeh/antennapod/storage/database/mapper/FeedPreferencesCursorMapper.java (renamed from core/src/main/java/de/danoeh/antennapod/core/storage/mapper/FeedPreferencesCursorMapper.java)4
-rw-r--r--ui/app-start-intent/build.gradle4
-rw-r--r--ui/app-start-intent/src/main/java/de/danoeh/antennapod/ui/appstartintent/PlaybackSpeedActivityStarter.java40
-rw-r--r--ui/app-start-intent/src/main/res/values/pending_intent.xml1
-rw-r--r--ui/common/build.gradle6
-rw-r--r--ui/common/src/main/java/de/danoeh/antennapod/ui/common/PagedToolbarFragment.java (renamed from app/src/main/java/de/danoeh/antennapod/fragment/PagedToolbarFragment.java)7
-rw-r--r--ui/common/src/main/java/de/danoeh/antennapod/ui/common/RecursiveRadioGroup.java12
-rw-r--r--ui/common/src/main/res/layout/pager_fragment.xml (renamed from app/src/main/res/layout/pager_fragment.xml)0
-rw-r--r--ui/i18n/README.md3
-rw-r--r--ui/i18n/build.gradle15
-rw-r--r--ui/i18n/lint.xml10
-rw-r--r--ui/i18n/src/main/AndroidManifest.xml1
-rw-r--r--ui/i18n/src/main/res/values-ar/strings.xml (renamed from core/src/main/res/values-ar/strings.xml)0
-rw-r--r--ui/i18n/src/main/res/values-ast/strings.xml (renamed from core/src/main/res/values-ast/strings.xml)0
-rw-r--r--ui/i18n/src/main/res/values-br/strings.xml (renamed from core/src/main/res/values-br/strings.xml)0
-rw-r--r--ui/i18n/src/main/res/values-ca/strings.xml (renamed from core/src/main/res/values-ca/strings.xml)0
-rw-r--r--ui/i18n/src/main/res/values-cs/strings.xml (renamed from core/src/main/res/values-cs/strings.xml)0
-rw-r--r--ui/i18n/src/main/res/values-da/strings.xml (renamed from core/src/main/res/values-da/strings.xml)0
-rw-r--r--ui/i18n/src/main/res/values-de/strings.xml (renamed from core/src/main/res/values-de/strings.xml)0
-rw-r--r--ui/i18n/src/main/res/values-es/strings.xml (renamed from core/src/main/res/values-es/strings.xml)0
-rw-r--r--ui/i18n/src/main/res/values-et/strings.xml (renamed from core/src/main/res/values-et/strings.xml)0
-rw-r--r--ui/i18n/src/main/res/values-eu/strings.xml (renamed from core/src/main/res/values-eu/strings.xml)0
-rw-r--r--ui/i18n/src/main/res/values-fa/strings.xml (renamed from core/src/main/res/values-fa/strings.xml)0
-rw-r--r--ui/i18n/src/main/res/values-fi/strings.xml (renamed from core/src/main/res/values-fi/strings.xml)0
-rw-r--r--ui/i18n/src/main/res/values-fr/strings.xml (renamed from core/src/main/res/values-fr/strings.xml)0
-rw-r--r--ui/i18n/src/main/res/values-gl/strings.xml (renamed from core/src/main/res/values-gl/strings.xml)0
-rw-r--r--ui/i18n/src/main/res/values-hu/strings.xml (renamed from core/src/main/res/values-hu/strings.xml)0
-rw-r--r--ui/i18n/src/main/res/values-it/strings.xml (renamed from core/src/main/res/values-it/strings.xml)0
-rw-r--r--ui/i18n/src/main/res/values-iw/strings.xml (renamed from core/src/main/res/values-iw/strings.xml)0
-rw-r--r--ui/i18n/src/main/res/values-ja/strings.xml (renamed from core/src/main/res/values-ja/strings.xml)0
-rw-r--r--ui/i18n/src/main/res/values-ko/strings.xml (renamed from core/src/main/res/values-ko/strings.xml)0
-rw-r--r--ui/i18n/src/main/res/values-lt/strings.xml (renamed from core/src/main/res/values-lt/strings.xml)0
-rw-r--r--ui/i18n/src/main/res/values-nb/strings.xml (renamed from core/src/main/res/values-nb/strings.xml)0
-rw-r--r--ui/i18n/src/main/res/values-nl/strings.xml (renamed from core/src/main/res/values-nl/strings.xml)0
-rw-r--r--ui/i18n/src/main/res/values-pl/strings.xml (renamed from core/src/main/res/values-pl/strings.xml)0
-rw-r--r--ui/i18n/src/main/res/values-pt-rBR/strings.xml (renamed from core/src/main/res/values-pt-rBR/strings.xml)0
-rw-r--r--ui/i18n/src/main/res/values-pt/strings.xml (renamed from core/src/main/res/values-pt/strings.xml)0
-rw-r--r--ui/i18n/src/main/res/values-ro/strings.xml (renamed from core/src/main/res/values-ro/strings.xml)0
-rw-r--r--ui/i18n/src/main/res/values-ru/strings.xml (renamed from core/src/main/res/values-ru/strings.xml)0
-rw-r--r--ui/i18n/src/main/res/values-sk/strings.xml (renamed from core/src/main/res/values-sk/strings.xml)0
-rw-r--r--ui/i18n/src/main/res/values-sl/strings.xml (renamed from core/src/main/res/values-sl/strings.xml)0
-rw-r--r--ui/i18n/src/main/res/values-sv/strings.xml (renamed from core/src/main/res/values-sv/strings.xml)0
-rw-r--r--ui/i18n/src/main/res/values-tr/strings.xml (renamed from core/src/main/res/values-tr/strings.xml)0
-rw-r--r--ui/i18n/src/main/res/values-uk/strings.xml (renamed from core/src/main/res/values-uk/strings.xml)0
-rw-r--r--ui/i18n/src/main/res/values-zh-rCN/strings.xml (renamed from core/src/main/res/values-zh-rCN/strings.xml)0
-rw-r--r--ui/i18n/src/main/res/values-zh-rTW/strings.xml (renamed from core/src/main/res/values-zh-rTW/strings.xml)0
-rw-r--r--ui/i18n/src/main/res/values/strings.xml (renamed from core/src/main/res/values/strings.xml)39
-rw-r--r--ui/png-icons/build.gradle4
-rw-r--r--ui/png-icons/src/main/res/drawable/ic_widget_playback_speed.xml16
-rw-r--r--ui/statistics/README.md3
-rw-r--r--ui/statistics/build.gradle33
-rw-r--r--ui/statistics/src/main/AndroidManifest.xml1
-rw-r--r--ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/PieChartView.java (renamed from app/src/main/java/de/danoeh/antennapod/view/PieChartView.java)2
-rw-r--r--ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/StatisticsFragment.java150
-rw-r--r--ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/StatisticsListAdapter.java (renamed from app/src/main/java/de/danoeh/antennapod/adapter/StatisticsListAdapter.java)36
-rw-r--r--ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/downloads/DownloadStatisticsFragment.java (renamed from app/src/main/java/de/danoeh/antennapod/fragment/preferences/DownloadStatisticsFragment.java)32
-rw-r--r--ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/downloads/DownloadStatisticsListAdapter.java (renamed from app/src/main/java/de/danoeh/antennapod/adapter/DownloadStatisticsListAdapter.java)18
-rw-r--r--ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/feed/FeedStatisticsDialogFragment.java (renamed from app/src/main/java/de/danoeh/antennapod/fragment/FeedStatisticsDialogFragment.java)4
-rw-r--r--ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/feed/FeedStatisticsFragment.java (renamed from app/src/main/java/de/danoeh/antennapod/fragment/FeedStatisticsFragment.java)14
-rw-r--r--ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/subscriptions/PlaybackStatisticsListAdapter.java75
-rw-r--r--ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/subscriptions/StatisticsFilterDialog.java135
-rw-r--r--ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/subscriptions/SubscriptionStatisticsFragment.java137
-rw-r--r--ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/years/BarChartView.java135
-rw-r--r--ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/years/YearStatisticsListAdapter.java121
-rw-r--r--ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/years/YearsStatisticsFragment.java97
-rw-r--r--ui/statistics/src/main/res/layout/feed_statistics.xml (renamed from app/src/main/res/layout/feed_statistics.xml)18
-rw-r--r--ui/statistics/src/main/res/layout/feed_statistics_dialog.xml (renamed from app/src/main/res/layout/feed_statistics_dialog.xml)0
-rw-r--r--ui/statistics/src/main/res/layout/statistics_filter_dialog.xml95
-rw-r--r--ui/statistics/src/main/res/layout/statistics_fragment.xml (renamed from app/src/main/res/layout/statistics_activity.xml)0
-rw-r--r--ui/statistics/src/main/res/layout/statistics_listitem.xml (renamed from app/src/main/res/layout/statistics_listitem.xml)0
-rw-r--r--ui/statistics/src/main/res/layout/statistics_listitem_barchart.xml20
-rw-r--r--ui/statistics/src/main/res/layout/statistics_listitem_total.xml (renamed from app/src/main/res/layout/statistics_listitem_total.xml)38
-rw-r--r--ui/statistics/src/main/res/layout/statistics_year_listitem.xml32
-rw-r--r--ui/statistics/src/main/res/menu/statistics.xml (renamed from app/src/main/res/menu/statistics.xml)9
331 files changed, 4257 insertions, 3837 deletions
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
index 96c33d973..50576fdc1 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -11,7 +11,7 @@ body:
attributes:
label: Checklist
options:
- - label: I have used the search function to see if someone else has already submitted the same bug report.
+ - label: I have used the search function for [open](https://github.com/AntennaPod/AntennaPod/issues) **and** [closed](https://github.com/AntennaPod/AntennaPod/issues?q=is%3Aissue+is%3Aclosed) issues to see if someone else has already submitted the same bug report.
required: true
- label: I will describe the problem with as much detail as possible.
required: true
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml
index 58ac86f8b..fbac2857a 100644
--- a/.github/ISSUE_TEMPLATE/feature_request.yml
+++ b/.github/ISSUE_TEMPLATE/feature_request.yml
@@ -6,7 +6,7 @@ body:
attributes:
label: Checklist
options:
- - label: I have used the search function to see if someone else has already submitted the same feature request.
+ - label: I have used the search function for [open](https://github.com/AntennaPod/AntennaPod/issues) **and** [closed](https://github.com/AntennaPod/AntennaPod/issues?q=is%3Aissue+is%3Aclosed) issues to see if someone else has already submitted the same feature request.
required: true
- label: I will describe the problem with as much detail as possible.
required: true
diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml
index 587b6780a..c4a4b0ceb 100644
--- a/.github/workflows/checks.yml
+++ b/.github/workflows/checks.yml
@@ -10,8 +10,9 @@ jobs:
code-style:
name: "Code Style"
runs-on: ubuntu-latest
+ timeout-minutes: 45
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Checkstyle
@@ -37,18 +38,20 @@ jobs:
name: "Gradle Wrapper Validation"
needs: code-style
runs-on: ubuntu-latest
+ timeout-minutes: 45
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3
- uses: gradle/wrapper-validation-action@v1
static-analysis:
name: "Static Code Analysis"
needs: code-style
runs-on: ubuntu-latest
+ timeout-minutes: 45
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3
- name: Cache Gradle
- uses: actions/cache@v2
+ uses: actions/cache@v3
with:
path: |
~/.gradle/caches
@@ -63,6 +66,7 @@ jobs:
name: "Unit Test: ${{ matrix.variant }}"
needs: code-style
runs-on: ubuntu-latest
+ timeout-minutes: 45
strategy:
matrix:
include:
@@ -79,9 +83,9 @@ jobs:
execute-tests: false
upload-artifact: false
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3
- name: Cache Gradle
- uses: actions/cache@v2
+ uses: actions/cache@v3
with:
path: |
~/.gradle/caches
@@ -94,7 +98,7 @@ jobs:
- name: Test
if: matrix.execute-tests == true
run: ./gradlew test${{ matrix.variant }}UnitTest test${{ matrix.base-variant }}UnitTest
- - uses: actions/upload-artifact@v2
+ - uses: actions/upload-artifact@v3
if: matrix.upload-artifact == true
with:
name: app-play-debug.apk
@@ -104,26 +108,27 @@ jobs:
name: "Emulator Test"
needs: code-style
runs-on: macOS-latest
+ timeout-minutes: 45
env:
api-level: 27
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3
- name: Set up JDK 11
- uses: actions/setup-java@v2
+ uses: actions/setup-java@v3
with:
distribution: 'adopt'
java-version: '11'
- name: Build with Gradle
run: ./gradlew assemblePlayDebugAndroidTest
- name: Cache Gradle
- uses: actions/cache@v2
+ uses: actions/cache@v3
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: gradle-${{ hashFiles('**/*.gradle*') }}-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties') }}
- name: Cache AVD
- uses: actions/cache@v2
+ uses: actions/cache@v3
id: avd-cache
with:
path: |
@@ -147,7 +152,7 @@ jobs:
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
disable-animations: true
script: zsh .github/workflows/runEmulatorTests.sh
- - uses: actions/upload-artifact@v2
+ - uses: actions/upload-artifact@v3
if: failure()
with:
name: test-report
diff --git a/.github/workflows/close-if-no-reply.yml b/.github/workflows/close-if-no-reply.yml
index c0c32843a..ba4903a7f 100644
--- a/.github/workflows/close-if-no-reply.yml
+++ b/.github/workflows/close-if-no-reply.yml
@@ -11,15 +11,17 @@ jobs:
stale:
runs-on: ubuntu-latest
steps:
- - uses: actions/stale@v4
+ - uses: actions/stale@v5
with:
- days-before-stale: 10
- days-before-close: 10
+ days-before-stale: 15
+ days-before-close: 15
only-labels: 'Awaiting reply'
stale-issue-label: 'Still awaiting reply'
stale-pr-label: 'Still awaiting reply'
+ stale-issue-message: "This issue will be closed when we don't get a reply within 15 days."
+ stale-pr-message: "This PR will be closed when we don't get a reply within 15 days."
labels-to-remove-when-unstale: 'Awaiting reply'
close-issue-label: "Close reason: no reply"
close-pr-label: "Close reason: no reply"
- close-issue-message: "This issue was closed because we didn't get a reply for 20 days."
- close-pr-message: "This pr was closed because we didn't get a reply for 20 days."
+ close-issue-message: "This issue was closed because we didn't get a reply for 30 days."
+ close-pr-message: "This PR was closed because we didn't get a reply for 30 days."
diff --git a/.tx/config b/.tx/config
index 5a24aecaf..601e24dbd 100644
--- a/.tx/config
+++ b/.tx/config
@@ -2,44 +2,44 @@
host = https://www.transifex.com
[antennapod.core-values]
-source_file = core/src/main/res/values/strings.xml
+source_file = ui/i18n/src/main/res/values/strings.xml
source_lang = en
-trans.ar = core/src/main/res/values-ar/strings.xml
-trans.ast_ES = core/src/main/res/values-ast/strings.xml
-trans.br = core/src/main/res/values-br/strings.xml
-trans.ca = core/src/main/res/values-ca/strings.xml
-trans.cs_CZ = core/src/main/res/values-cs/strings.xml
-trans.da = core/src/main/res/values-da/strings.xml
-trans.de = core/src/main/res/values-de/strings.xml
-trans.es = core/src/main/res/values-es/strings.xml
-trans.et = core/src/main/res/values-et/strings.xml
-trans.eu = core/src/main/res/values-eu/strings.xml
-trans.fa = core/src/main/res/values-fa/strings.xml
-trans.fi = core/src/main/res/values-fi/strings.xml
-trans.fr = core/src/main/res/values-fr/strings.xml
-trans.gl = core/src/main/res/values-gl/strings.xml
-trans.he_IL = core/src/main/res/values-iw/strings.xml
-trans.hi_IN = core/src/main/res/values-hi/strings.xml
-trans.hu = core/src/main/res/values-hu/strings.xml
-trans.it_IT = core/src/main/res/values-it/strings.xml
-trans.ja = core/src/main/res/values-ja/strings.xml
-trans.ko = core/src/main/res/values-ko/strings.xml
-trans.lt = core/src/main/res/values-lt/strings.xml
-trans.nb_NO = core/src/main/res/values-nb/strings.xml
-trans.nl = core/src/main/res/values-nl/strings.xml
-trans.pl_PL = core/src/main/res/values-pl/strings.xml
-trans.pt = core/src/main/res/values-pt/strings.xml
-trans.pt_BR = core/src/main/res/values-pt-rBR/strings.xml
-trans.ro_RO = core/src/main/res/values-ro/strings.xml
-trans.ru_RU = core/src/main/res/values-ru/strings.xml
-trans.sk = core/src/main/res/values-sk/strings.xml
-trans.sl_SI = core/src/main/res/values-sl/strings.xml
-trans.sv_SE = core/src/main/res/values-sv/strings.xml
-trans.tr = core/src/main/res/values-tr/strings.xml
-trans.uk_UA = core/src/main/res/values-uk/strings.xml
-trans.zh_CN = core/src/main/res/values-zh-rCN/strings.xml
-trans.zh_TW = core/src/main/res/values-zh-rTW/strings.xml
-trans.zh_HK = core/src/main/res/values-zh-rHK/strings.xml
+trans.ar = ui/i18n/src/main/res/values-ar/strings.xml
+trans.ast_ES = ui/i18n/src/main/res/values-ast/strings.xml
+trans.br = ui/i18n/src/main/res/values-br/strings.xml
+trans.ca = ui/i18n/src/main/res/values-ca/strings.xml
+trans.cs_CZ = ui/i18n/src/main/res/values-cs/strings.xml
+trans.da = ui/i18n/src/main/res/values-da/strings.xml
+trans.de = ui/i18n/src/main/res/values-de/strings.xml
+trans.es = ui/i18n/src/main/res/values-es/strings.xml
+trans.et = ui/i18n/src/main/res/values-et/strings.xml
+trans.eu = ui/i18n/src/main/res/values-eu/strings.xml
+trans.fa = ui/i18n/src/main/res/values-fa/strings.xml
+trans.fi = ui/i18n/src/main/res/values-fi/strings.xml
+trans.fr = ui/i18n/src/main/res/values-fr/strings.xml
+trans.gl = ui/i18n/src/main/res/values-gl/strings.xml
+trans.he_IL = ui/i18n/src/main/res/values-iw/strings.xml
+trans.hi_IN = ui/i18n/src/main/res/values-hi/strings.xml
+trans.hu = ui/i18n/src/main/res/values-hu/strings.xml
+trans.it_IT = ui/i18n/src/main/res/values-it/strings.xml
+trans.ja = ui/i18n/src/main/res/values-ja/strings.xml
+trans.ko = ui/i18n/src/main/res/values-ko/strings.xml
+trans.lt = ui/i18n/src/main/res/values-lt/strings.xml
+trans.nb_NO = ui/i18n/src/main/res/values-nb/strings.xml
+trans.nl = ui/i18n/src/main/res/values-nl/strings.xml
+trans.pl_PL = ui/i18n/src/main/res/values-pl/strings.xml
+trans.pt = ui/i18n/src/main/res/values-pt/strings.xml
+trans.pt_BR = ui/i18n/src/main/res/values-pt-rBR/strings.xml
+trans.ro_RO = ui/i18n/src/main/res/values-ro/strings.xml
+trans.ru_RU = ui/i18n/src/main/res/values-ru/strings.xml
+trans.sk = ui/i18n/src/main/res/values-sk/strings.xml
+trans.sl_SI = ui/i18n/src/main/res/values-sl/strings.xml
+trans.sv_SE = ui/i18n/src/main/res/values-sv/strings.xml
+trans.tr = ui/i18n/src/main/res/values-tr/strings.xml
+trans.uk_UA = ui/i18n/src/main/res/values-uk/strings.xml
+trans.zh_CN = ui/i18n/src/main/res/values-zh-rCN/strings.xml
+trans.zh_TW = ui/i18n/src/main/res/values-zh-rTW/strings.xml
+trans.zh_HK = ui/i18n/src/main/res/values-zh-rHK/strings.xml
[antennapod.description]
source_file = app/src/main/play/listings/en-US/full-description.txt
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index e3bc7269d..0017c897d 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -8,7 +8,7 @@ How to report a bug
- If possible, add instructions on how to reproduce the bug.
- If possible, add a logfile to your post. This is especially useful if the bug makes the application crash. AntennaPod has an `export logs` feature for this.
- Usually, you can take a screenshot of your smartphone by pressing *Power* + *Volume down* for a few seconds.
-- Please use the following **[template](.github/ISSUE_TEMPLATE/bug_report.md)**.
+- Please use the following **[template](https://github.com/AntennaPod/AntennaPod/issues/new?assignees=&labels=Type%3A+Possible+bug&template=bug_report.yml)**.
How to submit a feature request
@@ -18,7 +18,7 @@ How to submit a feature request
- To make it easier for us to keep track of requests, please only make one feature request per issue.
- Give a brief explanation about the problem that may currently exist and how your requested feature solves this problem.
- Try to be as specific as possible. Please not only explain what the feature does, but also how. If your request is about (or includes) changing or extending the UI, describe what the UI would look like and how the user would interact with it.
-- Please use the following **[template](.github/ISSUE_TEMPLATE/feature_request.md)**.
+- Please use the following **[template](https://github.com/AntennaPod/AntennaPod/issues/new?assignees=&labels=&template=feature_request.yml)**.
Translating AntennaPod
diff --git a/README.md b/README.md
index 8eb13073d..3cc9729ed 100644
--- a/README.md
+++ b/README.md
@@ -17,6 +17,8 @@ You can use the [AntennaPod Forum](https://forum.antennapod.org/) for discussion
Bug reports and feature requests can be submitted [here](https://github.com/AntennaPod/AntennaPod/issues) (please read the [instructions](https://github.com/AntennaPod/AntennaPod/blob/master/CONTRIBUTING.md) on how to report a bug and how to submit a feature request first!).
+We also hold regular community calls to discuss anything AntennaPod-related. [Come join the next call](https://forum.antennapod.org/t/monthly-community-call/1869)!
+
## Help to test AntennaPod
AntennaPod has many users and we don't want them to run into trouble when we add a new feature. It's important that we have a significant group test our app, so that we know all possible combinations of phones, Android versions and use cases work as expected. Check out our wiki on how to join our [Beta testing program](https://antennapod.org/documentation/general/beta)! If a bug is reported during the beta period, chances are high that it will be fixed before the stable version. If it is reported later, fixing might take another full beta cycle. So definitely let us know if something is not right.
diff --git a/app/build.gradle b/app/build.gradle
index f1c1bed08..baa206519 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -26,14 +26,6 @@ android {
}
buildConfigField "String", "COMMIT_HASH", ('"' + (commit.isEmpty() ? "Unknown commit" : commit) + '"')
- if (project.hasProperty("podcastindexApiKey")) {
- buildConfigField "String", "PODCASTINDEX_API_KEY", '"' + podcastindexApiKey + '"'
- buildConfigField "String", "PODCASTINDEX_API_SECRET", '"' + podcastindexApiSecret + '"'
- } else {
- buildConfigField "String", "PODCASTINDEX_API_KEY", '"XTMMQGA2YZ4WJUBYY4HK"'
- buildConfigField "String", "PODCASTINDEX_API_SECRET", '"XAaAhk4^2YBsTE33vdbwbZNj82ZRLABDDqFdKe7x"'
- }
-
javaCompileOptions {
annotationProcessorOptions {
arguments = [eventBusIndex: 'de.danoeh.antennapod.ApEventBusIndex']
@@ -43,8 +35,8 @@ android {
signingConfigs {
releaseConfig {
- v1SigningEnabled true
- v2SigningEnabled true
+ enableV1Signing true
+ enableV2Signing true
if (project.hasProperty("releaseStoreFile")) {
storeFile file(releaseStoreFile)
@@ -113,13 +105,17 @@ dependencies {
implementation project(":core")
implementation project(":event")
implementation project(':model')
+ implementation project(':net:discovery')
implementation project(':net:sync:gpoddernet')
implementation project(':net:sync:model')
implementation project(':parser:feed')
implementation project(':playback:base')
implementation project(':playback:cast')
+ implementation project(':storage:database')
implementation project(':ui:app-start-intent')
implementation project(':ui:common')
+ implementation project(':ui:i18n')
+ implementation project(':ui:statistics')
annotationProcessor "androidx.annotation:annotation:$annotationVersion"
implementation "androidx.appcompat:appcompat:$appcompatVersion"
@@ -128,6 +124,7 @@ dependencies {
implementation "androidx.fragment:fragment:$fragmentVersion"
implementation 'androidx.gridlayout:gridlayout:1.0.0'
implementation "androidx.media:media:$mediaVersion"
+ implementation 'androidx.multidex:multidex:2.0.1'
implementation "androidx.palette:palette:$paletteVersion"
implementation "androidx.preference:preference:$preferenceVersion"
implementation "androidx.recyclerview:recyclerview:$recyclerViewVersion"
@@ -153,7 +150,6 @@ dependencies {
implementation 'com.github.shts:TriangleLabelView:1.1.2'
implementation 'com.leinardi.android:speed-dial:3.2.0'
implementation "com.github.AntennaPod:AntennaPod-AudioPlayer:$audioPlayerVersion"
- implementation 'com.github.mfietz:fyydlin:v0.5.0'
implementation 'com.github.ByteHamster:SearchPreference:v2.0.0'
implementation 'com.github.skydoves:balloon:1.4.0'
implementation 'com.github.xabaras:RecyclerViewSwipeDecorator:1.3'
diff --git a/app/src/androidTest/java/de/danoeh/antennapod/core/service/download/StubDownloader.java b/app/src/androidTest/java/de/danoeh/antennapod/core/service/download/StubDownloader.java
index 7aac738e7..6e39967c1 100644
--- a/app/src/androidTest/java/de/danoeh/antennapod/core/service/download/StubDownloader.java
+++ b/app/src/androidTest/java/de/danoeh/antennapod/core/service/download/StubDownloader.java
@@ -2,6 +2,7 @@ package de.danoeh.antennapod.core.service.download;
import androidx.annotation.NonNull;
import androidx.core.util.Consumer;
+import de.danoeh.antennapod.model.download.DownloadStatus;
public class StubDownloader extends Downloader {
diff --git a/app/src/androidTest/java/de/test/antennapod/EspressoTestUtils.java b/app/src/androidTest/java/de/test/antennapod/EspressoTestUtils.java
index 21498effd..df3dbb7f8 100644
--- a/app/src/androidTest/java/de/test/antennapod/EspressoTestUtils.java
+++ b/app/src/androidTest/java/de/test/antennapod/EspressoTestUtils.java
@@ -18,6 +18,7 @@ import androidx.test.espresso.util.HumanReadables;
import androidx.test.espresso.util.TreeIterables;
import android.view.View;
+import de.danoeh.antennapod.storage.database.PodDBAdapter;
import junit.framework.AssertionFailedError;
import de.danoeh.antennapod.R;
@@ -25,7 +26,6 @@ import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.service.download.DownloadService;
import de.danoeh.antennapod.core.service.playback.PlaybackService;
-import de.danoeh.antennapod.core.storage.PodDBAdapter;
import de.danoeh.antennapod.dialog.RatingDialog;
import de.danoeh.antennapod.fragment.NavDrawerFragment;
import org.awaitility.Awaitility;
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 e31838671..29cea18dc 100644
--- a/app/src/androidTest/java/de/test/antennapod/dialogs/ShareDialogTest.java
+++ b/app/src/androidTest/java/de/test/antennapod/dialogs/ShareDialogTest.java
@@ -74,13 +74,13 @@ public class ShareDialogTest {
@Test
public void testShareDialogDisplayed() throws InterruptedException {
- onView(withText(R.string.share_label_with_ellipses)).perform(click());
+ onView(withText(R.string.share_label)).perform(click());
onView(allOf(isDisplayed(), withText(R.string.share_label)));
}
@Test
public void testShareDialogCancelButton() {
- onView(withText(R.string.share_label_with_ellipses)).perform(scrollTo()).perform(click());
+ onView(withText(R.string.share_label)).perform(scrollTo()).perform(click());
onView(withText(R.string.cancel_label)).check(matches(isDisplayed())).perform(scrollTo()).perform(click());
}
diff --git a/app/src/androidTest/java/de/test/antennapod/service/download/DownloadServiceTest.java b/app/src/androidTest/java/de/test/antennapod/service/download/DownloadServiceTest.java
index edd80b0d0..8aae95e69 100644
--- a/app/src/androidTest/java/de/test/antennapod/service/download/DownloadServiceTest.java
+++ b/app/src/androidTest/java/de/test/antennapod/service/download/DownloadServiceTest.java
@@ -28,7 +28,7 @@ import de.danoeh.antennapod.model.feed.FeedMedia;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.service.download.DownloadRequest;
import de.danoeh.antennapod.core.service.download.DownloadService;
-import de.danoeh.antennapod.core.service.download.DownloadStatus;
+import de.danoeh.antennapod.model.download.DownloadStatus;
import de.danoeh.antennapod.core.service.download.Downloader;
import de.danoeh.antennapod.core.service.download.DownloaderFactory;
import de.danoeh.antennapod.core.service.download.StubDownloader;
diff --git a/app/src/androidTest/java/de/test/antennapod/service/download/HttpDownloaderTest.java b/app/src/androidTest/java/de/test/antennapod/service/download/HttpDownloaderTest.java
index 6b8854d34..16e2ca1f4 100644
--- a/app/src/androidTest/java/de/test/antennapod/service/download/HttpDownloaderTest.java
+++ b/app/src/androidTest/java/de/test/antennapod/service/download/HttpDownloaderTest.java
@@ -10,10 +10,10 @@ import java.io.IOException;
import de.danoeh.antennapod.model.feed.FeedFile;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.service.download.DownloadRequest;
-import de.danoeh.antennapod.core.service.download.DownloadStatus;
+import de.danoeh.antennapod.model.download.DownloadStatus;
import de.danoeh.antennapod.core.service.download.Downloader;
import de.danoeh.antennapod.core.service.download.HttpDownloader;
-import de.danoeh.antennapod.core.util.DownloadError;
+import de.danoeh.antennapod.model.download.DownloadError;
import de.test.antennapod.util.service.download.HTTPBin;
import org.junit.After;
import org.junit.Before;
diff --git a/app/src/androidTest/java/de/test/antennapod/service/playback/PlaybackServiceMediaPlayerTest.java b/app/src/androidTest/java/de/test/antennapod/service/playback/PlaybackServiceMediaPlayerTest.java
index 0d05c7624..e115bc34e 100644
--- a/app/src/androidTest/java/de/test/antennapod/service/playback/PlaybackServiceMediaPlayerTest.java
+++ b/app/src/androidTest/java/de/test/antennapod/service/playback/PlaybackServiceMediaPlayerTest.java
@@ -8,6 +8,7 @@ import androidx.test.filters.MediumTest;
import de.danoeh.antennapod.model.feed.VolumeAdaptionSetting;
import de.danoeh.antennapod.playback.base.PlaybackServiceMediaPlayer;
import de.danoeh.antennapod.playback.base.PlayerStatus;
+import de.danoeh.antennapod.storage.database.PodDBAdapter;
import de.test.antennapod.EspressoTestUtils;
import junit.framework.AssertionFailedError;
@@ -27,7 +28,6 @@ import de.danoeh.antennapod.model.feed.FeedItem;
import de.danoeh.antennapod.model.feed.FeedMedia;
import de.danoeh.antennapod.model.feed.FeedPreferences;
import de.danoeh.antennapod.core.service.playback.LocalPSMP;
-import de.danoeh.antennapod.core.storage.PodDBAdapter;
import de.danoeh.antennapod.model.playback.Playable;
import de.test.antennapod.util.service.download.HTTPBin;
import org.junit.After;
diff --git a/app/src/androidTest/java/de/test/antennapod/service/playback/PlaybackServiceTaskManagerTest.java b/app/src/androidTest/java/de/test/antennapod/service/playback/PlaybackServiceTaskManagerTest.java
index 17d3d2e17..9f1fc2261 100644
--- a/app/src/androidTest/java/de/test/antennapod/service/playback/PlaybackServiceTaskManagerTest.java
+++ b/app/src/androidTest/java/de/test/antennapod/service/playback/PlaybackServiceTaskManagerTest.java
@@ -8,6 +8,7 @@ import androidx.test.filters.LargeTest;
import de.danoeh.antennapod.event.playback.SleepTimerUpdatedEvent;
import de.danoeh.antennapod.core.preferences.SleepTimerPreferences;
import de.danoeh.antennapod.core.widget.WidgetUpdater;
+import de.danoeh.antennapod.storage.database.PodDBAdapter;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.junit.After;
@@ -23,7 +24,6 @@ import java.util.concurrent.TimeUnit;
import de.danoeh.antennapod.model.feed.Feed;
import de.danoeh.antennapod.model.feed.FeedItem;
import de.danoeh.antennapod.core.service.playback.PlaybackServiceTaskManager;
-import de.danoeh.antennapod.core.storage.PodDBAdapter;
import de.danoeh.antennapod.model.playback.Playable;
import static org.junit.Assert.assertFalse;
diff --git a/app/src/androidTest/java/de/test/antennapod/storage/AutoDownloadTest.java b/app/src/androidTest/java/de/test/antennapod/storage/AutoDownloadTest.java
index 23c129cb1..7723d4941 100644
--- a/app/src/androidTest/java/de/test/antennapod/storage/AutoDownloadTest.java
+++ b/app/src/androidTest/java/de/test/antennapod/storage/AutoDownloadTest.java
@@ -99,8 +99,6 @@ public class AutoDownloadTest {
FeedMedia media = item.getMedia();
new PlaybackServiceStarter(context, media)
.callEvenIfRunning(true)
- .startWhenPrepared(true)
- .shouldStream(true)
.start();
Awaitility.await("episode is playing")
.atMost(2000, MILLISECONDS)
diff --git a/app/src/androidTest/java/de/test/antennapod/ui/MainActivityTest.java b/app/src/androidTest/java/de/test/antennapod/ui/MainActivityTest.java
index f048dbf63..81d7731c5 100644
--- a/app/src/androidTest/java/de/test/antennapod/ui/MainActivityTest.java
+++ b/app/src/androidTest/java/de/test/antennapod/ui/MainActivityTest.java
@@ -10,6 +10,7 @@ import androidx.test.platform.app.InstrumentationRegistry;
import com.robotium.solo.Solo;
+import de.danoeh.antennapod.storage.database.PodDBAdapter;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
@@ -21,7 +22,6 @@ import java.io.IOException;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.model.feed.Feed;
-import de.danoeh.antennapod.core.storage.PodDBAdapter;
import de.test.antennapod.EspressoTestUtils;
import static androidx.test.espresso.Espresso.onView;
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 902cad98c..ef5719d6f 100644
--- a/app/src/androidTest/java/de/test/antennapod/ui/PreferencesTest.java
+++ b/app/src/androidTest/java/de/test/antennapod/ui/PreferencesTest.java
@@ -10,6 +10,7 @@ import androidx.test.espresso.matcher.RootMatchers;
import androidx.test.filters.LargeTest;
import androidx.test.rule.ActivityTestRule;
+import de.danoeh.antennapod.core.storage.EpisodeCleanupAlgorithmFactory;
import org.awaitility.Awaitility;
import org.junit.Before;
import org.junit.Rule;
@@ -397,7 +398,7 @@ public class PreferencesTest {
onView(withId(R.id.select_dialog_listview)).perform(swipeDown());
onView(withText(R.string.episode_cleanup_except_favorite_removal)).perform(click());
Awaitility.await().atMost(1000, MILLISECONDS)
- .until(() -> UserPreferences.getEpisodeCleanupAlgorithm() instanceof ExceptFavoriteCleanupAlgorithm);
+ .until(() -> EpisodeCleanupAlgorithmFactory.build() instanceof ExceptFavoriteCleanupAlgorithm);
}
@Test
@@ -408,7 +409,7 @@ public class PreferencesTest {
onView(withId(R.id.select_dialog_listview)).perform(swipeDown());
onView(withText(R.string.episode_cleanup_queue_removal)).perform(click());
Awaitility.await().atMost(1000, MILLISECONDS)
- .until(() -> UserPreferences.getEpisodeCleanupAlgorithm() instanceof APQueueCleanupAlgorithm);
+ .until(() -> EpisodeCleanupAlgorithmFactory.build() instanceof APQueueCleanupAlgorithm);
}
@Test
@@ -419,7 +420,7 @@ public class PreferencesTest {
onView(withId(R.id.select_dialog_listview)).perform(swipeUp());
onView(withText(R.string.episode_cleanup_never)).perform(click());
Awaitility.await().atMost(1000, MILLISECONDS)
- .until(() -> UserPreferences.getEpisodeCleanupAlgorithm() instanceof APNullCleanupAlgorithm);
+ .until(() -> EpisodeCleanupAlgorithmFactory.build() instanceof APNullCleanupAlgorithm);
}
@Test
@@ -431,7 +432,7 @@ public class PreferencesTest {
onView(withText(R.string.episode_cleanup_after_listening)).perform(click());
Awaitility.await().atMost(1000, MILLISECONDS)
.until(() -> {
- EpisodeCleanupAlgorithm alg = UserPreferences.getEpisodeCleanupAlgorithm();
+ EpisodeCleanupAlgorithm alg = EpisodeCleanupAlgorithmFactory.build();
if (alg instanceof APCleanupAlgorithm) {
APCleanupAlgorithm cleanupAlg = (APCleanupAlgorithm) alg;
return cleanupAlg.getNumberOfHoursAfterPlayback() == 0;
@@ -450,7 +451,7 @@ public class PreferencesTest {
onView(withText(search)).perform(click());
Awaitility.await().atMost(1000, MILLISECONDS)
.until(() -> {
- EpisodeCleanupAlgorithm alg = UserPreferences.getEpisodeCleanupAlgorithm();
+ EpisodeCleanupAlgorithm alg = EpisodeCleanupAlgorithmFactory.build();
if (alg instanceof APCleanupAlgorithm) {
APCleanupAlgorithm cleanupAlg = (APCleanupAlgorithm) alg;
return cleanupAlg.getNumberOfHoursAfterPlayback() == 72; // 5 days
diff --git a/app/src/androidTest/java/de/test/antennapod/ui/UITestUtils.java b/app/src/androidTest/java/de/test/antennapod/ui/UITestUtils.java
index eedb2d9de..834c7d48b 100644
--- a/app/src/androidTest/java/de/test/antennapod/ui/UITestUtils.java
+++ b/app/src/androidTest/java/de/test/antennapod/ui/UITestUtils.java
@@ -7,7 +7,7 @@ import de.danoeh.antennapod.event.QueueEvent;
import de.danoeh.antennapod.model.feed.Feed;
import de.danoeh.antennapod.model.feed.FeedItem;
import de.danoeh.antennapod.model.feed.FeedMedia;
-import de.danoeh.antennapod.core.storage.PodDBAdapter;
+import de.danoeh.antennapod.storage.database.PodDBAdapter;
import de.test.antennapod.util.service.download.HTTPBin;
import de.test.antennapod.util.syndication.feedgenerator.Rss2Generator;
import org.apache.commons.io.FileUtils;
diff --git a/app/src/androidTest/java/de/test/antennapod/util/syndication/feedgenerator/Rss2Generator.java b/app/src/androidTest/java/de/test/antennapod/util/syndication/feedgenerator/Rss2Generator.java
index 6b294244a..6b85f3bf8 100644
--- a/app/src/androidTest/java/de/test/antennapod/util/syndication/feedgenerator/Rss2Generator.java
+++ b/app/src/androidTest/java/de/test/antennapod/util/syndication/feedgenerator/Rss2Generator.java
@@ -127,15 +127,9 @@ public class Rss2Generator implements FeedGenerator {
}
}
- writeAdditionalAttributes(xml);
-
xml.endTag(null, "channel");
xml.endTag(null, "rss");
xml.endDocument();
}
-
- protected void writeAdditionalAttributes(XmlSerializer xml) throws IOException {
-
- }
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 0f8242e63..93f1494f2 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -43,6 +43,17 @@
android:allowAudioPlaybackCapture="true"
android:networkSecurityConfig="@xml/network_security_config">
+ <activity
+ android:name=".activity.PlaybackSpeedDialogActivity"
+ android:noHistory="true"
+ android:exported="false"
+ android:excludeFromRecents="true"
+ android:theme="@style/Theme.AntennaPod.Light.Translucent">
+ <intent-filter>
+ <action android:name="de.danoeh.antennapod.intents.PLAYBACK_SPEED" />
+ <category android:name="android.intent.category.DEFAULT"/>
+ </intent-filter>
+ </activity>
<meta-data android:name="android.webkit.WebView.MetricsOptOut"
android:value="true"/>
@@ -366,11 +377,4 @@
android:resource="@xml/actions" />
</application>
- <queries>
- <intent>
- <action android:name="android.intent.action.SEND" />
- <data android:mimeType="text/*" />
- </intent>
- </queries>
-
</manifest>
diff --git a/app/src/main/java/de/danoeh/antennapod/PodcastApp.java b/app/src/main/java/de/danoeh/antennapod/PodcastApp.java
index 534d48479..8ce894684 100644
--- a/app/src/main/java/de/danoeh/antennapod/PodcastApp.java
+++ b/app/src/main/java/de/danoeh/antennapod/PodcastApp.java
@@ -1,10 +1,10 @@
package de.danoeh.antennapod;
-import android.app.Application;
import android.content.ComponentName;
import android.content.Intent;
import android.os.StrictMode;
+import androidx.multidex.MultiDexApplication;
import com.joanzapata.iconify.Iconify;
import com.joanzapata.iconify.fonts.FontAwesomeModule;
import com.joanzapata.iconify.fonts.MaterialModule;
@@ -18,7 +18,7 @@ import de.danoeh.antennapod.spa.SPAUtil;
import org.greenrobot.eventbus.EventBus;
/** Main application class. */
-public class PodcastApp extends Application {
+public class PodcastApp extends MultiDexApplication {
// make sure that ClientConfigurator executes its static code
static {
diff --git a/app/src/main/java/de/danoeh/antennapod/activity/BugReportActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/BugReportActivity.java
index f7c96a93a..64e5a109c 100644
--- a/app/src/main/java/de/danoeh/antennapod/activity/BugReportActivity.java
+++ b/app/src/main/java/de/danoeh/antennapod/activity/BugReportActivity.java
@@ -3,9 +3,6 @@ package de.danoeh.antennapod.activity;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
-import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
@@ -14,6 +11,7 @@ import com.google.android.material.snackbar.Snackbar;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
+import androidx.core.app.ShareCompat;
import androidx.core.content.FileProvider;
@@ -32,7 +30,6 @@ import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.charset.Charset;
-import java.util.List;
/**
* Displays the 'crash report' screen
@@ -102,21 +99,14 @@ public class BugReportActivity extends AppCompatActivity {
Runtime.getRuntime().exec(cmd);
//share file
try {
- Intent intent = new Intent(Intent.ACTION_SEND);
- intent.setType("text/*");
- String authString = getString(de.danoeh.antennapod.core.R.string.provider_authority);
- Uri fileUri = FileProvider.getUriForFile(this, authString, filename);
- intent.putExtra(Intent.EXTRA_STREAM, fileUri);
- intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
- String chooserTitle = getString(de.danoeh.antennapod.core.R.string.share_file_label);
- Intent chooser = Intent.createChooser(intent, chooserTitle);
- List<ResolveInfo> resInfos = getPackageManager()
- .queryIntentActivities(chooser, PackageManager.MATCH_DEFAULT_ONLY);
- for (ResolveInfo resolveInfo : resInfos) {
- String packageName = resolveInfo.activityInfo.packageName;
- grantUriPermission(packageName, fileUri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
- }
- startActivity(chooser);
+ 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;
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 3e4782f1f..b23e22c9f 100644
--- a/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java
+++ b/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java
@@ -18,7 +18,6 @@ import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
-import android.widget.FrameLayout;
import android.widget.Toast;
import androidx.annotation.NonNull;
@@ -29,6 +28,7 @@ import androidx.appcompat.widget.Toolbar;
import androidx.core.content.ContextCompat;
import androidx.drawerlayout.widget.DrawerLayout;
import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentContainerView;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import androidx.recyclerview.widget.RecyclerView;
@@ -231,6 +231,7 @@ public class MainActivity extends CastEnabledActivity {
// for backward compatibility, we only change defaults for fresh installs
UserPreferences.setUpdateInterval(12);
+ AutoUpdateManager.restartUpdateAlarm(this);
SharedPreferences.Editor edit = prefs.edit();
edit.putBoolean(PREF_IS_FIRST_LAUNCH, false);
@@ -248,7 +249,7 @@ public class MainActivity extends CastEnabledActivity {
public void setPlayerVisible(boolean visible) {
getBottomSheet().setLocked(!visible);
- FrameLayout mainView = findViewById(R.id.main_view);
+ FragmentContainerView mainView = findViewById(R.id.main_view);
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) mainView.getLayoutParams();
params.setMargins(0, 0, 0, visible ? (int) getResources().getDimension(R.dimen.external_player_height) : 0);
mainView.setLayoutParams(params);
diff --git a/app/src/main/java/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java
index 3f01a2b2d..9e71ac1db 100644
--- a/app/src/main/java/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java
+++ b/app/src/main/java/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java
@@ -35,8 +35,7 @@ import de.danoeh.antennapod.core.event.DownloadEvent;
import de.danoeh.antennapod.core.service.download.DownloadService;
import de.danoeh.antennapod.core.service.download.DownloadRequestCreator;
import de.danoeh.antennapod.core.feed.FeedUrlNotFoundException;
-import de.danoeh.antennapod.discovery.CombinedSearcher;
-import de.danoeh.antennapod.discovery.PodcastSearchResult;
+import de.danoeh.antennapod.core.util.DownloadErrorLabel;
import de.danoeh.antennapod.event.FeedListUpdateEvent;
import de.danoeh.antennapod.event.PlayerStatusEvent;
import de.danoeh.antennapod.core.glide.ApGlideSettings;
@@ -44,15 +43,18 @@ import de.danoeh.antennapod.core.glide.FastBlurTransformation;
import de.danoeh.antennapod.core.preferences.PlaybackPreferences;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.service.download.DownloadRequest;
-import de.danoeh.antennapod.core.service.download.DownloadStatus;
+import de.danoeh.antennapod.model.download.DownloadStatus;
import de.danoeh.antennapod.core.service.download.Downloader;
import de.danoeh.antennapod.core.service.download.HttpDownloader;
import de.danoeh.antennapod.core.service.playback.PlaybackService;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.storage.DBWriter;
+import de.danoeh.antennapod.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.core.util.DownloadError;
+import de.danoeh.antennapod.model.download.DownloadError;
import de.danoeh.antennapod.core.util.IntentUtils;
import de.danoeh.antennapod.core.util.StorageUtils;
import de.danoeh.antennapod.core.util.URLChecker;
@@ -60,7 +62,6 @@ 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.dialog.AuthenticationDialog;
-import de.danoeh.antennapod.discovery.PodcastSearcherRegistry;
import de.danoeh.antennapod.model.feed.Feed;
import de.danoeh.antennapod.model.feed.FeedPreferences;
import de.danoeh.antennapod.model.playback.RemoteMedia;
@@ -133,10 +134,10 @@ public class OnlineFeedViewActivity extends AppCompatActivity {
String feedUrl = null;
if (getIntent().hasExtra(ARG_FEEDURL)) {
feedUrl = getIntent().getStringExtra(ARG_FEEDURL);
- } else if (TextUtils.equals(getIntent().getAction(), Intent.ACTION_SEND)
- || TextUtils.equals(getIntent().getAction(), Intent.ACTION_VIEW)) {
- feedUrl = TextUtils.equals(getIntent().getAction(), Intent.ACTION_SEND)
- ? getIntent().getStringExtra(Intent.EXTRA_TEXT) : getIntent().getDataString();
+ } 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) {
@@ -325,7 +326,7 @@ public class OnlineFeedViewActivity extends AppCompatActivity {
dialog.show();
}
} else {
- showErrorDialog(status.getReason().getErrorString(OnlineFeedViewActivity.this), status.getReasonDetailed());
+ showErrorDialog(getString(DownloadErrorLabel.from(status.getReason())), status.getReasonDetailed());
}
}
diff --git a/app/src/main/java/de/danoeh/antennapod/activity/PlaybackSpeedDialogActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/PlaybackSpeedDialogActivity.java
new file mode 100644
index 000000000..137724acd
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/activity/PlaybackSpeedDialogActivity.java
@@ -0,0 +1,29 @@
+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.core.preferences.UserPreferences;
+import de.danoeh.antennapod.dialog.VariableSpeedDialog;
+
+public class PlaybackSpeedDialogActivity extends AppCompatActivity {
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ setTheme(UserPreferences.getTranslucentTheme());
+ 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
index 1fc16ab32..05a514c76 100644
--- a/app/src/main/java/de/danoeh/antennapod/activity/PreferenceActivity.java
+++ b/app/src/main/java/de/danoeh/antennapod/activity/PreferenceActivity.java
@@ -4,7 +4,6 @@ import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.provider.Settings;
-import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
@@ -131,12 +130,6 @@ public class PreferenceActivity extends AppCompatActivity implements SearchPrefe
}
@Override
- public boolean onCreateOptionsMenu(Menu menu) {
- super.onCreateOptionsMenu(menu);
- return true;
- }
-
- @Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) {
if (getSupportFragmentManager().getBackStackEntryCount() == 0) {
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 f0c76d545..ecf490f48 100644
--- a/app/src/main/java/de/danoeh/antennapod/activity/SplashActivity.java
+++ b/app/src/main/java/de/danoeh/antennapod/activity/SplashActivity.java
@@ -12,8 +12,8 @@ import androidx.appcompat.app.AppCompatActivity;
import android.widget.ProgressBar;
import de.danoeh.antennapod.R;
-import de.danoeh.antennapod.core.storage.PodDBAdapter;
import de.danoeh.antennapod.error.CrashReportWriter;
+import de.danoeh.antennapod.storage.database.PodDBAdapter;
import io.reactivex.Completable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
diff --git a/app/src/main/java/de/danoeh/antennapod/activity/VideoplayerActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/VideoplayerActivity.java
index 4ff2a5775..954a6c2f6 100644
--- a/app/src/main/java/de/danoeh/antennapod/activity/VideoplayerActivity.java
+++ b/app/src/main/java/de/danoeh/antennapod/activity/VideoplayerActivity.java
@@ -91,6 +91,7 @@ public class VideoplayerActivity extends CastEnabledActivity implements SeekBar.
private PlaybackController controller;
private boolean showTimeLeft = false;
private boolean isFavorite = false;
+ private boolean switchToAudioOnly = false;
private Disposable disposable;
private float prog;
@@ -119,6 +120,7 @@ public class VideoplayerActivity extends CastEnabledActivity implements SeekBar.
protected void onResume() {
super.onResume();
StorageUtils.checkStorageAvailability(this);
+ switchToAudioOnly = false;
if (PlaybackService.isCasting()) {
Intent intent = PlaybackService.getPlayerActivityIntent(this);
if (!intent.getComponent().getClassName().equals(VideoplayerActivity.class.getName())) {
@@ -149,8 +151,7 @@ public class VideoplayerActivity extends CastEnabledActivity implements SeekBar.
@Override
public void onUserLeaveHint() {
- if (!PictureInPictureUtil.isInPictureInPictureMode(this) && UserPreferences.getVideoBackgroundBehavior()
- == UserPreferences.VideoBackgroundBehavior.PICTURE_IN_PICTURE) {
+ if (!PictureInPictureUtil.isInPictureInPictureMode(this)) {
compatEnterPictureInPicture();
}
}
@@ -480,9 +481,7 @@ public class VideoplayerActivity extends CastEnabledActivity implements SeekBar.
public void surfaceDestroyed(SurfaceHolder holder) {
Log.d(TAG, "Videosurface was destroyed");
videoSurfaceCreated = false;
- if (controller != null && !destroyingDueToReload
- && UserPreferences.getVideoBackgroundBehavior()
- != UserPreferences.VideoBackgroundBehavior.CONTINUE_PLAYING) {
+ if (controller != null && !destroyingDueToReload && !switchToAudioOnly) {
controller.notifyVideoSurfaceAbandoned();
}
}
@@ -590,17 +589,16 @@ public class VideoplayerActivity extends CastEnabledActivity implements SeekBar.
menu.findItem(R.id.set_sleeptimer_item).setVisible(!controller.sleepTimerActive());
menu.findItem(R.id.disable_sleeptimer_item).setVisible(controller.sleepTimerActive());
- if (PictureInPictureUtil.supportsPictureInPicture(this)) {
- menu.findItem(R.id.player_go_to_picture_in_picture).setVisible(true);
- }
+ menu.findItem(R.id.player_switch_to_audio_only).setVisible(true);
menu.findItem(R.id.audio_controls).setIcon(R.drawable.ic_sliders);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
- if (item.getItemId() == R.id.player_go_to_picture_in_picture) {
- compatEnterPictureInPicture();
+ if (item.getItemId() == R.id.player_switch_to_audio_only) {
+ switchToAudioOnly = true;
+ finish();
return true;
}
if (item.getItemId() == android.R.id.home) {
diff --git a/app/src/main/java/de/danoeh/antennapod/activity/WidgetConfigActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/WidgetConfigActivity.java
index 674071294..32789e51a 100644
--- a/app/src/main/java/de/danoeh/antennapod/activity/WidgetConfigActivity.java
+++ b/app/src/main/java/de/danoeh/antennapod/activity/WidgetConfigActivity.java
@@ -12,7 +12,7 @@ import androidx.appcompat.app.AppCompatActivity;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.receiver.PlayerWidget;
-import de.danoeh.antennapod.core.widget.WidgetUpdaterJobService;
+import de.danoeh.antennapod.core.widget.WidgetUpdaterWorker;
public class WidgetConfigActivity extends AppCompatActivity {
private int appWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID;
@@ -20,6 +20,7 @@ public class WidgetConfigActivity extends AppCompatActivity {
private SeekBar opacitySeekBar;
private TextView opacityTextView;
private View widgetPreview;
+ private CheckBox ckPlaybackSpeed;
private CheckBox ckRewind;
private CheckBox ckFastForward;
private CheckBox ckSkip;
@@ -47,7 +48,7 @@ public class WidgetConfigActivity extends AppCompatActivity {
opacityTextView = findViewById(R.id.widget_opacity_textView);
opacitySeekBar = findViewById(R.id.widget_opacity_seekBar);
widgetPreview = findViewById(R.id.widgetLayout);
- findViewById(R.id.butConfirm).setOnClickListener(this::confirmCreateWidget);
+ findViewById(R.id.butConfirm).setOnClickListener(v -> confirmCreateWidget());
opacitySeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
@@ -75,6 +76,8 @@ public class WidgetConfigActivity extends AppCompatActivity {
progress.setVisibility(View.VISIBLE);
progress.setText(R.string.position_default_label);
+ ckPlaybackSpeed = findViewById(R.id.ckPlaybackSpeed);
+ ckPlaybackSpeed.setOnClickListener(v -> displayPreviewPanel());
ckRewind = findViewById(R.id.ckRewind);
ckRewind.setOnClickListener(v -> displayPreviewPanel());
ckFastForward = findViewById(R.id.ckFastForward);
@@ -84,22 +87,26 @@ public class WidgetConfigActivity extends AppCompatActivity {
}
private void displayPreviewPanel() {
- boolean showExtendedPreview = ckRewind.isChecked() || ckFastForward.isChecked() || ckSkip.isChecked();
+ boolean showExtendedPreview =
+ ckPlaybackSpeed.isChecked() || ckRewind.isChecked() || ckFastForward.isChecked() || ckSkip.isChecked();
widgetPreview.findViewById(R.id.extendedButtonsContainer)
.setVisibility(showExtendedPreview ? View.VISIBLE : View.GONE);
widgetPreview.findViewById(R.id.butPlay).setVisibility(showExtendedPreview ? View.GONE : View.VISIBLE);
+ widgetPreview.findViewById(R.id.butPlaybackSpeed)
+ .setVisibility(ckPlaybackSpeed.isChecked() ? View.VISIBLE : View.GONE);
widgetPreview.findViewById(R.id.butFastForward)
.setVisibility(ckFastForward.isChecked() ? View.VISIBLE : View.GONE);
widgetPreview.findViewById(R.id.butSkip).setVisibility(ckSkip.isChecked() ? View.VISIBLE : View.GONE);
widgetPreview.findViewById(R.id.butRew).setVisibility(ckRewind.isChecked() ? View.VISIBLE : View.GONE);
}
- private void confirmCreateWidget(View v) {
+ private void confirmCreateWidget() {
int backgroundColor = getColorWithAlpha(PlayerWidget.DEFAULT_COLOR, opacitySeekBar.getProgress());
SharedPreferences prefs = getSharedPreferences(PlayerWidget.PREFS_NAME, MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
editor.putInt(PlayerWidget.KEY_WIDGET_COLOR + appWidgetId, backgroundColor);
+ editor.putBoolean(PlayerWidget.KEY_WIDGET_PLAYBACK_SPEED + appWidgetId, ckPlaybackSpeed.isChecked());
editor.putBoolean(PlayerWidget.KEY_WIDGET_SKIP + appWidgetId, ckSkip.isChecked());
editor.putBoolean(PlayerWidget.KEY_WIDGET_REWIND + appWidgetId, ckRewind.isChecked());
editor.putBoolean(PlayerWidget.KEY_WIDGET_FAST_FORWARD + appWidgetId, ckFastForward.isChecked());
@@ -109,7 +116,7 @@ public class WidgetConfigActivity extends AppCompatActivity {
resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
setResult(RESULT_OK, resultValue);
finish();
- WidgetUpdaterJobService.performBackgroundUpdate(this);
+ WidgetUpdaterWorker.enqueueWork(this);
}
private int getColorWithAlpha(int color, int opacity) {
diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/DownloadLogAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/DownloadLogAdapter.java
index 072ca8acf..e46073457 100644
--- a/app/src/main/java/de/danoeh/antennapod/adapter/DownloadLogAdapter.java
+++ b/app/src/main/java/de/danoeh/antennapod/adapter/DownloadLogAdapter.java
@@ -17,11 +17,14 @@ import de.danoeh.antennapod.core.service.download.DownloadRequest;
import de.danoeh.antennapod.core.service.download.DownloadRequestCreator;
import de.danoeh.antennapod.core.service.download.DownloadService;
import de.danoeh.antennapod.core.service.download.Downloader;
-import de.danoeh.antennapod.core.service.download.DownloadStatus;
+import de.danoeh.antennapod.core.storage.DBWriter;
+import de.danoeh.antennapod.model.download.DownloadStatus;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.storage.DBTasks;
-import de.danoeh.antennapod.core.util.DownloadError;
+import de.danoeh.antennapod.model.download.DownloadError;
+import de.danoeh.antennapod.core.util.DownloadErrorLabel;
import de.danoeh.antennapod.model.feed.Feed;
+import de.danoeh.antennapod.model.feed.FeedItem;
import de.danoeh.antennapod.model.feed.FeedMedia;
import de.danoeh.antennapod.ui.common.ThemeUtils;
import de.danoeh.antennapod.view.viewholder.DownloadLogItemViewHolder;
@@ -109,7 +112,7 @@ public class DownloadLogAdapter extends BaseAdapter {
holder.icon.setText("{fa-times-circle}");
}
holder.icon.setContentDescription(context.getString(R.string.error_label));
- holder.reason.setText(status.getReason().getErrorString(context));
+ holder.reason.setText(DownloadErrorLabel.from(status.getReason()));
holder.reason.setVisibility(View.VISIBLE);
holder.tapForDetails.setVisibility(View.VISIBLE);
@@ -156,8 +159,15 @@ public class DownloadLogAdapter extends BaseAdapter {
holder.secondaryActionButton.setContentDescription(context.getString(R.string.cancel_download_label));
holder.secondaryActionButton.setVisibility(View.VISIBLE);
holder.secondaryActionButton.setTag(downloader);
- holder.secondaryActionButton.setOnClickListener(v ->
- listFragment.onListItemClick(null, holder.itemView, position, 0));
+ holder.secondaryActionButton.setOnClickListener(v -> {
+ DownloadService.cancel(context, request.getSource());
+ if (request.getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA) {
+ FeedMedia media = DBReader.getFeedMedia(request.getFeedfileId());
+ FeedItem feedItem = media.getItem();
+ feedItem.disableAutoDownload();
+ DBWriter.setFeedItem(feedItem);
+ }
+ });
holder.reason.setVisibility(View.GONE);
holder.tapForDetails.setVisibility(View.GONE);
holder.icon.setTextColor(ThemeUtils.getColorFromAttr(context, R.attr.colorPrimary));
diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/FeedDiscoverAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/FeedDiscoverAdapter.java
index 66fa79a4e..3628b4bee 100644
--- a/app/src/main/java/de/danoeh/antennapod/adapter/FeedDiscoverAdapter.java
+++ b/app/src/main/java/de/danoeh/antennapod/adapter/FeedDiscoverAdapter.java
@@ -8,7 +8,7 @@ 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.discovery.PodcastSearchResult;
+import de.danoeh.antennapod.net.discovery.PodcastSearchResult;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/FeedItemlistDescriptionAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/FeedItemlistDescriptionAdapter.java
index 5ddb6407c..f790a5784 100644
--- a/app/src/main/java/de/danoeh/antennapod/adapter/FeedItemlistDescriptionAdapter.java
+++ b/app/src/main/java/de/danoeh/antennapod/adapter/FeedItemlistDescriptionAdapter.java
@@ -80,8 +80,6 @@ public class FeedItemlistDescriptionAdapter extends ArrayAdapter<FeedItem> {
}
new PlaybackServiceStarter(getContext(), playable)
- .shouldStream(true)
- .startWhenPrepared(true)
.callEvenIfRunning(true)
.start();
diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/PlaybackStatisticsListAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/PlaybackStatisticsListAdapter.java
deleted file mode 100644
index 26674b2b2..000000000
--- a/app/src/main/java/de/danoeh/antennapod/adapter/PlaybackStatisticsListAdapter.java
+++ /dev/null
@@ -1,69 +0,0 @@
-package de.danoeh.antennapod.adapter;
-
-import androidx.fragment.app.Fragment;
-import de.danoeh.antennapod.R;
-import de.danoeh.antennapod.core.preferences.UserPreferences;
-import de.danoeh.antennapod.core.storage.StatisticsItem;
-import de.danoeh.antennapod.core.util.Converter;
-import de.danoeh.antennapod.core.util.DateFormatter;
-import de.danoeh.antennapod.fragment.FeedStatisticsDialogFragment;
-import de.danoeh.antennapod.view.PieChartView;
-
-import java.util.Date;
-import java.util.List;
-
-/**
- * Adapter for the playback statistics list.
- */
-public class PlaybackStatisticsListAdapter extends StatisticsListAdapter {
-
- private final Fragment fragment;
- boolean countAll = true;
-
- public PlaybackStatisticsListAdapter(Fragment fragment) {
- super(fragment.getContext());
- this.fragment = fragment;
- }
-
- public void setCountAll(boolean countAll) {
- this.countAll = countAll;
- }
-
- @Override
- String getHeaderCaption() {
- long usageCounting = UserPreferences.getUsageCountingDateMillis();
- if (usageCounting > 0) {
- String date = DateFormatter.formatAbbrev(context, new Date(usageCounting));
- return context.getString(R.string.statistics_counting_since, date);
- } else {
- return context.getString(R.string.total_time_listened_to_podcasts);
- }
- }
-
- @Override
- String getHeaderValue() {
- return Converter.shortLocalizedDuration(context, (long) pieChartData.getSum());
- }
-
- @Override
- PieChartView.PieChartData generateChartData(List<StatisticsItem> statisticsData) {
- float[] dataValues = new float[statisticsData.size()];
- for (int i = 0; i < statisticsData.size(); i++) {
- StatisticsItem item = statisticsData.get(i);
- dataValues[i] = countAll ? item.timePlayedCountAll : item.timePlayed;
- }
- return new PieChartView.PieChartData(dataValues);
- }
-
- @Override
- void onBindFeedViewHolder(StatisticsHolder holder, StatisticsItem statsItem) {
- long time = countAll ? statsItem.timePlayedCountAll : statsItem.timePlayed;
- holder.value.setText(Converter.shortLocalizedDuration(context, time));
-
- holder.itemView.setOnClickListener(v -> {
- FeedStatisticsDialogFragment yourDialogFragment = FeedStatisticsDialogFragment.newInstance(
- statsItem.feed.getId(), statsItem.feed.getTitle());
- yourDialogFragment.show(fragment.getChildFragmentManager().beginTransaction(), "DialogFragment");
- });
- }
-}
diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/SubscriptionsRecyclerAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/SubscriptionsRecyclerAdapter.java
index 492d49759..7c3166043 100644
--- a/app/src/main/java/de/danoeh/antennapod/adapter/SubscriptionsRecyclerAdapter.java
+++ b/app/src/main/java/de/danoeh/antennapod/adapter/SubscriptionsRecyclerAdapter.java
@@ -33,7 +33,6 @@ import java.util.Locale;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MainActivity;
-import de.danoeh.antennapod.core.feed.LocalFeedUpdater;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.storage.NavDrawerData;
import de.danoeh.antennapod.fragment.FeedItemlistFragment;
@@ -255,7 +254,7 @@ public class SubscriptionsRecyclerAdapter extends SelectableAdapter<Subscription
if (drawerItem.type == NavDrawerData.DrawerItem.Type.FEED) {
Feed feed = ((NavDrawerData.FeedDrawerItem) drawerItem).feed;
boolean textAndImageCombind = feed.isLocalFeed()
- && LocalFeedUpdater.getDefaultIconUrl(itemView.getContext()).equals(feed.getImageUrl());
+ && feed.getImageUrl() != null && feed.getImageUrl().startsWith(Feed.PREFIX_GENERATIVE_COVER);
new CoverLoader(mainActivityRef.get())
.withUri(feed.getImageUrl())
.withPlaceholderView(feedTitle, textAndImageCombind)
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
index 974b12bab..983ae3df9 100644
--- a/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/PlayActionButton.java
+++ b/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/PlayActionButton.java
@@ -41,8 +41,6 @@ public class PlayActionButton extends ItemActionButton {
}
new PlaybackServiceStarter(context, media)
.callEvenIfRunning(true)
- .startWhenPrepared(true)
- .shouldStream(false)
.start();
if (media.getMediaType() == MediaType.VIDEO) {
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
index ab2122b12..ce251f5a6 100644
--- a/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/PlayLocalActionButton.java
+++ b/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/PlayLocalActionButton.java
@@ -37,8 +37,6 @@ public class PlayLocalActionButton extends ItemActionButton {
new PlaybackServiceStarter(context, media)
.callEvenIfRunning(true)
- .startWhenPrepared(true)
- .shouldStream(true)
.start();
if (media.getMediaType() == MediaType.VIDEO) {
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
index 94c446f50..2f5cb5430 100644
--- a/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/StreamActionButton.java
+++ b/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/StreamActionButton.java
@@ -47,8 +47,6 @@ public class StreamActionButton extends ItemActionButton {
}
new PlaybackServiceStarter(context, media)
.callEvenIfRunning(true)
- .startWhenPrepared(true)
- .shouldStream(true)
.start();
if (media.getMediaType() == MediaType.VIDEO) {
diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/itunes/ItunesAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/itunes/ItunesAdapter.java
index 1968c0b62..40c2029d6 100644
--- a/app/src/main/java/de/danoeh/antennapod/adapter/itunes/ItunesAdapter.java
+++ b/app/src/main/java/de/danoeh/antennapod/adapter/itunes/ItunesAdapter.java
@@ -1,10 +1,10 @@
package de.danoeh.antennapod.adapter.itunes;
import android.content.Context;
+import android.widget.ArrayAdapter;
import androidx.annotation.NonNull;
import android.view.View;
import android.view.ViewGroup;
-import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.TextView;
@@ -14,12 +14,12 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.load.resource.bitmap.FitCenter;
import com.bumptech.glide.load.resource.bitmap.RoundedCorners;
import com.bumptech.glide.request.RequestOptions;
-import de.danoeh.antennapod.discovery.PodcastSearchResult;
import java.util.List;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MainActivity;
+import de.danoeh.antennapod.net.discovery.PodcastSearchResult;
public class ItunesAdapter extends ArrayAdapter<PodcastSearchResult> {
/**
diff --git a/app/src/main/java/de/danoeh/antennapod/asynctask/DocumentFileExportWorker.java b/app/src/main/java/de/danoeh/antennapod/asynctask/DocumentFileExportWorker.java
index 73e19c746..8acfcb58f 100644
--- a/app/src/main/java/de/danoeh/antennapod/asynctask/DocumentFileExportWorker.java
+++ b/app/src/main/java/de/danoeh/antennapod/asynctask/DocumentFileExportWorker.java
@@ -36,7 +36,7 @@ public class DocumentFileExportWorker {
OutputStreamWriter writer = null;
try {
Uri uri = output.getUri();
- outputStream = context.getContentResolver().openOutputStream(uri);
+ outputStream = context.getContentResolver().openOutputStream(uri, "wt");
if (outputStream == null) {
throw new IOException();
}
diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/AllEpisodesFilterDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/AllEpisodesFilterDialog.java
new file mode 100644
index 000000000..f47a8f8eb
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/dialog/AllEpisodesFilterDialog.java
@@ -0,0 +1,31 @@
+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<String> newFilterValues) {
+ EventBus.getDefault().post(new AllEpisodesFilterChangedEvent(newFilterValues));
+ }
+
+ public static class AllEpisodesFilterChangedEvent {
+ public final Set<String> filterValues;
+
+ public AllEpisodesFilterChangedEvent(Set<String> 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
new file mode 100644
index 000000000..50fb0651f
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/dialog/DownloadLogDetailsDialog.java
@@ -0,0 +1,59 @@
+package de.danoeh.antennapod.dialog;
+
+import android.content.ClipData;
+import android.content.ClipboardManager;
+import android.content.Context;
+import android.widget.TextView;
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.AlertDialog;
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.model.download.DownloadStatus;
+import de.danoeh.antennapod.core.storage.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 AlertDialog.Builder {
+
+ public DownloadLogDetailsDialog(@NonNull Context context, DownloadStatus status) {
+ super(context);
+
+ String url = "unknown";
+ String message = context.getString(R.string.download_successful);
+ if (status.getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA) {
+ FeedMedia media = DBReader.getFeedMedia(status.getFeedfileId());
+ if (media != null) {
+ url = media.getDownload_url();
+ }
+ } else if (status.getFeedfileType() == Feed.FEEDFILETYPE_FEED) {
+ Feed feed = DBReader.getFeed(status.getFeedfileId());
+ if (feed != null) {
+ url = feed.getDownload_url();
+ }
+ }
+
+ if (!status.isSuccessful()) {
+ message = status.getReasonDetailed();
+ }
+
+ String messageFull = context.getString(R.string.download_error_details_message, 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);
+ 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/FeedItemFilterDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/FeedItemFilterDialog.java
new file mode 100644
index 000000000..e91e88fbf
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/dialog/FeedItemFilterDialog.java
@@ -0,0 +1,26 @@
+package de.danoeh.antennapod.dialog;
+
+import android.os.Bundle;
+import de.danoeh.antennapod.core.storage.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<String> newFilterValues) {
+ long feedId = getArguments().getLong(ARGUMENT_FEED_ID);
+ DBWriter.setFeedItemsFilter(feedId, newFilterValues);
+ }
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/FeedRefreshIntervalDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/FeedRefreshIntervalDialog.java
index 7e42302d1..f97940f8b 100644
--- a/app/src/main/java/de/danoeh/antennapod/dialog/FeedRefreshIntervalDialog.java
+++ b/app/src/main/java/de/danoeh/antennapod/dialog/FeedRefreshIntervalDialog.java
@@ -10,6 +10,7 @@ import android.widget.ArrayAdapter;
import androidx.appcompat.app.AlertDialog;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.core.preferences.UserPreferences;
+import de.danoeh.antennapod.core.util.download.AutoUpdateManager;
import de.danoeh.antennapod.databinding.FeedRefreshDialogBinding;
import org.apache.commons.lang3.ArrayUtils;
@@ -70,6 +71,7 @@ public class FeedRefreshIntervalDialog {
builder.setPositiveButton(R.string.confirm_label, (dialog, which) -> {
if (viewBinding.intervalRadioButton.isChecked()) {
UserPreferences.setUpdateInterval(INTERVAL_VALUES_HOURS[viewBinding.spinner.getSelectedItemPosition()]);
+ AutoUpdateManager.restartUpdateAlarm(context);
} else if (viewBinding.timeRadioButton.isChecked()) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
UserPreferences.setUpdateTimeOfDay(viewBinding.timePicker.getHour(),
@@ -78,8 +80,10 @@ public class FeedRefreshIntervalDialog {
UserPreferences.setUpdateTimeOfDay(viewBinding.timePicker.getCurrentHour(),
viewBinding.timePicker.getCurrentMinute());
}
+ AutoUpdateManager.restartUpdateAlarm(context);
} else if (viewBinding.disableRadioButton.isChecked()) {
- UserPreferences.disableAutoUpdate(context);
+ UserPreferences.disableAutoUpdate();
+ AutoUpdateManager.disableAutoUpdate(context);
} else {
throw new IllegalStateException("Unexpected error.");
}
diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/FilterDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/FilterDialog.java
deleted file mode 100644
index 593b055f6..000000000
--- a/app/src/main/java/de/danoeh/antennapod/dialog/FilterDialog.java
+++ /dev/null
@@ -1,83 +0,0 @@
-package de.danoeh.antennapod.dialog;
-
-import android.content.Context;
-import android.text.TextUtils;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.widget.LinearLayout;
-import android.widget.RadioButton;
-
-import androidx.appcompat.app.AlertDialog;
-
-import java.util.Arrays;
-import java.util.HashSet;
-import java.util.Set;
-
-import de.danoeh.antennapod.R;
-import de.danoeh.antennapod.model.feed.FeedItemFilter;
-import de.danoeh.antennapod.core.feed.FeedItemFilterGroup;
-import de.danoeh.antennapod.ui.common.RecursiveRadioGroup;
-
-public abstract class FilterDialog {
-
- protected FeedItemFilter filter;
- protected Context context;
-
- public FilterDialog(Context context, FeedItemFilter feedItemFilter) {
- this.context = context;
- this.filter = feedItemFilter;
- }
-
- public void openDialog() {
-
- final Set<String> filterValues = new HashSet<>(Arrays.asList(filter.getValues()));
- AlertDialog.Builder builder = new AlertDialog.Builder(context);
- builder.setTitle(R.string.filter);
-
- LayoutInflater inflater = LayoutInflater.from(this.context);
- View layout = inflater.inflate(R.layout.filter_dialog, null, false);
- LinearLayout rows = layout.findViewById(R.id.filter_rows);
- builder.setView(layout);
-
- for (FeedItemFilterGroup item : FeedItemFilterGroup.values()) {
- RecursiveRadioGroup row = (RecursiveRadioGroup) inflater.inflate(R.layout.filter_dialog_row, null, false);
- RadioButton filter1 = row.findViewById(R.id.filter_dialog_radioButton1);
- RadioButton filter2 = row.findViewById(R.id.filter_dialog_radioButton2);
- filter1.setText(item.values[0].displayName);
- filter1.setTag(item.values[0].filterId);
- filter2.setText(item.values[1].displayName);
- filter2.setTag(item.values[1].filterId);
- rows.addView(row);
- }
-
- for (String filterId : filterValues) {
- if (!TextUtils.isEmpty(filterId)) {
- RadioButton button = layout.findViewWithTag(filterId);
- if (button != null) {
- button.setChecked(true);
- }
- }
- }
-
- builder.setPositiveButton(R.string.confirm_label, (dialog, which) -> {
- filterValues.clear();
- for (int i = 0; i < rows.getChildCount(); i++) {
- if (!(rows.getChildAt(i) instanceof RecursiveRadioGroup)) {
- continue;
- }
- RecursiveRadioGroup group = (RecursiveRadioGroup) rows.getChildAt(i);
- if (group.getCheckedButton() != null) {
- String tag = (String) group.getCheckedButton().getTag();
- if (tag != null) { // Clear buttons use no tag
- filterValues.add((String) group.getCheckedButton().getTag());
- }
- }
- }
- updateFilter(filterValues);
- });
- builder.setNegativeButton(R.string.cancel_label, null);
- builder.create().show();
- }
-
- protected abstract void updateFilter(Set<String> filterValues);
-}
diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/ItemFilterDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/ItemFilterDialog.java
new file mode 100644
index 000000000..ae938a6ec
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/dialog/ItemFilterDialog.java
@@ -0,0 +1,75 @@
+package de.danoeh.antennapod.dialog;
+
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+import android.widget.RadioButton;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.core.feed.FeedItemFilterGroup;
+import de.danoeh.antennapod.model.feed.FeedItemFilter;
+import de.danoeh.antennapod.ui.common.RecursiveRadioGroup;
+
+import java.util.HashSet;
+import java.util.Set;
+
+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);
+ rows = layout.findViewById(R.id.filter_rows);
+ FeedItemFilter filter = (FeedItemFilter) getArguments().getSerializable(ARGUMENT_FILTER);
+
+ for (FeedItemFilterGroup item : FeedItemFilterGroup.values()) {
+ RecursiveRadioGroup row = (RecursiveRadioGroup) inflater.inflate(R.layout.filter_dialog_row, null, false);
+ row.setOnCheckedChangeListener((group, checkedId) -> onFilterChanged(getNewFilterValues()));
+ RadioButton filter1 = row.findViewById(R.id.filter_dialog_radioButton1);
+ RadioButton filter2 = row.findViewById(R.id.filter_dialog_radioButton2);
+ filter1.setText(item.values[0].displayName);
+ filter1.setTag(item.values[0].filterId);
+ filter2.setText(item.values[1].displayName);
+ filter2.setTag(item.values[1].filterId);
+ rows.addView(row);
+ }
+
+ for (String filterId : filter.getValues()) {
+ if (!TextUtils.isEmpty(filterId)) {
+ RadioButton button = layout.findViewWithTag(filterId);
+ if (button != null) {
+ button.setChecked(true);
+ }
+ }
+ }
+ return layout;
+ }
+
+ protected Set<String> getNewFilterValues() {
+ final Set<String> newFilterValues = new HashSet<>();
+ for (int i = 0; i < rows.getChildCount(); i++) {
+ if (!(rows.getChildAt(i) instanceof RecursiveRadioGroup)) {
+ continue;
+ }
+ RecursiveRadioGroup group = (RecursiveRadioGroup) rows.getChildAt(i);
+ if (group.getCheckedButton() != null) {
+ String tag = (String) group.getCheckedButton().getTag();
+ if (tag != null) { // Clear buttons use no tag
+ newFilterValues.add((String) group.getCheckedButton().getTag());
+ }
+ }
+ }
+ return newFilterValues;
+ }
+
+ abstract void onFilterChanged(Set<String> newFilterValues);
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/PlaybackControlsDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/PlaybackControlsDialog.java
index 5cc1f99c6..841c121e9 100644
--- a/app/src/main/java/de/danoeh/antennapod/dialog/PlaybackControlsDialog.java
+++ b/app/src/main/java/de/danoeh/antennapod/dialog/PlaybackControlsDialog.java
@@ -10,24 +10,14 @@ import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.DialogFragment;
import android.widget.Button;
import android.widget.CheckBox;
-import android.widget.TextView;
import de.danoeh.antennapod.R;
-import de.danoeh.antennapod.event.playback.SpeedChangedEvent;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.util.playback.PlaybackController;
-import de.danoeh.antennapod.view.PlaybackSpeedSeekBar;
-import org.greenrobot.eventbus.EventBus;
-import org.greenrobot.eventbus.Subscribe;
-import org.greenrobot.eventbus.ThreadMode;
-
import java.util.List;
-import java.util.Locale;
public class PlaybackControlsDialog extends DialogFragment {
private PlaybackController controller;
private AlertDialog dialog;
- private PlaybackSpeedSeekBar speedSeekBar;
- private TextView txtvPlaybackSpeed;
public static PlaybackControlsDialog newInstance() {
Bundle arguments = new Bundle();
@@ -48,12 +38,10 @@ public class PlaybackControlsDialog extends DialogFragment {
public void loadMediaInfo() {
setupUi();
setupAudioTracks();
- updateSpeed(new SpeedChangedEvent(getCurrentPlaybackSpeedMultiplier()));
}
};
controller.init();
setupUi();
- EventBus.getDefault().register(this);
}
@Override
@@ -61,7 +49,6 @@ public class PlaybackControlsDialog extends DialogFragment {
super.onStop();
controller.release();
controller = null;
- EventBus.getDefault().unregister(this);
}
@NonNull
@@ -75,15 +62,6 @@ public class PlaybackControlsDialog extends DialogFragment {
}
private void setupUi() {
- txtvPlaybackSpeed = dialog.findViewById(R.id.txtvPlaybackSpeed);
- speedSeekBar = dialog.findViewById(R.id.speed_seek_bar);
- speedSeekBar.setProgressChangedListener(speed -> {
- if (controller != null) {
- controller.setPlaybackSpeed(speed);
- }
- });
- updateSpeed(new SpeedChangedEvent(controller.getCurrentPlaybackSpeedMultiplier()));
-
final CheckBox stereoToMono = dialog.findViewById(R.id.stereo_to_mono);
stereoToMono.setChecked(UserPreferences.stereoToMono());
if (controller != null && !controller.canDownmix()) {
@@ -111,12 +89,6 @@ public class PlaybackControlsDialog extends DialogFragment {
});
}
- @Subscribe(threadMode = ThreadMode.MAIN)
- public void updateSpeed(SpeedChangedEvent event) {
- txtvPlaybackSpeed.setText(String.format(Locale.getDefault(), "%.2fx", event.getNewSpeed()));
- speedSeekBar.updateSpeed(event.getNewSpeed());
- }
-
private void setupAudioTracks() {
List<String> audioTracks = controller.getAudioTracks();
int selectedAudioTrack = controller.getSelectedAudioTrack();
diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/ProxyDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/ProxyDialog.java
index 2c3c44b1d..266724e78 100644
--- a/app/src/main/java/de/danoeh/antennapod/dialog/ProxyDialog.java
+++ b/app/src/main/java/de/danoeh/antennapod/dialog/ProxyDialog.java
@@ -29,7 +29,7 @@ import java.util.concurrent.TimeUnit;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.service.download.AntennapodHttpClient;
-import de.danoeh.antennapod.core.service.download.ProxyConfig;
+import de.danoeh.antennapod.model.download.ProxyConfig;
import io.reactivex.Completable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
@@ -165,7 +165,9 @@ public class ProxyDialog {
if (!TextUtils.isEmpty(port)) {
portValue = Integer.parseInt(port);
}
- UserPreferences.setProxyConfig(new ProxyConfig(typeEnum, host, portValue, username, password));
+ ProxyConfig config = new ProxyConfig(typeEnum, host, portValue, username, password);
+ UserPreferences.setProxyConfig(config);
+ AntennapodHttpClient.setProxyConfig(config);
}
private final TextWatcher requireTestOnChange = new TextWatcher() {
diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/ShareDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/ShareDialog.java
index 699e40e71..dbd4cdb59 100644
--- a/app/src/main/java/de/danoeh/antennapod/dialog/ShareDialog.java
+++ b/app/src/main/java/de/danoeh/antennapod/dialog/ShareDialog.java
@@ -5,15 +5,13 @@ import android.content.Context;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.view.View;
-import android.widget.CheckBox;
-import android.widget.RadioButton;
-import android.widget.RadioGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.DialogFragment;
import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.databinding.ShareEpisodeDialogBinding;
import de.danoeh.antennapod.model.feed.FeedItem;
import de.danoeh.antennapod.core.util.ShareUtils;
@@ -21,14 +19,13 @@ public class ShareDialog extends DialogFragment {
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 RadioButton radioMediaFile;
- private RadioButton radioLinkToEpisode;
- private CheckBox checkBoxStartAt;
+ ShareEpisodeDialogBinding viewBinding;
public ShareDialog() {
// Empty constructor required for DialogFragment
@@ -51,31 +48,34 @@ public class ShareDialog extends DialogFragment {
prefs = getActivity().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
}
- View content = View.inflate(ctx, R.layout.share_episode_dialog, null);
+ viewBinding = ShareEpisodeDialogBinding.inflate(getLayoutInflater());
+ viewBinding.shareDialogRadioGroup.setOnCheckedChangeListener((group, checkedId) ->
+ viewBinding.sharePositionCheckbox.setEnabled(checkedId == viewBinding.shareSocialRadio.getId()));
+
AlertDialog.Builder builder = new AlertDialog.Builder(ctx);
builder.setTitle(R.string.share_label);
- builder.setView(content);
-
- RadioGroup radioGroup = content.findViewById(R.id.share_dialog_radio_group);
- radioGroup.setOnCheckedChangeListener((group, checkedId) ->
- checkBoxStartAt.setEnabled(checkedId != R.id.share_media_file_radio));
-
- radioLinkToEpisode = content.findViewById(R.id.share_link_to_episode_radio);
- radioMediaFile = content.findViewById(R.id.share_media_file_radio);
- checkBoxStartAt = content.findViewById(R.id.share_start_at_timer_dialog);
-
+ builder.setView(viewBinding.getRoot());
setupOptions();
builder.setPositiveButton(R.string.share_label, (dialog, id) -> {
- boolean includePlaybackPosition = checkBoxStartAt.isChecked();
- if (radioLinkToEpisode.isChecked()) {
+ boolean includePlaybackPosition = viewBinding.sharePositionCheckbox.isChecked();
+ int position;
+ if (viewBinding.shareSocialRadio.isChecked()) {
ShareUtils.shareFeedItemLinkWithDownloadLink(ctx, item, includePlaybackPosition);
- } else if (radioMediaFile.isChecked()) {
+ 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).apply();
+ prefs.edit()
+ .putBoolean(PREF_SHARE_EPISODE_START_AT, includePlaybackPosition)
+ .putInt(PREF_SHARE_EPISODE_TYPE, position)
+ .apply();
}).setNegativeButton(R.string.cancel_label, (dialog, id) -> dialog.dismiss());
return builder.create();
@@ -83,18 +83,22 @@ public class ShareDialog extends DialogFragment {
private void setupOptions() {
final boolean hasMedia = item.getMedia() != null;
-
boolean downloaded = hasMedia && item.getMedia().isDownloaded();
- radioMediaFile.setVisibility(downloaded ? View.VISIBLE : View.GONE);
+ viewBinding.shareMediaFileRadio.setVisibility(downloaded ? View.VISIBLE : View.GONE);
boolean hasDownloadUrl = hasMedia && item.getMedia().getDownload_url() != null;
- if (!ShareUtils.hasLinkToShare(item) && !hasDownloadUrl) {
- radioLinkToEpisode.setVisibility(View.GONE);
+ if (!hasDownloadUrl) {
+ viewBinding.shareMediaReceiverRadio.setVisibility(View.GONE);
}
-
- radioMediaFile.setChecked(false);
+ 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);
- checkBoxStartAt.setChecked(switchIsChecked);
+ viewBinding.sharePositionCheckbox.setChecked(switchIsChecked);
}
}
diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/StreamingConfirmationDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/StreamingConfirmationDialog.java
index 22f62d410..a38463c4a 100644
--- a/app/src/main/java/de/danoeh/antennapod/dialog/StreamingConfirmationDialog.java
+++ b/app/src/main/java/de/danoeh/antennapod/dialog/StreamingConfirmationDialog.java
@@ -32,8 +32,6 @@ public class StreamingConfirmationDialog {
private void stream() {
new PlaybackServiceStarter(context, playable)
.callEvenIfRunning(true)
- .startWhenPrepared(true)
- .shouldStream(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
index 9e524188f..a468794cb 100644
--- a/app/src/main/java/de/danoeh/antennapod/dialog/SubscriptionsFilterDialog.java
+++ b/app/src/main/java/de/danoeh/antennapod/dialog/SubscriptionsFilterDialog.java
@@ -17,7 +17,7 @@ import java.util.Set;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.event.UnreadItemsUpdateEvent;
-import de.danoeh.antennapod.core.feed.SubscriptionsFilter;
+import de.danoeh.antennapod.model.feed.SubscriptionsFilter;
import de.danoeh.antennapod.core.feed.SubscriptionsFilterGroup;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.ui.common.RecursiveRadioGroup;
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/AddFeedFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/AddFeedFragment.java
index 120d1def8..d2a2e6de8 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/AddFeedFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/AddFeedFragment.java
@@ -1,6 +1,7 @@
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;
@@ -34,11 +35,11 @@ import de.danoeh.antennapod.core.storage.DBTasks;
import de.danoeh.antennapod.model.feed.SortOrder;
import de.danoeh.antennapod.databinding.AddfeedBinding;
import de.danoeh.antennapod.databinding.EditTextDialogBinding;
-import de.danoeh.antennapod.discovery.CombinedSearcher;
-import de.danoeh.antennapod.discovery.FyydPodcastSearcher;
-import de.danoeh.antennapod.discovery.ItunesPodcastSearcher;
-import de.danoeh.antennapod.discovery.PodcastIndexPodcastSearcher;
import de.danoeh.antennapod.fragment.gpodnet.GpodnetMainFragment;
+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 io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
@@ -140,9 +141,12 @@ public class AddFeedFragment extends Fragment {
alertViewBinding.urlEditText.setHint(R.string.add_podcast_by_url_hint);
ClipboardManager clipboard = (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE);
- String clipboardContent = clipboard.getText() != null ? clipboard.getText().toString() : "";
- if (clipboardContent.trim().startsWith("http")) {
- alertViewBinding.urlEditText.setText(clipboardContent.trim());
+ 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")) {
+ alertViewBinding.urlEditText.setText(clipboardContent.trim());
+ }
}
builder.setView(alertViewBinding.getRoot());
builder.setPositiveButton(R.string.confirm_label,
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/AllEpisodesFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/AllEpisodesFragment.java
index f65c6fdc6..cbae077cb 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/AllEpisodesFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/AllEpisodesFragment.java
@@ -10,14 +10,14 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.joanzapata.iconify.Iconify;
import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.core.storage.DBReader;
+import de.danoeh.antennapod.dialog.AllEpisodesFilterDialog;
import de.danoeh.antennapod.model.feed.FeedItem;
import de.danoeh.antennapod.model.feed.FeedItemFilter;
-import de.danoeh.antennapod.core.storage.DBReader;
-import de.danoeh.antennapod.dialog.FilterDialog;
import org.apache.commons.lang3.StringUtils;
+import org.greenrobot.eventbus.Subscribe;
import java.util.List;
-import java.util.Set;
/**
* Like 'EpisodesFragment' except that it only shows new episodes and
@@ -43,15 +43,23 @@ public class AllEpisodesFragment extends EpisodesListFragment {
@Override
public boolean onOptionsItemSelected(MenuItem item) {
- if (!super.onOptionsItemSelected(item)) {
- if (item.getItemId() == R.id.filter_items) {
- showFilterDialog();
- return true;
- }
- return false;
- } else {
+ if (super.onOptionsItemSelected(item)) {
+ return true;
+ }
+ if (item.getItemId() == R.id.filter_items) {
+ AllEpisodesFilterDialog.newInstance(feedItemFilter).show(getChildFragmentManager(), null);
return true;
}
+ return false;
+ }
+
+ @Subscribe
+ public void onFilterChanged(AllEpisodesFilterDialog.AllEpisodesFilterChangedEvent event) {
+ feedItemFilter = new FeedItemFilter(event.filterValues.toArray(new String[0]));
+ SharedPreferences prefs = getActivity().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
+ prefs.edit().putString(PREF_FILTER, StringUtils.join(event.filterValues, ",")).apply();
+ page = 1;
+ loadItems();
}
@Override
@@ -75,20 +83,6 @@ public class AllEpisodesFragment extends EpisodesListFragment {
}
}
- private void showFilterDialog() {
- FilterDialog filterDialog = new FilterDialog(getContext(), feedItemFilter) {
- @Override
- protected void updateFilter(Set<String> filterValues) {
- feedItemFilter = new FeedItemFilter(filterValues.toArray(new String[0]));
- SharedPreferences prefs = getActivity().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
- prefs.edit().putString(PREF_FILTER, StringUtils.join(filterValues, ",")).apply();
- loadItems();
- }
- };
-
- filterDialog.openDialog();
- }
-
@Override
protected boolean shouldUpdatedItemRemainInList(FeedItem item) {
SharedPreferences prefs = getActivity().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/ChaptersFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/ChaptersFragment.java
index 04ad6e2bd..de63227e5 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/ChaptersFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/ChaptersFragment.java
@@ -71,7 +71,7 @@ public class ChaptersFragment extends AppCompatDialogFragment {
}
Chapter chapter = adapter.getItem(pos);
controller.seekTo((int) chapter.getStart());
- updateChapterSelection(pos);
+ updateChapterSelection(pos, true);
});
recyclerView.setAdapter(adapter);
@@ -117,7 +117,7 @@ public class ChaptersFragment extends AppCompatDialogFragment {
@Subscribe(threadMode = ThreadMode.MAIN)
public void onEventMainThread(PlaybackPositionEvent event) {
- updateChapterSelection(getCurrentChapter(media));
+ updateChapterSelection(getCurrentChapter(media), false);
adapter.notifyTimeChanged(event.getPosition());
}
@@ -160,10 +160,10 @@ public class ChaptersFragment extends AppCompatDialogFragment {
}
adapter.setMedia(media);
int positionOfCurrentChapter = getCurrentChapter(media);
- updateChapterSelection(positionOfCurrentChapter);
+ updateChapterSelection(positionOfCurrentChapter, true);
}
- private void updateChapterSelection(int position) {
+ private void updateChapterSelection(int position, boolean scrollTo) {
if (adapter == null) {
return;
}
@@ -171,8 +171,8 @@ public class ChaptersFragment extends AppCompatDialogFragment {
if (position != -1 && focusedChapter != position) {
focusedChapter = position;
adapter.notifyChapterChanged(focusedChapter);
- if (layoutManager.findFirstCompletelyVisibleItemPosition() >= position
- || layoutManager.findLastCompletelyVisibleItemPosition() <= position) {
+ 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
index 14b8cf6ed..cf0a96e91 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/CompletedDownloadsFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/CompletedDownloadsFragment.java
@@ -35,6 +35,7 @@ import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.util.FeedItemUtil;
import de.danoeh.antennapod.core.util.download.AutoUpdateManager;
import de.danoeh.antennapod.menuhandler.FeedItemMenuHandler;
+import de.danoeh.antennapod.ui.common.PagedToolbarFragment;
import de.danoeh.antennapod.view.EmptyViewHandler;
import de.danoeh.antennapod.view.EpisodeItemListRecyclerView;
import de.danoeh.antennapod.view.viewholder.EpisodeItemViewHolder;
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/DiscoveryFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/DiscoveryFragment.java
index 230a0ce0d..274fc5878 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/DiscoveryFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/DiscoveryFragment.java
@@ -18,14 +18,14 @@ import androidx.annotation.NonNull;
import androidx.appcompat.widget.Toolbar;
import androidx.fragment.app.Fragment;
+import de.danoeh.antennapod.net.discovery.ItunesTopListLoader;
+import de.danoeh.antennapod.net.discovery.PodcastSearchResult;
import org.greenrobot.eventbus.EventBus;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.OnlineFeedViewActivity;
import de.danoeh.antennapod.adapter.itunes.ItunesAdapter;
import de.danoeh.antennapod.event.DiscoveryDefaultUpdateEvent;
-import de.danoeh.antennapod.discovery.ItunesTopListLoader;
-import de.danoeh.antennapod.discovery.PodcastSearchResult;
import io.reactivex.disposables.Disposable;
import java.util.ArrayList;
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/DownloadLogFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/DownloadLogFragment.java
index f43828000..9f0995f38 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/DownloadLogFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/DownloadLogFragment.java
@@ -1,38 +1,28 @@
package de.danoeh.antennapod.fragment;
-import android.app.Dialog;
-import android.content.ClipData;
-import android.content.ClipboardManager;
-import android.content.Context;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.ListView;
-import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.ListFragment;
-import com.google.android.material.snackbar.Snackbar;
import de.danoeh.antennapod.R;
-import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.adapter.DownloadLogAdapter;
import de.danoeh.antennapod.core.event.DownloadEvent;
import de.danoeh.antennapod.core.event.DownloadLogEvent;
import de.danoeh.antennapod.core.event.DownloaderUpdate;
import de.danoeh.antennapod.core.menuhandler.MenuItemUtils;
-import de.danoeh.antennapod.core.service.download.DownloadRequest;
import de.danoeh.antennapod.core.service.download.DownloadService;
-import de.danoeh.antennapod.core.service.download.DownloadStatus;
+import de.danoeh.antennapod.model.download.DownloadStatus;
import de.danoeh.antennapod.core.service.download.Downloader;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.storage.DBWriter;
import de.danoeh.antennapod.core.util.download.AutoUpdateManager;
-import de.danoeh.antennapod.model.feed.Feed;
-import de.danoeh.antennapod.model.feed.FeedItem;
-import de.danoeh.antennapod.model.feed.FeedMedia;
+import de.danoeh.antennapod.dialog.DownloadLogDetailsDialog;
+import de.danoeh.antennapod.ui.common.PagedToolbarFragment;
import de.danoeh.antennapod.view.EmptyViewHandler;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
@@ -105,51 +95,8 @@ public class DownloadLogFragment extends ListFragment {
super.onListItemClick(l, v, position, id);
Object item = adapter.getItem(position);
- if (item instanceof Downloader) {
- DownloadRequest downloadRequest = ((Downloader) item).getDownloadRequest();
- DownloadService.cancel(getContext(), downloadRequest.getSource());
-
- if (downloadRequest.getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA) {
- FeedMedia media = DBReader.getFeedMedia(downloadRequest.getFeedfileId());
- FeedItem feedItem = media.getItem();
- feedItem.disableAutoDownload();
- DBWriter.setFeedItem(feedItem);
- }
- } else if (item instanceof DownloadStatus) {
- DownloadStatus status = (DownloadStatus) item;
- String url = "unknown";
- String message = getString(R.string.download_successful);
- if (status.getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA) {
- FeedMedia media = DBReader.getFeedMedia(status.getFeedfileId());
- if (media != null) {
- url = media.getDownload_url();
- }
- } else if (status.getFeedfileType() == Feed.FEEDFILETYPE_FEED) {
- Feed feed = DBReader.getFeed(status.getFeedfileId());
- if (feed != null) {
- url = feed.getDownload_url();
- }
- }
-
- if (!status.isSuccessful()) {
- message = status.getReasonDetailed();
- }
-
- String messageFull = getString(R.string.download_error_details_message, message, url);
- AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
- builder.setTitle(R.string.download_error_details);
- builder.setMessage(messageFull);
- builder.setPositiveButton(android.R.string.ok, null);
- builder.setNeutralButton(R.string.copy_to_clipboard, (dialog, which) -> {
- ClipboardManager clipboard = (ClipboardManager) getContext()
- .getSystemService(Context.CLIPBOARD_SERVICE);
- ClipData clip = ClipData.newPlainText(getString(R.string.download_error_details), messageFull);
- clipboard.setPrimaryClip(clip);
- ((MainActivity) getActivity()).showSnackbarAbovePlayer(
- R.string.copied_to_clipboard, Snackbar.LENGTH_SHORT);
- });
- Dialog dialog = builder.show();
- ((TextView) dialog.findViewById(android.R.id.message)).setTextIsSelectable(true);
+ if (item instanceof DownloadStatus) {
+ new DownloadLogDetailsDialog(getContext(), (DownloadStatus) item).show();
}
}
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/DownloadsFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/DownloadsFragment.java
index bc3884b37..4467a0cf0 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/DownloadsFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/DownloadsFragment.java
@@ -19,6 +19,7 @@ import com.google.android.material.tabs.TabLayoutMediator;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MainActivity;
+import de.danoeh.antennapod.ui.common.PagedToolbarFragment;
/**
* Shows the CompletedDownloadsFragment and the RunningDownloadsFragment.
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/EpisodesFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/EpisodesFragment.java
index cfa226f8f..bb987666e 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/EpisodesFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/EpisodesFragment.java
@@ -18,6 +18,7 @@ import com.google.android.material.tabs.TabLayoutMediator;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MainActivity;
+import de.danoeh.antennapod.ui.common.PagedToolbarFragment;
public class EpisodesFragment extends PagedToolbarFragment {
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/EpisodesListFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/EpisodesListFragment.java
index 9b2156a2b..b78b777f9 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/EpisodesListFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/EpisodesListFragment.java
@@ -28,6 +28,7 @@ import de.danoeh.antennapod.event.playback.PlaybackPositionEvent;
import de.danoeh.antennapod.event.PlayerStatusEvent;
import de.danoeh.antennapod.event.UnreadItemsUpdateEvent;
import de.danoeh.antennapod.core.menuhandler.MenuItemUtils;
+import de.danoeh.antennapod.ui.common.PagedToolbarFragment;
import de.danoeh.antennapod.view.EpisodeItemListRecyclerView;
import de.danoeh.antennapod.view.viewholder.EpisodeItemViewHolder;
import org.greenrobot.eventbus.EventBus;
@@ -397,6 +398,7 @@ public abstract class EpisodesListFragment extends Fragment {
.observeOn(AndroidSchedulers.mainThread())
.subscribe(data -> {
progLoading.setVisibility(View.GONE);
+ loadingMoreView.setVisibility(View.GONE);
hasMoreItems = true;
episodes = data;
onFragmentLoaded(episodes);
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/FeedInfoFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/FeedInfoFragment.java
index ccc4e2e5f..7902a4988 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/FeedInfoFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/FeedInfoFragment.java
@@ -42,10 +42,11 @@ import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.storage.DBTasks;
import de.danoeh.antennapod.core.util.IntentUtils;
import de.danoeh.antennapod.core.util.syndication.HtmlToPlainText;
-import de.danoeh.antennapod.fragment.preferences.StatisticsFragment;
import de.danoeh.antennapod.menuhandler.FeedMenuHandler;
import de.danoeh.antennapod.model.feed.Feed;
import de.danoeh.antennapod.model.feed.FeedFunding;
+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;
@@ -123,7 +124,7 @@ public class FeedInfoFragment extends Fragment implements Toolbar.OnMenuItemClic
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_parent)
+ toolbar.getMenu().findItem(R.id.share_item)
.setIcon(AppCompatResources.getDrawable(themedContext, R.drawable.ic_share));
}
};
@@ -138,6 +139,7 @@ public class FeedInfoFragment extends Fragment implements Toolbar.OnMenuItemClic
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));
@@ -260,13 +262,8 @@ public class FeedInfoFragment extends Fragment implements Toolbar.OnMenuItemClic
}
private void refreshToolbarState() {
- boolean shareLinkVisible = feed != null && feed.getLink() != null;
- boolean downloadUrlVisible = feed != null && !feed.isLocalFeed();
-
toolbar.getMenu().findItem(R.id.reconnect_local_folder).setVisible(feed != null && feed.isLocalFeed());
- toolbar.getMenu().findItem(R.id.share_download_url_item).setVisible(downloadUrlVisible);
- toolbar.getMenu().findItem(R.id.share_link_item).setVisible(shareLinkVisible);
- toolbar.getMenu().findItem(R.id.share_parent).setVisible(downloadUrlVisible || shareLinkVisible);
+ 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()))));
}
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/FeedItemlistFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/FeedItemlistFragment.java
index d6ff15799..bb20cb4bc 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/FeedItemlistFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/FeedItemlistFragment.java
@@ -9,83 +9,73 @@ import android.os.Handler;
import android.os.Looper;
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.ImageButton;
-import android.widget.ImageView;
-import android.widget.ProgressBar;
-import android.widget.TextView;
import android.widget.Toast;
-
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.appcompat.widget.Toolbar;
+import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.RecyclerView;
-import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
-
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.snackbar.Snackbar;
import com.joanzapata.iconify.Iconify;
-import com.joanzapata.iconify.widget.IconTextView;
import com.leinardi.android.speeddial.SpeedDialView;
-
-import de.danoeh.antennapod.core.menuhandler.MenuItemUtils;
-import org.apache.commons.lang3.Validate;
-import org.greenrobot.eventbus.EventBus;
-import org.greenrobot.eventbus.Subscribe;
-import org.greenrobot.eventbus.ThreadMode;
-
-import java.util.List;
-import java.util.Set;
-
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.adapter.EpisodeItemListAdapter;
import de.danoeh.antennapod.core.event.DownloadEvent;
import de.danoeh.antennapod.core.event.DownloaderUpdate;
-import de.danoeh.antennapod.event.FavoritesEvent;
-import de.danoeh.antennapod.event.FeedItemEvent;
-import de.danoeh.antennapod.event.FeedListUpdateEvent;
-import de.danoeh.antennapod.event.playback.PlaybackPositionEvent;
-import de.danoeh.antennapod.event.PlayerStatusEvent;
-import de.danoeh.antennapod.event.QueueEvent;
-import de.danoeh.antennapod.event.UnreadItemsUpdateEvent;
import de.danoeh.antennapod.core.feed.FeedEvent;
import de.danoeh.antennapod.core.glide.ApGlideSettings;
import de.danoeh.antennapod.core.glide.FastBlurTransformation;
+import de.danoeh.antennapod.core.menuhandler.MenuItemUtils;
import de.danoeh.antennapod.core.service.download.DownloadService;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.storage.DBTasks;
-import de.danoeh.antennapod.core.storage.DBWriter;
import de.danoeh.antennapod.core.util.FeedItemPermutors;
import de.danoeh.antennapod.core.util.FeedItemUtil;
import de.danoeh.antennapod.core.util.gui.MoreContentListFooterUtil;
-import de.danoeh.antennapod.dialog.FilterDialog;
+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.RemoveFeedDialog;
import de.danoeh.antennapod.dialog.RenameItemDialog;
-import de.danoeh.antennapod.fragment.swipeactions.SwipeActions;
+import de.danoeh.antennapod.event.FavoritesEvent;
+import de.danoeh.antennapod.event.FeedItemEvent;
+import de.danoeh.antennapod.event.FeedListUpdateEvent;
+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.menuhandler.FeedMenuHandler;
+import de.danoeh.antennapod.model.download.DownloadStatus;
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.view.EpisodeItemListRecyclerView;
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;
-import android.view.KeyEvent;
-import androidx.fragment.app.Fragment;
+import org.apache.commons.lang3.Validate;
+import org.greenrobot.eventbus.EventBus;
+import org.greenrobot.eventbus.Subscribe;
+import org.greenrobot.eventbus.ThreadMode;
+
+import java.util.List;
/**
* Displays a list of FeedItems.
@@ -99,29 +89,14 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem
private FeedItemListAdapter adapter;
private SwipeActions swipeActions;
private MoreContentListFooterUtil nextPageLoader;
-
- private ProgressBar progressBar;
- private EpisodeItemListRecyclerView recyclerView;
- private TextView txtvTitle;
- private IconTextView txtvFailure;
- private ImageView imgvBackground;
- private ImageView imgvCover;
- private TextView txtvInformation;
- private TextView txtvAuthor;
- private TextView txtvUpdatesDisabled;
- private ImageButton butShowInfo;
- private ImageButton butShowSettings;
- private View header;
- private Toolbar toolbar;
- private SpeedDialView speedDialView;
-
private boolean displayUpArrow;
-
private long feedID;
private Feed feed;
private boolean headerCreated = false;
private boolean isUpdatingFeed;
private Disposable disposable;
+ private FeedItemListFragmentBinding viewBinding;
+ private MultiSelectSpeedDialBinding speedDialBinding;
/**
* Creates new ItemlistFragment which shows the Feeditems of a specific
@@ -141,7 +116,6 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
- setRetainInstance(true);
Bundle args = getArguments();
Validate.notNull(args);
@@ -152,84 +126,63 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
- View root = inflater.inflate(R.layout.feed_item_list_fragment, container, false);
- toolbar = root.findViewById(R.id.toolbar);
- toolbar.inflateMenu(R.menu.feedlist);
- toolbar.setOnMenuItemClickListener(this);
+ viewBinding = FeedItemListFragmentBinding.inflate(inflater);
+ speedDialBinding = MultiSelectSpeedDialBinding.bind(viewBinding.getRoot());
+ viewBinding.toolbar.inflateMenu(R.menu.feedlist);
+ viewBinding.toolbar.setOnMenuItemClickListener(this);
displayUpArrow = getParentFragmentManager().getBackStackEntryCount() != 0;
if (savedInstanceState != null) {
displayUpArrow = savedInstanceState.getBoolean(KEY_UP_ARROW);
}
- ((MainActivity) getActivity()).setupToolbarToggle(toolbar, displayUpArrow);
+ ((MainActivity) getActivity()).setupToolbarToggle(viewBinding.toolbar, displayUpArrow);
refreshToolbarState();
- recyclerView = root.findViewById(R.id.recyclerView);
- recyclerView.setRecycledViewPool(((MainActivity) getActivity()).getRecycledViewPool());
-
- progressBar = root.findViewById(R.id.progLoading);
- progressBar.setVisibility(View.VISIBLE);
- txtvTitle = root.findViewById(R.id.txtvTitle);
- txtvAuthor = root.findViewById(R.id.txtvAuthor);
- imgvBackground = root.findViewById(R.id.imgvBackground);
- imgvCover = root.findViewById(R.id.imgvCover);
- butShowInfo = root.findViewById(R.id.butShowInfo);
- butShowSettings = root.findViewById(R.id.butShowSettings);
- txtvInformation = root.findViewById(R.id.txtvInformation);
- txtvFailure = root.findViewById(R.id.txtvFailure);
- txtvUpdatesDisabled = root.findViewById(R.id.txtvUpdatesDisabled);
- header = root.findViewById(R.id.headerContainer);
- AppBarLayout appBar = root.findViewById(R.id.appBar);
- CollapsingToolbarLayout collapsingToolbar = root.findViewById(R.id.collapsing_toolbar);
-
- ToolbarIconTintManager iconTintManager = new ToolbarIconTintManager(getContext(), toolbar, collapsingToolbar) {
+ viewBinding.recyclerView.setRecycledViewPool(((MainActivity) getActivity()).getRecycledViewPool());
+ viewBinding.progLoading.setVisibility(View.VISIBLE);
+ ToolbarIconTintManager iconTintManager = new ToolbarIconTintManager(
+ getContext(), viewBinding.toolbar, viewBinding.collapsingToolbar) {
@Override
protected void doTint(Context themedContext) {
- toolbar.getMenu().findItem(R.id.sort_items)
- .setIcon(AppCompatResources.getDrawable(themedContext, R.drawable.ic_sort));
- toolbar.getMenu().findItem(R.id.filter_items)
- .setIcon(AppCompatResources.getDrawable(themedContext, R.drawable.ic_filter));
- toolbar.getMenu().findItem(R.id.refresh_item)
+ viewBinding.toolbar.getMenu().findItem(R.id.refresh_item)
.setIcon(AppCompatResources.getDrawable(themedContext, R.drawable.ic_refresh));
- toolbar.getMenu().findItem(R.id.action_search)
+ viewBinding.toolbar.getMenu().findItem(R.id.action_search)
.setIcon(AppCompatResources.getDrawable(themedContext, R.drawable.ic_search));
}
};
iconTintManager.updateTint();
- appBar.addOnOffsetChangedListener(iconTintManager);
+ viewBinding.appBar.addOnOffsetChangedListener(iconTintManager);
- nextPageLoader = new MoreContentListFooterUtil(root.findViewById(R.id.more_content_list_footer));
+ nextPageLoader = new MoreContentListFooterUtil(viewBinding.moreContent.moreContentListFooter);
nextPageLoader.setClickListener(() -> {
if (feed != null) {
DBTasks.loadNextPageOfFeed(getActivity(), feed, false);
}
});
- recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
+ 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;
nextPageLoader.getRoot().setVisibility(
- (recyclerView.isScrolledToBottom() && hasMorePages) ? View.VISIBLE : View.GONE);
+ (viewBinding.recyclerView.isScrolledToBottom() && hasMorePages) ? View.VISIBLE : View.GONE);
}
});
EventBus.getDefault().register(this);
- SwipeRefreshLayout swipeRefreshLayout = root.findViewById(R.id.swipeRefresh);
- swipeRefreshLayout.setDistanceToTriggerSync(getResources().getInteger(R.integer.swipe_refresh_distance));
- swipeRefreshLayout.setOnRefreshListener(() -> {
+ viewBinding.swipeRefresh.setDistanceToTriggerSync(getResources().getInteger(R.integer.swipe_refresh_distance));
+ viewBinding.swipeRefresh.setOnRefreshListener(() -> {
DBTasks.forceRefreshFeed(requireContext(), feed, true);
- new Handler(Looper.getMainLooper()).postDelayed(() -> swipeRefreshLayout.setRefreshing(false),
+ new Handler(Looper.getMainLooper()).postDelayed(() -> viewBinding.swipeRefresh.setRefreshing(false),
getResources().getInteger(R.integer.swipe_to_refresh_duration_in_ms));
});
loadItems();
// Init action UI (via a FAB Speed Dial)
- 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() {
+ 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;
@@ -240,17 +193,17 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem
if (open && adapter.getSelectedCount() == 0) {
((MainActivity) getActivity()).showSnackbarAbovePlayer(R.string.no_items_selected,
Snackbar.LENGTH_SHORT);
- speedDialView.close();
+ speedDialBinding.fabSD.close();
}
}
});
- speedDialView.setOnActionSelectedListener(actionItem -> {
+ speedDialBinding.fabSD.setOnActionSelectedListener(actionItem -> {
new EpisodeMultiSelectActionHandler(((MainActivity) getActivity()), adapter.getSelectedItems())
.handleAction(actionItem.getId());
adapter.endSelectMode();
return true;
});
- return root;
+ return viewBinding.getRoot();
}
@Override
@@ -280,19 +233,20 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem
if (feed == null) {
return;
}
- toolbar.getMenu().findItem(R.id.share_link_item).setVisible(feed.getLink() != null);
- toolbar.getMenu().findItem(R.id.visit_website_item).setVisible(feed.getLink() != null);
+ viewBinding.toolbar.getMenu().findItem(R.id.visit_website_item).setVisible(feed.getLink() != null);
- isUpdatingFeed = MenuItemUtils.updateRefreshMenuItem(toolbar.getMenu(),
+ isUpdatingFeed = MenuItemUtils.updateRefreshMenuItem(viewBinding.toolbar.getMenu(),
R.id.refresh_item, updateRefreshMenuItemChecker);
- FeedMenuHandler.onPrepareOptionsMenu(toolbar.getMenu(), feed);
+ FeedMenuHandler.onPrepareOptionsMenu(viewBinding.toolbar.getMenu(), feed);
}
@Override
public void onConfigurationChanged(@NonNull Configuration newConfig) {
super.onConfigurationChanged(newConfig);
int horizontalSpacing = (int) getResources().getDimension(R.dimen.additional_horizontal_spacing);
- header.setPadding(horizontalSpacing, header.getPaddingTop(), horizontalSpacing, header.getPaddingBottom());
+ viewBinding.header.headerContainer.setPadding(
+ horizontalSpacing, viewBinding.header.headerContainer.getPaddingTop(),
+ horizontalSpacing, viewBinding.header.headerContainer.getPaddingBottom());
}
@Override
@@ -393,7 +347,8 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem
public void onEventMainThread(PlaybackPositionEvent event) {
if (adapter != null) {
for (int i = 0; i < adapter.getItemCount(); i++) {
- EpisodeItemViewHolder holder = (EpisodeItemViewHolder) recyclerView.findViewHolderForAdapterPosition(i);
+ EpisodeItemViewHolder holder = (EpisodeItemViewHolder)
+ viewBinding.recyclerView.findViewHolderForAdapterPosition(i);
if (holder != null && holder.isCurrentlyPlayingItem()) {
holder.notifyPlaybackPositionUpdated(event);
break;
@@ -416,18 +371,18 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem
public void onStartSelectMode() {
swipeActions.detach();
if (feed.isLocalFeed()) {
- speedDialView.removeActionItemById(R.id.download_batch);
- speedDialView.removeActionItemById(R.id.delete_batch);
+ speedDialBinding.fabSD.removeActionItemById(R.id.download_batch);
+ speedDialBinding.fabSD.removeActionItemById(R.id.delete_batch);
}
- speedDialView.setVisibility(View.VISIBLE);
+ speedDialBinding.fabSD.setVisibility(View.VISIBLE);
refreshToolbarState();
}
@Override
public void onEndSelectMode() {
- speedDialView.close();
- speedDialView.setVisibility(View.GONE);
- swipeActions.attachTo(recyclerView);
+ speedDialBinding.fabSD.close();
+ speedDialBinding.fabSD.setVisibility(View.GONE);
+ swipeActions.attachTo(viewBinding.recyclerView);
}
private void updateUi() {
@@ -468,13 +423,13 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem
return;
}
if (adapter == null) {
- recyclerView.setAdapter(null);
+ viewBinding.recyclerView.setAdapter(null);
adapter = new FeedItemListAdapter((MainActivity) getActivity());
adapter.setOnSelectModeListener(this);
- recyclerView.setAdapter(adapter);
- swipeActions = new SwipeActions(this, TAG).attachTo(recyclerView);
+ viewBinding.recyclerView.setAdapter(adapter);
+ swipeActions = new SwipeActions(this, TAG).attachTo(viewBinding.recyclerView);
}
- progressBar.setVisibility(View.GONE);
+ viewBinding.progLoading.setVisibility(View.GONE);
if (feed != null) {
adapter.updateItems(feed.getItems());
swipeActions.setFilter(feed.getItemFilter());
@@ -486,47 +441,40 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem
private void refreshHeaderView() {
setupHeaderView();
- if (recyclerView == null || feed == null) {
+ if (viewBinding == null || feed == null) {
Log.e(TAG, "Unable to refresh header view");
return;
}
loadFeedImage();
if (feed.hasLastUpdateFailed()) {
- txtvFailure.setVisibility(View.VISIBLE);
+ viewBinding.header.txtvFailure.setVisibility(View.VISIBLE);
} else {
- txtvFailure.setVisibility(View.GONE);
+ viewBinding.header.txtvFailure.setVisibility(View.GONE);
}
if (!feed.getPreferences().getKeepUpdated()) {
- txtvUpdatesDisabled.setText("{md-pause-circle-outline} " + this.getString(R.string.updates_disabled_label));
- Iconify.addIcons(txtvUpdatesDisabled);
- txtvUpdatesDisabled.setVisibility(View.VISIBLE);
+ viewBinding.header.txtvUpdatesDisabled.setText("{md-pause-circle-outline} "
+ + this.getString(R.string.updates_disabled_label));
+ Iconify.addIcons(viewBinding.header.txtvUpdatesDisabled);
+ viewBinding.header.txtvUpdatesDisabled.setVisibility(View.VISIBLE);
} else {
- txtvUpdatesDisabled.setVisibility(View.GONE);
+ viewBinding.header.txtvUpdatesDisabled.setVisibility(View.GONE);
}
- txtvTitle.setText(feed.getTitle());
- txtvAuthor.setText(feed.getAuthor());
+ 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) {
- txtvInformation.setText("{md-info-outline} " + this.getString(R.string.filtered_label));
- Iconify.addIcons(txtvInformation);
- txtvInformation.setOnClickListener((l) -> {
- FilterDialog filterDialog = new FilterDialog(requireContext(), feed.getItemFilter()) {
- @Override
- protected void updateFilter(Set<String> filterValues) {
- feed.setItemFilter(filterValues.toArray(new String[0]));
- DBWriter.setFeedItemsFilter(feed.getId(), filterValues);
- }
- };
-
- filterDialog.openDialog();
- });
- txtvInformation.setVisibility(View.VISIBLE);
+ viewBinding.header.txtvInformation.setText("{md-info-outline} "
+ + this.getString(R.string.filtered_label));
+ Iconify.addIcons(viewBinding.header.txtvInformation);
+ viewBinding.header.txtvInformation.setOnClickListener(l ->
+ FeedItemFilterDialog.newInstance(feed).show(getChildFragmentManager(), null));
+ viewBinding.header.txtvInformation.setVisibility(View.VISIBLE);
} else {
- txtvInformation.setVisibility(View.GONE);
+ viewBinding.header.txtvInformation.setVisibility(View.GONE);
}
} else {
- txtvInformation.setVisibility(View.GONE);
+ viewBinding.header.txtvInformation.setVisibility(View.GONE);
}
}
@@ -536,28 +484,45 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem
}
// https://github.com/bumptech/glide/issues/529
- imgvBackground.setColorFilter(new LightingColorFilter(0xff666666, 0x000000));
- butShowInfo.setVisibility(View.VISIBLE);
- butShowInfo.setOnClickListener(v -> showFeedInfo());
- imgvCover.setOnClickListener(v -> showFeedInfo());
- butShowSettings.setVisibility(View.VISIBLE);
- butShowSettings.setOnClickListener(v -> {
+ 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);
}
});
- txtvFailure.setOnClickListener(v -> {
- Intent intent = new Intent(getContext(), MainActivity.class);
- intent.putExtra(MainActivity.EXTRA_FRAGMENT_TAG, DownloadsFragment.TAG);
- Bundle args = new Bundle();
- args.putInt(DownloadsFragment.ARG_SELECTED_TAB, DownloadsFragment.POS_LOG);
- intent.putExtra(MainActivity.EXTRA_FRAGMENT_ARGS, args);
- startActivity(intent);
- });
+ 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<DownloadStatus> 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(),
+ () -> {
+ Intent intent = new Intent(getContext(), MainActivity.class);
+ intent.putExtra(MainActivity.EXTRA_FRAGMENT_TAG, DownloadsFragment.TAG);
+ Bundle args = new Bundle();
+ args.putInt(DownloadsFragment.ARG_SELECTED_TAB, DownloadsFragment.POS_LOG);
+ intent.putExtra(MainActivity.EXTRA_FRAGMENT_ARGS, args);
+ startActivity(intent);
+ });
+ }
+
private void showFeedInfo() {
if (feed != null) {
FeedInfoFragment fragment = FeedInfoFragment.newInstance(feed);
@@ -574,7 +539,7 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem
.diskCacheStrategy(ApGlideSettings.AP_DISK_CACHE_STRATEGY)
.transform(new FastBlurTransformation())
.dontAnimate())
- .into(imgvBackground);
+ .into(viewBinding.imgvBackground);
Glide.with(getActivity())
.load(feed.getImageUrl())
@@ -584,7 +549,7 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem
.diskCacheStrategy(ApGlideSettings.AP_DISK_CACHE_STRATEGY)
.fitCenter()
.dontAnimate())
- .into(imgvCover);
+ .into(viewBinding.header.imgvCover);
}
private void loadItems() {
@@ -629,10 +594,10 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem
}
switch (event.getKeyCode()) {
case KeyEvent.KEYCODE_T:
- recyclerView.smoothScrollToPosition(0);
+ viewBinding.recyclerView.smoothScrollToPosition(0);
break;
case KeyEvent.KEYCODE_B:
- recyclerView.smoothScrollToPosition(adapter.getItemCount() - 1);
+ viewBinding.recyclerView.smoothScrollToPosition(adapter.getItemCount() - 1);
break;
default:
break;
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/ItemFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/ItemFragment.java
index afb0ab8c5..974cbb370 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/ItemFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/ItemFragment.java
@@ -192,9 +192,9 @@ public class ItemFragment extends Fragment {
}
private void showOnDemandConfigBalloon(boolean offerStreaming) {
- boolean isLocaleRtl = TextUtils.getLayoutDirectionFromLocale(Locale.getDefault())
+ final boolean isLocaleRtl = TextUtils.getLayoutDirectionFromLocale(Locale.getDefault())
== View.LAYOUT_DIRECTION_RTL;
- Balloon balloon = new Balloon.Builder(getContext())
+ final Balloon balloon = new Balloon.Builder(getContext())
.setArrowOrientation(ArrowOrientation.TOP)
.setArrowOrientationRules(ArrowOrientationRules.ALIGN_FIXED)
.setArrowPosition(0.25f + ((isLocaleRtl ^ offerStreaming) ? 0f : 0.5f))
@@ -207,9 +207,9 @@ public class ItemFragment extends Fragment {
.setDismissWhenTouchOutside(true)
.setLifecycleOwner(this)
.build();
- Button positiveButton = balloon.getContentView().findViewById(R.id.balloon_button_positive);
- Button negativeButton = balloon.getContentView().findViewById(R.id.balloon_button_negative);
- TextView message = balloon.getContentView().findViewById(R.id.balloon_message);
+ 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 -> {
@@ -221,7 +221,7 @@ public class ItemFragment extends Fragment {
balloon.dismiss();
});
negativeButton.setOnClickListener(v1 -> {
- UsageStatistics.askAgainLater(UsageStatistics.ACTION_STREAM); // Type does not matter. Both are silenced.
+ UsageStatistics.doNotAskAgain(UsageStatistics.ACTION_STREAM); // Type does not matter. Both are silenced.
balloon.dismiss();
});
balloon.showAlignBottom(butAction1, 0, (int) (-12 * getResources().getDisplayMetrics().density));
@@ -287,7 +287,7 @@ public class ItemFragment extends Fragment {
if (item.getPubDate() != null) {
String pubDateStr = DateFormatter.formatAbbrev(getActivity(), item.getPubDate());
txtvPublished.setText(pubDateStr);
- txtvPublished.setContentDescription(DateFormatter.formatForAccessibility(getContext(), item.getPubDate()));
+ txtvPublished.setContentDescription(DateFormatter.formatForAccessibility(item.getPubDate()));
}
RequestOptions options = new RequestOptions()
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/OnlineSearchFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/OnlineSearchFragment.java
index f3080f655..2fda74296 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/OnlineSearchFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/OnlineSearchFragment.java
@@ -23,9 +23,9 @@ import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.activity.OnlineFeedViewActivity;
import de.danoeh.antennapod.adapter.itunes.ItunesAdapter;
-import de.danoeh.antennapod.discovery.PodcastSearchResult;
-import de.danoeh.antennapod.discovery.PodcastSearcher;
-import de.danoeh.antennapod.discovery.PodcastSearcherRegistry;
+import de.danoeh.antennapod.net.discovery.PodcastSearchResult;
+import de.danoeh.antennapod.net.discovery.PodcastSearcher;
+import de.danoeh.antennapod.net.discovery.PodcastSearcherRegistry;
import io.reactivex.disposables.Disposable;
import java.util.ArrayList;
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/QuickFeedDiscoveryFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/QuickFeedDiscoveryFragment.java
index 8bfcfd1ed..439f8a2cc 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/QuickFeedDiscoveryFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/QuickFeedDiscoveryFragment.java
@@ -17,6 +17,8 @@ import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.TextView;
+import de.danoeh.antennapod.net.discovery.ItunesTopListLoader;
+import de.danoeh.antennapod.net.discovery.PodcastSearchResult;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
@@ -26,8 +28,6 @@ import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.activity.OnlineFeedViewActivity;
import de.danoeh.antennapod.adapter.FeedDiscoverAdapter;
import de.danoeh.antennapod.event.DiscoveryDefaultUpdateEvent;
-import de.danoeh.antennapod.discovery.ItunesTopListLoader;
-import de.danoeh.antennapod.discovery.PodcastSearchResult;
import io.reactivex.disposables.Disposable;
import java.util.ArrayList;
@@ -119,13 +119,12 @@ public class QuickFeedDiscoveryFragment extends Fragment implements AdapterView.
String countryCode = prefs.getString(ItunesTopListLoader.PREF_KEY_COUNTRY_CODE,
Locale.getDefault().getCountry());
if (countryCode.equals(ItunesTopListLoader.DISCOVER_HIDE_FAKE_COUNTRY_CODE)) {
- errorTextView.setText(String.format(getResources().getString(R.string.discover_is_hidden),
- getResources().getString(R.string.discover_hide)));
+ errorTextView.setText(R.string.discover_is_hidden);
errorView.setVisibility(View.VISIBLE);
progressBar.setVisibility(View.GONE);
- discoverGridLayout.setVisibility(View.INVISIBLE);
- errorRetry.setVisibility(View.INVISIBLE);
- poweredByTextView.setVisibility(View.INVISIBLE);
+ discoverGridLayout.setVisibility(View.GONE);
+ errorRetry.setVisibility(View.GONE);
+ poweredByTextView.setVisibility(View.GONE);
return;
}
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/SearchFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/SearchFragment.java
index be23775ca..0142498a8 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/SearchFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/SearchFragment.java
@@ -343,8 +343,8 @@ public class SearchFragment extends Fragment {
return new Pair<>(Collections.emptyList(), Collections.emptyList());
}
long feed = getArguments().getLong(ARG_FEED);
- List<FeedItem> items = FeedSearcher.searchFeedItems(getContext(), query, feed);
- List<Feed> feeds = FeedSearcher.searchFeeds(getContext(), query);
+ List<FeedItem> items = FeedSearcher.searchFeedItems(query, feed);
+ List<Feed> feeds = FeedSearcher.searchFeeds(query);
return new Pair<>(items, feeds);
}
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/SubscriptionFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/SubscriptionFragment.java
index 9f6167c65..ec3240496 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/SubscriptionFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/SubscriptionFragment.java
@@ -29,6 +29,7 @@ import com.joanzapata.iconify.Iconify;
import com.leinardi.android.speeddial.SpeedDialView;
import de.danoeh.antennapod.dialog.TagSettingsDialog;
+import de.danoeh.antennapod.ui.statistics.StatisticsFragment;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
@@ -230,6 +231,9 @@ public class SubscriptionFragment extends Fragment
} 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;
}
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
index 992c925f3..5cd0f26ab 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/actions/FeedMultiSelectActionHandler.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/actions/FeedMultiSelectActionHandler.java
@@ -8,8 +8,6 @@ import androidx.core.util.Consumer;
import com.google.android.material.snackbar.Snackbar;
-import java.text.DecimalFormat;
-import java.text.DecimalFormatSymbols;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
@@ -66,9 +64,6 @@ public class FeedMultiSelectActionHandler {
preferenceSwitchDialog.openDialog();
}
- private static final DecimalFormat SPEED_FORMAT =
- new DecimalFormat("0.00", DecimalFormatSymbols.getInstance(Locale.US));
-
private void playbackSpeedPrefHandler() {
PlaybackSpeedFeedSettingDialogBinding viewBinding =
PlaybackSpeedFeedSettingDialogBinding.inflate(activity.getLayoutInflater());
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/GpodnetMainFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/GpodnetMainFragment.java
index 2ec30b8c3..fc2f1fa47 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/GpodnetMainFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/GpodnetMainFragment.java
@@ -19,8 +19,8 @@ import com.google.android.material.tabs.TabLayoutMediator;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MainActivity;
-import de.danoeh.antennapod.discovery.GpodnetPodcastSearcher;
import de.danoeh.antennapod.fragment.OnlineSearchFragment;
+import de.danoeh.antennapod.net.discovery.GpodnetPodcastSearcher;
/**
* Main navigation hub for gpodder.net podcast directory
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/TagFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/TagFragment.java
index 62e2e30d1..655cd6ed4 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/TagFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/TagFragment.java
@@ -15,8 +15,6 @@ import de.danoeh.antennapod.net.sync.gpoddernet.model.GpodnetTag;
* Use the newInstance method of this class to create a new TagFragment.
*/
public class TagFragment extends PodcastListFragment {
-
- private static final String TAG = "TagFragment";
private static final int PODCAST_COUNT = 50;
private GpodnetTag tag;
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
index 5156de432..075d01b8b 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/ImportExportPreferencesFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/ImportExportPreferencesFragment.java
@@ -5,8 +5,6 @@ import android.app.ProgressDialog;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
@@ -17,7 +15,9 @@ 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.StringRes;
import androidx.appcompat.app.AlertDialog;
+import androidx.core.app.ShareCompat;
import androidx.core.content.FileProvider;
import androidx.preference.PreferenceFragmentCompat;
import com.google.android.material.snackbar.Snackbar;
@@ -31,7 +31,6 @@ import de.danoeh.antennapod.core.export.ExportWriter;
import de.danoeh.antennapod.core.export.favorites.FavoritesWriter;
import de.danoeh.antennapod.core.export.html.HtmlWriter;
import de.danoeh.antennapod.core.export.opml.OpmlWriter;
-import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.storage.DatabaseExporter;
import io.reactivex.Completable;
import io.reactivex.Observable;
@@ -42,7 +41,6 @@ import io.reactivex.schedulers.Schedulers;
import java.io.File;
import java.text.SimpleDateFormat;
import java.util.Date;
-import java.util.List;
import java.util.Locale;
public class ImportExportPreferencesFragment extends PreferenceFragmentCompat {
@@ -104,15 +102,13 @@ public class ImportExportPreferencesFragment extends PreferenceFragmentCompat {
private void setupStorageScreen() {
findPreference(PREF_OPML_EXPORT).setOnPreferenceClickListener(
preference -> {
- openExportPathPicker(CONTENT_TYPE_OPML, dateStampFilename(DEFAULT_OPML_OUTPUT_NAME),
- chooseOpmlExportPathLauncher, new OpmlWriter());
+ openExportPathPicker(Export.OPML, chooseOpmlExportPathLauncher, new OpmlWriter());
return true;
}
);
findPreference(PREF_HTML_EXPORT).setOnPreferenceClickListener(
preference -> {
- openExportPathPicker(CONTENT_TYPE_HTML, dateStampFilename(DEFAULT_HTML_OUTPUT_NAME),
- chooseHtmlExportPathLauncher, new HtmlWriter());
+ openExportPathPicker(Export.HTML, chooseHtmlExportPathLauncher, new HtmlWriter());
return true;
});
findPreference(PREF_OPML_IMPORT).setOnPreferenceClickListener(
@@ -136,13 +132,12 @@ public class ImportExportPreferencesFragment extends PreferenceFragmentCompat {
});
findPreference(PREF_FAVORITE_EXPORT).setOnPreferenceClickListener(
preference -> {
- openExportPathPicker(CONTENT_TYPE_HTML, dateStampFilename(DEFAULT_FAVORITES_OUTPUT_NAME),
- chooseFavoritesExportPathLauncher, new FavoritesWriter());
+ openExportPathPicker(Export.FAVORITES, chooseFavoritesExportPathLauncher, new FavoritesWriter());
return true;
});
}
- private void exportWithWriter(ExportWriter exportWriter, final Uri uri) {
+ private void exportWithWriter(ExportWriter exportWriter, Uri uri, Export exportType) {
Context context = getActivity();
progressDialog.show();
if (uri == null) {
@@ -152,7 +147,7 @@ public class ImportExportPreferencesFragment extends PreferenceFragmentCompat {
.subscribe(output -> {
Uri fileUri = FileProvider.getUriForFile(context.getApplicationContext(),
context.getString(R.string.provider_authority), output);
- showExportSuccessDialog(output.toString(), fileUri);
+ showExportSuccessDialog(output.toString(), fileUri, exportType);
}, this::showExportErrorDialog, progressDialog::dismiss);
} else {
DocumentFileExportWorker worker = new DocumentFileExportWorker(exportWriter, context, uri);
@@ -160,7 +155,7 @@ public class ImportExportPreferencesFragment extends PreferenceFragmentCompat {
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(output ->
- showExportSuccessDialog(output.getUri().toString(), output.getUri()),
+ showExportSuccessDialog(output.getUri().toString(), output.getUri(), exportType),
this::showExportErrorDialog, progressDialog::dismiss);
}
}
@@ -196,25 +191,18 @@ public class ImportExportPreferencesFragment extends PreferenceFragmentCompat {
builder.show();
}
- private void showExportSuccessDialog(final String path, final Uri streamUri) {
+ private void showExportSuccessDialog(String path, Uri streamUri, Export exportType) {
final AlertDialog.Builder alert = new AlertDialog.Builder(getContext());
alert.setNeutralButton(android.R.string.ok, (dialog, which) -> dialog.dismiss());
alert.setTitle(R.string.export_success_title);
alert.setMessage(getContext().getString(R.string.export_success_sum, path));
alert.setPositiveButton(R.string.send_label, (dialog, which) -> {
- Intent sendIntent = new Intent(Intent.ACTION_SEND);
- sendIntent.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.opml_export_label));
- sendIntent.putExtra(Intent.EXTRA_STREAM, streamUri);
- sendIntent.setType("text/plain");
- sendIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
- Intent chooserIntent = Intent.createChooser(sendIntent, getString(R.string.send_label));
- List<ResolveInfo> resInfoList = getContext().getPackageManager()
- .queryIntentActivities(sendIntent, PackageManager.MATCH_DEFAULT_ONLY);
- for (ResolveInfo resolveInfo : resInfoList) {
- String packageName = resolveInfo.activityInfo.packageName;
- getContext().grantUriPermission(packageName, streamUri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
- }
- getContext().startActivity(chooserIntent);
+ new ShareCompat.IntentBuilder(getContext())
+ .setType(exportType.contentType)
+ .setSubject(getString(exportType.labelResId))
+ .addStream(streamUri)
+ .setChooserTitle(R.string.send_label)
+ .startChooser();
});
alert.create().show();
}
@@ -233,7 +221,7 @@ public class ImportExportPreferencesFragment extends PreferenceFragmentCompat {
return;
}
final Uri uri = result.getData().getData();
- exportWithWriter(new OpmlWriter(), uri);
+ exportWithWriter(new OpmlWriter(), uri, Export.OPML);
}
private void chooseHtmlExportPathResult(final ActivityResult result) {
@@ -241,7 +229,7 @@ public class ImportExportPreferencesFragment extends PreferenceFragmentCompat {
return;
}
final Uri uri = result.getData().getData();
- exportWithWriter(new HtmlWriter(), uri);
+ exportWithWriter(new HtmlWriter(), uri, Export.HTML);
}
private void chooseFavoritesExportPathResult(final ActivityResult result) {
@@ -249,7 +237,7 @@ public class ImportExportPreferencesFragment extends PreferenceFragmentCompat {
return;
}
final Uri uri = result.getData().getData();
- exportWithWriter(new FavoritesWriter(), uri);
+ exportWithWriter(new FavoritesWriter(), uri, Export.FAVORITES);
}
private void restoreDatabaseResult(final ActivityResult result) {
@@ -263,7 +251,6 @@ public class ImportExportPreferencesFragment extends PreferenceFragmentCompat {
.observeOn(AndroidSchedulers.mainThread())
.subscribe(() -> {
showDatabaseImportSuccessDialog();
- UserPreferences.unsetUsageCountingDate();
progressDialog.dismiss();
}, this::showExportErrorDialog);
}
@@ -291,11 +278,12 @@ public class ImportExportPreferencesFragment extends PreferenceFragmentCompat {
startActivity(intent);
}
- private void openExportPathPicker(String contentType, String title,
- final ActivityResultLauncher<Intent> result, ExportWriter writer) {
+ private void openExportPathPicker(Export exportType, ActivityResultLauncher<Intent> result, ExportWriter writer) {
+ String title = dateStampFilename(exportType.outputNameTemplate);
+
Intent intentPickAction = new Intent(Intent.ACTION_CREATE_DOCUMENT)
.addCategory(Intent.CATEGORY_OPENABLE)
- .setType(contentType)
+ .setType(exportType.contentType)
.putExtra(Intent.EXTRA_TITLE, title);
// Creates an implicit intent to launch a file manager which lets
@@ -309,7 +297,7 @@ public class ImportExportPreferencesFragment extends PreferenceFragmentCompat {
// If we are using a SDK lower than API 21 or the implicit intent failed
// fallback to the legacy export process
- exportWithWriter(writer, null);
+ exportWithWriter(writer, null, exportType);
}
private static class BackupDatabase extends ActivityResultContracts.CreateDocument {
@@ -321,4 +309,21 @@ public class ImportExportPreferencesFragment extends PreferenceFragmentCompat {
.setType("application/x-sqlite3");
}
}
+
+ 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
index 891d3737b..e2c5036df 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/MainPreferencesFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/MainPreferencesFragment.java
@@ -29,7 +29,6 @@ public class MainPreferencesFragment extends PreferenceFragmentCompat {
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 STATISTICS = "statistics";
private static final String PREF_ABOUT = "prefAbout";
private static final String PREF_NOTIFICATION = "notifications";
private static final String PREF_CONTRIBUTE = "prefContribute";
@@ -106,14 +105,6 @@ public class MainPreferencesFragment extends PreferenceFragmentCompat {
return true;
}
);
- findPreference(STATISTICS).setOnPreferenceClickListener(
- preference -> {
- getParentFragmentManager().beginTransaction()
- .replace(R.id.settingsContainer, new StatisticsFragment())
- .addToBackStack(getString(R.string.statistics_label)).commit();
- return true;
- }
- );
findPreference(PREF_DOCUMENTATION).setOnPreferenceClickListener(preference -> {
IntentUtils.openInBrowser(getContext(), "https://antennapod.org/documentation/");
return true;
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
index 7fa2ed4d1..15fdf5d97 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/PlaybackPreferencesFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/PlaybackPreferencesFragment.java
@@ -13,7 +13,6 @@ import de.danoeh.antennapod.activity.PreferenceActivity;
import de.danoeh.antennapod.event.UnreadItemsUpdateEvent;
import de.danoeh.antennapod.core.preferences.UsageStatistics;
import de.danoeh.antennapod.core.preferences.UserPreferences;
-import de.danoeh.antennapod.core.util.gui.PictureInPictureUtil;
import de.danoeh.antennapod.dialog.SkipPreferenceDialog;
import de.danoeh.antennapod.dialog.VariableSpeedDialog;
import java.util.Map;
@@ -54,15 +53,11 @@ public class PlaybackPreferencesFragment extends PreferenceFragmentCompat {
SkipPreferenceDialog.showSkipPreference(activity, SkipPreferenceDialog.SkipDirection.SKIP_FORWARD, null);
return true;
});
- if (!PictureInPictureUtil.supportsPictureInPicture(activity)) {
- ListPreference behaviour = findPreference(UserPreferences.PREF_VIDEO_BEHAVIOR);
- behaviour.setEntries(R.array.video_background_behavior_options_without_pip);
- behaviour.setEntryValues(R.array.video_background_behavior_values_without_pip);
- }
findPreference(PREF_PLAYBACK_PREFER_STREAMING).setOnPreferenceChangeListener((preference, newValue) -> {
// Update all visible lists to reflect new streaming action button
EventBus.getDefault().post(new UnreadItemsUpdateEvent());
- UsageStatistics.askAgainLater(UsageStatistics.ACTION_STREAM);
+ // User consciously decided whether to prefer the streaming button, disable suggestion to change that
+ UsageStatistics.doNotAskAgain(UsageStatistics.ACTION_STREAM);
return true;
});
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/PlaybackStatisticsFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/PlaybackStatisticsFragment.java
deleted file mode 100644
index ba6164212..000000000
--- a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/PlaybackStatisticsFragment.java
+++ /dev/null
@@ -1,197 +0,0 @@
-package de.danoeh.antennapod.fragment.preferences;
-
-import android.content.Context;
-import android.content.DialogInterface;
-import android.content.SharedPreferences;
-import android.os.Bundle;
-import android.util.Log;
-import android.view.LayoutInflater;
-import android.view.Menu;
-import android.view.MenuInflater;
-import android.view.MenuItem;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.ProgressBar;
-import android.widget.RadioButton;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.appcompat.app.AlertDialog;
-import androidx.fragment.app.Fragment;
-import androidx.recyclerview.widget.LinearLayoutManager;
-import androidx.recyclerview.widget.RecyclerView;
-
-import de.danoeh.antennapod.R;
-import de.danoeh.antennapod.adapter.PlaybackStatisticsListAdapter;
-import de.danoeh.antennapod.core.dialog.ConfirmationDialog;
-import de.danoeh.antennapod.core.preferences.UserPreferences;
-import de.danoeh.antennapod.core.storage.DBReader;
-import de.danoeh.antennapod.core.storage.DBWriter;
-import de.danoeh.antennapod.core.storage.StatisticsItem;
-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.util.Collections;
-import java.util.List;
-
-/**
- * Displays the 'playback statistics' screen
- */
-public class PlaybackStatisticsFragment extends Fragment {
- private static final String TAG = PlaybackStatisticsFragment.class.getSimpleName();
- private static final String PREF_NAME = "StatisticsActivityPrefs";
- private static final String PREF_COUNT_ALL = "countAll";
-
- private Disposable disposable;
- private RecyclerView feedStatisticsList;
- private ProgressBar progressBar;
- private PlaybackStatisticsListAdapter listAdapter;
- private boolean countAll = false;
- private SharedPreferences prefs;
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- prefs = getContext().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
- countAll = prefs.getBoolean(PREF_COUNT_ALL, false);
- setHasOptionsMenu(true);
- }
-
- @Nullable
- @Override
- public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
- @Nullable Bundle savedInstanceState) {
- View root = inflater.inflate(R.layout.statistics_activity, container, false);
- feedStatisticsList = root.findViewById(R.id.statistics_list);
- progressBar = root.findViewById(R.id.progressBar);
- listAdapter = new PlaybackStatisticsListAdapter(this);
- listAdapter.setCountAll(countAll);
- feedStatisticsList.setLayoutManager(new LinearLayoutManager(getContext()));
- feedStatisticsList.setAdapter(listAdapter);
- return root;
- }
-
- @Override
- public void onStart() {
- super.onStart();
- refreshStatistics();
- }
-
- @Override
- public void onDestroyView() {
- super.onDestroyView();
- if (disposable != null) {
- disposable.dispose();
- }
- }
-
- @Override
- public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
- inflater.inflate(R.menu.statistics, menu);
- menu.findItem(R.id.statistics_reset).setEnabled(!countAll);
- }
-
- @Override
- public boolean onOptionsItemSelected(MenuItem item) {
- if (item.getItemId() == R.id.statistics_mode) {
- selectStatisticsMode();
- return true;
- }
- if (item.getItemId() == R.id.statistics_reset) {
- confirmResetStatistics();
- return true;
- }
- return super.onOptionsItemSelected(item);
- }
-
- private void selectStatisticsMode() {
- View contentView = View.inflate(getContext(), R.layout.statistics_mode_select_dialog, null);
- AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
- builder.setView(contentView);
- builder.setTitle(R.string.statistics_mode);
-
- if (countAll) {
- ((RadioButton) contentView.findViewById(R.id.statistics_mode_count_all)).setChecked(true);
- } else {
- ((RadioButton) contentView.findViewById(R.id.statistics_mode_normal)).setChecked(true);
- }
-
- builder.setPositiveButton(android.R.string.ok, (dialog, which) -> {
- countAll = ((RadioButton) contentView.findViewById(R.id.statistics_mode_count_all)).isChecked();
- listAdapter.setCountAll(countAll);
- prefs.edit().putBoolean(PREF_COUNT_ALL, countAll).apply();
- refreshStatistics();
- getActivity().invalidateOptionsMenu();
- });
-
- builder.show();
- }
-
- private void confirmResetStatistics() {
- if (!countAll) {
- ConfirmationDialog conDialog = new ConfirmationDialog(
- getActivity(),
- R.string.statistics_reset_data,
- R.string.statistics_reset_data_msg) {
-
- @Override
- public void onConfirmButtonPressed(DialogInterface dialog) {
- dialog.dismiss();
- doResetStatistics();
- }
- };
- conDialog.createNewDialog().show();
- }
- }
-
- private void doResetStatistics() {
- progressBar.setVisibility(View.VISIBLE);
- feedStatisticsList.setVisibility(View.GONE);
- if (disposable != null) {
- disposable.dispose();
- }
-
- disposable = Completable.fromFuture(DBWriter.resetStatistics())
- .subscribeOn(Schedulers.io())
- .observeOn(AndroidSchedulers.mainThread())
- .subscribe(() -> {
- refreshStatistics();
- UserPreferences.resetUsageCountingDate();
- }, error -> Log.e(TAG, Log.getStackTraceString(error)));
- }
-
- private void refreshStatistics() {
- progressBar.setVisibility(View.VISIBLE);
- feedStatisticsList.setVisibility(View.GONE);
- loadStatistics();
- }
-
- private void loadStatistics() {
- if (disposable != null) {
- disposable.dispose();
- }
- disposable = Observable.fromCallable(this::fetchStatistics)
- .subscribeOn(Schedulers.io())
- .observeOn(AndroidSchedulers.mainThread())
- .subscribe(result -> {
- listAdapter.update(result);
- progressBar.setVisibility(View.GONE);
- feedStatisticsList.setVisibility(View.VISIBLE);
- }, error -> Log.e(TAG, Log.getStackTraceString(error)));
- }
-
- private List<StatisticsItem> fetchStatistics() {
- List<StatisticsItem> statisticsData = DBReader.getStatistics();
- if (countAll) {
- Collections.sort(statisticsData, (item1, item2) ->
- Long.compare(item2.timePlayedCountAll, item1.timePlayedCountAll));
- } else {
- Collections.sort(statisticsData, (item1, item2) ->
- Long.compare(item2.timePlayed, item1.timePlayed));
- }
- return statisticsData;
- }
-}
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/StatisticsFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/StatisticsFragment.java
deleted file mode 100644
index 2c72ab75b..000000000
--- a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/StatisticsFragment.java
+++ /dev/null
@@ -1,106 +0,0 @@
-package de.danoeh.antennapod.fragment.preferences;
-
-import android.os.Bundle;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-
-import androidx.annotation.NonNull;
-import androidx.appcompat.widget.Toolbar;
-import androidx.fragment.app.Fragment;
-import androidx.viewpager2.adapter.FragmentStateAdapter;
-import androidx.viewpager2.widget.ViewPager2;
-
-import com.google.android.material.tabs.TabLayout;
-import com.google.android.material.tabs.TabLayoutMediator;
-
-import de.danoeh.antennapod.R;
-import de.danoeh.antennapod.activity.PreferenceActivity;
-
-/**
- * Displays the 'statistics' screen
- */
-public class StatisticsFragment extends Fragment {
-
- public static final String TAG = "StatisticsFragment";
-
- private static final int POS_LISTENED_HOURS = 0;
- private static final int POS_SPACE_TAKEN = 1;
- private static final int TOTAL_COUNT = 2;
-
-
- private TabLayout tabLayout;
- private ViewPager2 viewPager;
- private Toolbar toolbar;
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- }
-
- public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
- Bundle savedInstanceState) {
- super.onCreateView(inflater, container, savedInstanceState);
- setHasOptionsMenu(true);
-
- View rootView = inflater.inflate(R.layout.pager_fragment, container, false);
- viewPager = rootView.findViewById(R.id.viewpager);
- toolbar = rootView.findViewById(R.id.toolbar);
- viewPager.setAdapter(new StatisticsPagerAdapter(this));
- // Give the TabLayout the ViewPager
- tabLayout = rootView.findViewById(R.id.sliding_tabs);
- new TabLayoutMediator(tabLayout, viewPager, (tab, position) -> {
- switch (position) {
- case POS_LISTENED_HOURS:
- tab.setText(R.string.playback_statistics_label);
- break;
- case POS_SPACE_TAKEN:
- tab.setText(R.string.download_statistics_label);
- break;
- default:
- break;
- }
- }).attach();
-
- if (getActivity().getClass() == PreferenceActivity.class) {
- rootView.findViewById(R.id.toolbar).setVisibility(View.GONE);
- } else {
- toolbar.setTitle(getString(R.string.statistics_label));
- toolbar.setNavigationOnClickListener(v -> getParentFragmentManager().popBackStack());
- }
-
- return rootView;
- }
-
- @Override
- public void onStart() {
- super.onStart();
- if (getActivity().getClass() == PreferenceActivity.class) {
- ((PreferenceActivity) getActivity()).getSupportActionBar().setTitle(R.string.statistics_label);
- }
- }
-
- public static class StatisticsPagerAdapter extends FragmentStateAdapter {
-
- StatisticsPagerAdapter(@NonNull Fragment fragment) {
- super(fragment);
- }
-
- @NonNull
- @Override
- public Fragment createFragment(int position) {
- switch (position) {
- case POS_LISTENED_HOURS:
- return new PlaybackStatisticsFragment();
- default:
- case POS_SPACE_TAKEN:
- return new DownloadStatisticsFragment();
- }
- }
-
- @Override
- public int getItemCount() {
- return TOTAL_COUNT;
- }
- }
-}
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/StoragePreferencesFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/StoragePreferencesFragment.java
index e5617b8ea..00ff9fb64 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/StoragePreferencesFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/StoragePreferencesFragment.java
@@ -10,7 +10,6 @@ import de.danoeh.antennapod.dialog.ChooseDataFolderDialog;
import java.io.File;
public class StoragePreferencesFragment extends PreferenceFragmentCompat {
- private static final String TAG = "StoragePrefFragment";
private static final String PREF_CHOOSE_DATA_DIR = "prefChooseDataDir";
private static final String PREF_IMPORT_EXPORT = "prefImportExport";
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
index 3d9709f74..19099a380 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/SwipePreferencesFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/SwipePreferencesFragment.java
@@ -11,8 +11,6 @@ import de.danoeh.antennapod.fragment.QueueFragment;
public class SwipePreferencesFragment extends PreferenceFragmentCompat {
private static final String PREF_SWIPE_FEED = "prefSwipeFeed";
private static final String PREF_SWIPE_QUEUE = "prefSwipeQueue";
- //private static final String PREF_SWIPE_INBOX = "prefSwipeInbox";
- //private static final String PREF_SWIPE_EPISODES = "prefSwipeEpisodes";
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/about/ContributorsPagerFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/about/ContributorsPagerFragment.java
index 20cef1313..caa8031ae 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/about/ContributorsPagerFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/about/ContributorsPagerFragment.java
@@ -17,19 +17,12 @@ import de.danoeh.antennapod.activity.PreferenceActivity;
* Displays the 'about->Contributors' pager screen.
*/
public class ContributorsPagerFragment extends Fragment {
-
- public static final String TAG = "StatisticsFragment";
-
private static final int POS_DEVELOPERS = 0;
private static final int POS_TRANSLATORS = 1;
private static final int POS_SPECIAL_THANKS = 2;
private static final int TOTAL_COUNT = 3;
@Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- }
-
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
super.onCreateView(inflater, container, savedInstanceState);
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/synchronization/GpodderAuthenticationFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/synchronization/GpodderAuthenticationFragment.java
index 9dfe6840c..358985cea 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/synchronization/GpodderAuthenticationFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/synchronization/GpodderAuthenticationFragment.java
@@ -250,18 +250,6 @@ public class GpodderAuthenticationFragment extends DialogFragment {
return false;
}
- private GpodnetDevice findDevice(String id) {
- if (devices == null) {
- return null;
- }
- for (GpodnetDevice device : devices) {
- if (device.getId().equals(id)) {
- return device;
- }
- }
- return null;
- }
-
private void setupFinishView(View view) {
final Button sync = view.findViewById(R.id.butSyncNow);
diff --git a/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedMenuHandler.java b/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedMenuHandler.java
index f59be601c..bfe269caa 100644
--- a/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedMenuHandler.java
+++ b/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedMenuHandler.java
@@ -5,23 +5,17 @@ import android.content.DialogInterface;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
-
import androidx.annotation.NonNull;
-
-import org.apache.commons.lang3.StringUtils;
-
-import java.util.Set;
-
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.core.dialog.ConfirmationDialog;
-import de.danoeh.antennapod.model.feed.Feed;
import de.danoeh.antennapod.core.storage.DBTasks;
import de.danoeh.antennapod.core.storage.DBWriter;
import de.danoeh.antennapod.core.util.IntentUtils;
import de.danoeh.antennapod.core.util.ShareUtils;
-import de.danoeh.antennapod.model.feed.SortOrder;
-import de.danoeh.antennapod.dialog.FilterDialog;
import de.danoeh.antennapod.dialog.IntraFeedSortDialog;
+import de.danoeh.antennapod.model.feed.Feed;
+import de.danoeh.antennapod.model.feed.SortOrder;
+import org.apache.commons.lang3.StringUtils;
/**
* Handles interactions with the FeedItemMenu.
@@ -42,7 +36,6 @@ public class FeedMenuHandler {
menu.findItem(R.id.refresh_complete_item).setVisible(selectedFeed.isPaged());
if (StringUtils.isBlank(selectedFeed.getLink())) {
menu.findItem(R.id.visit_website_item).setVisible(false);
- menu.findItem(R.id.share_link_item).setVisible(false);
}
if (selectedFeed.isLocalFeed()) {
// hide complete submenu "Share..." as both sub menu items are not visible
@@ -63,8 +56,6 @@ public class FeedMenuHandler {
DBTasks.forceRefreshCompleteFeed(context, selectedFeed);
} else if (itemId == R.id.sort_items) {
showSortDialog(context, selectedFeed);
- } else if (itemId == R.id.filter_items) {
- showFilterDialog(context, selectedFeed);
} else if (itemId == R.id.mark_all_read_item) {
ConfirmationDialog conDialog = new ConfirmationDialog(context,
R.string.mark_all_read_label,
@@ -80,29 +71,14 @@ public class FeedMenuHandler {
conDialog.createNewDialog().show();
} else if (itemId == R.id.visit_website_item) {
IntentUtils.openInBrowser(context, selectedFeed.getLink());
- } else if (itemId == R.id.share_link_item) {
- ShareUtils.shareFeedlink(context, selectedFeed);
- } else if (itemId == R.id.share_download_url_item) {
- ShareUtils.shareFeedDownloadLink(context, selectedFeed);
+ } else if (itemId == R.id.share_item) {
+ ShareUtils.shareFeedLink(context, selectedFeed);
} else {
return false;
}
return true;
}
- private static void showFilterDialog(Context context, Feed selectedFeed) {
- FilterDialog filterDialog = new FilterDialog(context, selectedFeed.getItemFilter()) {
- @Override
- protected void updateFilter(Set<String> filterValues) {
- selectedFeed.setItemFilter(filterValues.toArray(new String[0]));
- DBWriter.setFeedItemsFilter(selectedFeed.getId(), filterValues);
- }
- };
-
- filterDialog.openDialog();
- }
-
-
private static void showSortDialog(Context context, Feed selectedFeed) {
IntraFeedSortDialog sortDialog = new IntraFeedSortDialog(context, selectedFeed.getSortOrder(), selectedFeed.isLocalFeed()) {
@Override
diff --git a/app/src/main/java/de/danoeh/antennapod/preferences/PreferenceUpgrader.java b/app/src/main/java/de/danoeh/antennapod/preferences/PreferenceUpgrader.java
index af35bbac9..a2c5ca3ff 100644
--- a/app/src/main/java/de/danoeh/antennapod/preferences/PreferenceUpgrader.java
+++ b/app/src/main/java/de/danoeh/antennapod/preferences/PreferenceUpgrader.java
@@ -39,9 +39,6 @@ public class PreferenceUpgrader {
private static void upgrade(int oldVersion, Context context) {
if (oldVersion == -1) {
//New installation
- if (UserPreferences.getUsageCountingDateMillis() < 0) {
- UserPreferences.resetUsageCountingDate();
- }
return;
}
if (oldVersion < 1070196) {
@@ -93,9 +90,6 @@ public class PreferenceUpgrader {
UserPreferences.setEnqueueLocation(enqueueLocation);
}
}
- if (oldVersion < 1080100) {
- prefs.edit().putString(UserPreferences.PREF_VIDEO_BEHAVIOR, "pip").apply();
- }
if (oldVersion < 2010300) {
// Migrate hardware button preferences
if (prefs.getBoolean("prefHardwareForwardButtonSkips", false)) {
diff --git a/app/src/main/java/de/danoeh/antennapod/view/TimePicker.java b/app/src/main/java/de/danoeh/antennapod/view/TimePicker.java
new file mode 100644
index 000000000..191f72d2e
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/view/TimePicker.java
@@ -0,0 +1,31 @@
+package de.danoeh.antennapod.view;
+
+import android.content.Context;
+import android.util.AttributeSet;
+
+/**
+ * Samsung's Android 6.0.1 has a bug that crashes the app when inflating a time picker.
+ * This class serves as a workaround for affected devices.
+ */
+public class TimePicker extends android.widget.TimePicker {
+ public TimePicker(Context context) {
+ super(context);
+ }
+
+ public TimePicker(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public TimePicker(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ @Override
+ public void onRtlPropertiesChanged(int layoutDirection) {
+ try {
+ super.onRtlPropertiesChanged(layoutDirection);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+}
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
index d231e522f..c1ab3a7a6 100644
--- a/app/src/main/java/de/danoeh/antennapod/view/viewholder/EpisodeItemViewHolder.java
+++ b/app/src/main/java/de/danoeh/antennapod/view/viewholder/EpisodeItemViewHolder.java
@@ -104,7 +104,7 @@ public class EpisodeItemViewHolder extends RecyclerView.ViewHolder {
title.setText(item.getTitle());
leftPadding.setContentDescription(item.getTitle());
pubDate.setText(DateFormatter.formatAbbrev(activity, item.getPubDate()));
- pubDate.setContentDescription(DateFormatter.formatForAccessibility(activity, item.getPubDate()));
+ pubDate.setContentDescription(DateFormatter.formatForAccessibility(item.getPubDate()));
isNew.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);
diff --git a/app/src/main/res/layout-sw720dp/main.xml b/app/src/main/res/layout-sw720dp/main.xml
index fe5a86d24..e00406464 100644
--- a/app/src/main/res/layout-sw720dp/main.xml
+++ b/app/src/main/res/layout-sw720dp/main.xml
@@ -8,7 +8,7 @@
android:orientation="horizontal"
tools:viewBindingIgnore="true">
- <FrameLayout
+ <androidx.fragment.app.FragmentContainerView
android:id="@+id/navDrawerFragment"
android:layout_width="300dp"
android:layout_height="match_parent"
@@ -25,7 +25,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
- <FrameLayout
+ <androidx.fragment.app.FragmentContainerView
android:id="@+id/main_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
@@ -33,7 +33,7 @@
android:foreground="?android:windowContentOverlay"
tools:background="@android:color/holo_red_dark" />
- <FrameLayout
+ <androidx.fragment.app.FragmentContainerView
android:id="@+id/audioplayerFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
diff --git a/app/src/main/res/layout/activity_widget_config.xml b/app/src/main/res/layout/activity_widget_config.xml
index 8c540fcc6..325b5cec5 100644
--- a/app/src/main/res/layout/activity_widget_config.xml
+++ b/app/src/main/res/layout/activity_widget_config.xml
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
@@ -16,91 +17,108 @@
android:id="@+id/widget_config_background"
android:layout_width="match_parent"
android:layout_height="match_parent"
- app:srcCompat="@drawable/teaser"
- android:scaleType="centerCrop" />
+ android:scaleType="centerCrop"
+ app:srcCompat="@drawable/teaser" />
<include
android:id="@+id/widget_config_preview"
- layout="@layout/player_widget"
android:layout_width="match_parent"
android:layout_height="96dp"
android:layout_gravity="center"
- android:layout_margin="16dp" />
+ android:layout_margin="16dp"
+ layout="@layout/player_widget" />
+
</FrameLayout>
- <LinearLayout
+ <androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:orientation="vertical"
- android:padding="16dp">
+ android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:gravity="center_vertical"
- android:orientation="horizontal">
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:padding="16dp">
- <TextView
- android:id="@+id/textView"
- android:layout_width="wrap_content"
+ <LinearLayout
+ android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:text="@string/widget_opacity"
- android:textSize="16sp"
- android:textColor="?android:attr/textColorPrimary" />
+ android:gravity="center_vertical"
+ android:orientation="horizontal">
- <TextView
- android:id="@+id/widget_opacity_textView"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_weight="1"
- android:gravity="end"
- android:text="100%"
- android:textSize="16sp"
- android:textColor="?android:attr/textColorSecondary" />
+ <TextView
+ android:id="@+id/textView"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/widget_opacity"
+ android:textSize="16sp"
+ android:textColor="?android:attr/textColorPrimary" />
- </LinearLayout>
+ <TextView
+ android:id="@+id/widget_opacity_textView"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:gravity="end"
+ android:text="100%"
+ android:textSize="16sp"
+ android:textColor="?android:attr/textColorSecondary" />
- <SeekBar
- android:id="@+id/widget_opacity_seekBar"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_marginTop="16dp"
- android:layout_marginBottom="16dp"
- android:max="100"
- android:progress="100" />
+ </LinearLayout>
- <LinearLayout
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:orientation="horizontal">
-
- <CheckBox
- android:id="@+id/ckRewind"
- android:layout_width="wrap_content"
+ <SeekBar
+ android:id="@+id/widget_opacity_seekBar"
+ android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:layout_weight="1"
- android:text="Rewind" />
+ android:layout_marginTop="16dp"
+ android:layout_marginBottom="16dp"
+ android:max="100"
+ android:progress="100" />
- <CheckBox
- android:id="@+id/ckFastForward"
- android:layout_width="wrap_content"
+ <LinearLayout
+ android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:layout_weight="1"
- android:text="Forward" />
+ android:orientation="vertical">
+
+ <CheckBox
+ android:id="@+id/ckPlaybackSpeed"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="@string/playback_speed" />
- <CheckBox
- android:id="@+id/ckSkip"
- android:layout_width="wrap_content"
+ <CheckBox
+ android:id="@+id/ckRewind"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="@string/rewind_label" />
+
+ <CheckBox
+ android:id="@+id/ckFastForward"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="@string/fast_forward_label" />
+
+ <CheckBox
+ android:id="@+id/ckSkip"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="@string/skip_episode_label" />
+
+ </LinearLayout>
+
+ <Button
+ android:id="@+id/butConfirm"
+ android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:layout_weight="1"
- android:text="Skip" />
+ android:layout_gravity="center"
+ android:text="@string/widget_create_button" />
+
</LinearLayout>
- <Button
- android:id="@+id/butConfirm"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_gravity="center"
- android:text="@string/widget_create_button" />
- </LinearLayout>
+
+ </androidx.core.widget.NestedScrollView>
</LinearLayout>
diff --git a/app/src/main/res/layout/audio_controls.xml b/app/src/main/res/layout/audio_controls.xml
index 0bfa4f521..dc48006bb 100644
--- a/app/src/main/res/layout/audio_controls.xml
+++ b/app/src/main/res/layout/audio_controls.xml
@@ -19,31 +19,6 @@
android:visibility="gone"
android:layout_marginBottom="8dp" />
- <LinearLayout
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:orientation="horizontal">
-
- <TextView
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_weight="1"
- android:text="@string/playback_speed"
- style="@style/AntennaPod.TextView.ListItemPrimaryTitle" />
-
- <TextView
- android:id="@+id/txtvPlaybackSpeed"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="1.00x" />
-
- </LinearLayout>
-
- <de.danoeh.antennapod.view.PlaybackSpeedSeekBar
- android:id="@+id/speed_seek_bar"
- android:layout_width="match_parent"
- android:layout_height="wrap_content" />
-
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
diff --git a/app/src/main/res/layout/audioplayer_fragment.xml b/app/src/main/res/layout/audioplayer_fragment.xml
index 7efbd23c8..4e4ab389c 100644
--- a/app/src/main/res/layout/audioplayer_fragment.xml
+++ b/app/src/main/res/layout/audioplayer_fragment.xml
@@ -1,273 +1,274 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
- xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:tools="http://schemas.android.com/tools"
- xmlns:app="http://schemas.android.com/apk/res-auto"
- android:layout_width="match_parent"
- android:layout_height="match_parent">
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
<androidx.appcompat.widget.Toolbar
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:minHeight="?attr/actionBarSize"
- android:theme="?attr/actionBarTheme"
- android:layout_alignParentTop="true"
- app:navigationContentDescription="@string/toolbar_back_button_content_description"
- app:navigationIcon="?homeAsUpIndicator"
- android:id="@+id/toolbar"/>
+ android:id="@+id/toolbar"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentTop="true"
+ android:minHeight="?attr/actionBarSize"
+ android:theme="?attr/actionBarTheme"
+ app:navigationContentDescription="@string/toolbar_back_button_content_description"
+ app:navigationIcon="?homeAsUpIndicator" />
- <FrameLayout
- android:id="@+id/playerFragment"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_alignParentTop="true"
- android:background="?attr/background_elevated"
- tools:layout_height="@dimen/external_player_height"
- android:elevation="8dp"/>
+ <androidx.fragment.app.FragmentContainerView
+ android:id="@+id/playerFragment"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentTop="true"
+ android:background="?attr/background_elevated"
+ android:elevation="8dp"
+ tools:layout_height="@dimen/external_player_height" />
<androidx.viewpager2.widget.ViewPager2
- android:id="@+id/pager"
- android:layout_width="match_parent"
- android:layout_height="0dp"
- android:layout_above="@id/playtime_layout"
- android:layout_below="@id/toolbar"
- android:layout_marginBottom="12dp"
- android:foreground="?android:windowContentOverlay"
- android:orientation="vertical" />
+ android:id="@+id/pager"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_above="@id/playtime_layout"
+ android:layout_below="@id/toolbar"
+ android:layout_marginBottom="12dp"
+ android:foreground="?android:windowContentOverlay"
+ android:orientation="vertical" />
<ImageView
- android:layout_width="match_parent"
- android:layout_height="8dp"
- android:layout_alignBottom="@id/pager"
- app:srcCompat="@drawable/bg_gradient"
- app:tint="?android:attr/windowBackground"
- android:importantForAccessibility="no"/>
+ android:layout_width="match_parent"
+ android:layout_height="8dp"
+ android:layout_alignBottom="@id/pager"
+ android:importantForAccessibility="no"
+ app:srcCompat="@drawable/bg_gradient"
+ app:tint="?android:attr/windowBackground" />
<androidx.cardview.widget.CardView
- android:id="@+id/cardViewSeek"
- android:alpha="0"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginLeft="16dp"
- android:layout_marginRight="16dp"
- android:layout_alignBottom="@+id/pager"
- android:layout_centerHorizontal="true"
- android:layout_marginBottom="12dp"
- app:cardCornerRadius="8dp"
- app:cardBackgroundColor="?attr/seek_background"
- app:cardElevation="0dp"
- tools:alpha="1">
+ android:id="@+id/cardViewSeek"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignBottom="@+id/pager"
+ android:layout_centerHorizontal="true"
+ android:layout_marginLeft="16dp"
+ android:layout_marginRight="16dp"
+ android:layout_marginBottom="12dp"
+ android:alpha="0"
+ app:cardBackgroundColor="?attr/seek_background"
+ app:cardCornerRadius="8dp"
+ app:cardElevation="0dp"
+ tools:alpha="1">
<TextView
- android:id="@+id/txtvSeek"
- android:gravity="center"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:paddingLeft="24dp"
- android:paddingTop="4dp"
- android:paddingRight="24dp"
- android:paddingBottom="4dp"
- android:textColor="@color/white"
- android:textSize="16sp"
- tools:text="1:06:29" />
+ android:id="@+id/txtvSeek"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ android:paddingLeft="24dp"
+ android:paddingTop="4dp"
+ android:paddingRight="24dp"
+ android:paddingBottom="4dp"
+ android:textColor="@color/white"
+ android:textSize="16sp"
+ tools:text="1:06:29" />
</androidx.cardview.widget.CardView>
<LinearLayout
- android:id="@+id/playtime_layout"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_alignParentBottom="true"
- android:layoutDirection="ltr"
- android:orientation="vertical">
+ android:id="@+id/playtime_layout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentBottom="true"
+ android:layoutDirection="ltr"
+ android:orientation="vertical">
<de.danoeh.antennapod.view.ChapterSeekBar
- android:id="@+id/sbPosition"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:max="500"
- tools:progress="100"
- android:layout_marginLeft="8dp"
- android:layout_marginRight="8dp"
- android:clickable="true"/>
+ android:id="@+id/sbPosition"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="8dp"
+ android:layout_marginRight="8dp"
+ android:clickable="true"
+ android:max="500"
+ tools:progress="100" />
<RelativeLayout
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:paddingLeft="8dp"
- android:paddingRight="8dp"
- android:layout_marginBottom="4dp"
- android:layout_marginTop="4dp">
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="4dp"
+ android:layout_marginBottom="4dp"
+ android:paddingLeft="8dp"
+ android:paddingRight="8dp">
<TextView
- android:id="@+id/txtvPosition"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_alignParentLeft="true"
- android:layout_alignParentStart="true"
- android:layout_marginLeft="16dp"
- android:layout_marginStart="16dp"
- android:text="@string/position_default_label"
- android:textColor="?android:attr/textColorSecondary"
- android:textSize="@dimen/text_size_micro"/>
+ android:id="@+id/txtvPosition"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentStart="true"
+ android:layout_alignParentLeft="true"
+ android:layout_marginStart="16dp"
+ android:layout_marginLeft="16dp"
+ android:text="@string/position_default_label"
+ android:textColor="?android:attr/textColorSecondary"
+ android:textSize="@dimen/text_size_micro" />
<TextView
- android:id="@+id/txtvLength"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_alignParentRight="true"
- android:layout_alignParentEnd="true"
- android:layout_marginRight="16dp"
- android:layout_marginEnd="16dp"
- android:text="@string/position_default_label"
- android:textColor="?android:attr/textColorSecondary"
- android:textSize="@dimen/text_size_micro"
- android:background="?android:attr/selectableItemBackground"/>
+ android:id="@+id/txtvLength"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentEnd="true"
+ android:layout_alignParentRight="true"
+ android:layout_marginEnd="16dp"
+ android:layout_marginRight="16dp"
+ android:background="?android:attr/selectableItemBackground"
+ android:text="@string/position_default_label"
+ android:textColor="?android:attr/textColorSecondary"
+ android:textSize="@dimen/text_size_micro" />
</RelativeLayout>
<RelativeLayout
- android:id="@+id/player_control"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_marginBottom="24dp">
+ android:id="@+id/player_control"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="24dp">
<de.danoeh.antennapod.view.PlayButton
- android:id="@+id/butPlay"
- android:layout_width="@dimen/audioplayer_playercontrols_length_big"
- android:layout_height="@dimen/audioplayer_playercontrols_length_big"
- android:layout_marginLeft="@dimen/audioplayer_playercontrols_margin"
- android:layout_marginStart="@dimen/audioplayer_playercontrols_margin"
- android:layout_marginRight="@dimen/audioplayer_playercontrols_margin"
- android:layout_marginEnd="@dimen/audioplayer_playercontrols_margin"
- android:padding="8dp"
- android:layout_centerHorizontal="true"
- android:layout_centerVertical="true"
- android:background="?attr/selectableItemBackgroundBorderless"
- android:contentDescription="@string/pause_label"
- app:srcCompat="@drawable/ic_play_48dp"
- android:scaleType="fitCenter"
- tools:srcCompat="@drawable/ic_play_48dp"/>
+ android:id="@+id/butPlay"
+ android:layout_width="@dimen/audioplayer_playercontrols_length_big"
+ android:layout_height="@dimen/audioplayer_playercontrols_length_big"
+ android:layout_centerHorizontal="true"
+ android:layout_centerVertical="true"
+ android:layout_marginStart="@dimen/audioplayer_playercontrols_margin"
+ android:layout_marginLeft="@dimen/audioplayer_playercontrols_margin"
+ android:layout_marginEnd="@dimen/audioplayer_playercontrols_margin"
+ android:layout_marginRight="@dimen/audioplayer_playercontrols_margin"
+ android:background="?attr/selectableItemBackgroundBorderless"
+ android:contentDescription="@string/pause_label"
+ android:padding="8dp"
+ android:scaleType="fitCenter"
+ app:srcCompat="@drawable/ic_play_48dp"
+ tools:srcCompat="@drawable/ic_play_48dp" />
<de.danoeh.antennapod.ui.common.CircularProgressBar
- android:layout_width="@dimen/audioplayer_playercontrols_length_big"
- android:layout_height="@dimen/audioplayer_playercontrols_length_big"
- android:layout_marginLeft="16dp"
- android:layout_marginStart="16dp"
- android:layout_marginRight="16dp"
- android:layout_marginEnd="16dp"
- android:layout_centerHorizontal="true"
- android:layout_centerVertical="true"
- app:foregroundColor="?attr/action_icon_color"/>
+ android:layout_width="@dimen/audioplayer_playercontrols_length_big"
+ android:layout_height="@dimen/audioplayer_playercontrols_length_big"
+ android:layout_centerHorizontal="true"
+ android:layout_centerVertical="true"
+ android:layout_marginStart="16dp"
+ android:layout_marginLeft="16dp"
+ android:layout_marginEnd="16dp"
+ android:layout_marginRight="16dp"
+ app:foregroundColor="?attr/action_icon_color" />
<ProgressBar
- style="?android:attr/progressBarStyle"
- android:layout_width="@dimen/audioplayer_playercontrols_length_big"
- android:layout_height="@dimen/audioplayer_playercontrols_length_big"
- android:layout_centerHorizontal="true"
- android:layout_centerVertical="true"
- android:id="@+id/progLoading"
- android:visibility="gone"/>
+ android:id="@+id/progLoading"
+ android:layout_width="@dimen/audioplayer_playercontrols_length_big"
+ android:layout_height="@dimen/audioplayer_playercontrols_length_big"
+ android:layout_centerHorizontal="true"
+ android:layout_centerVertical="true"
+ android:visibility="gone"
+ style="?android:attr/progressBarStyle" />
<ImageButton
- android:id="@+id/butRev"
- android:layout_width="@dimen/audioplayer_playercontrols_length"
- android:layout_height="@dimen/audioplayer_playercontrols_length"
- android:layout_toLeftOf="@id/butPlay"
- android:layout_toStartOf="@id/butPlay"
- android:layout_marginLeft="@dimen/audioplayer_playercontrols_margin"
- android:layout_marginStart="@dimen/audioplayer_playercontrols_margin"
- android:layout_centerVertical="true"
- android:background="?attr/selectableItemBackgroundBorderless"
- android:contentDescription="@string/rewind_label"
- app:srcCompat="@drawable/ic_fast_rewind"
- android:scaleType="fitCenter"
- tools:srcCompat="@drawable/ic_fast_rewind"/>
+ android:id="@+id/butRev"
+ android:layout_width="@dimen/audioplayer_playercontrols_length"
+ android:layout_height="@dimen/audioplayer_playercontrols_length"
+ android:layout_centerVertical="true"
+ android:layout_marginStart="@dimen/audioplayer_playercontrols_margin"
+ android:layout_marginLeft="@dimen/audioplayer_playercontrols_margin"
+ android:layout_toStartOf="@id/butPlay"
+ android:layout_toLeftOf="@id/butPlay"
+ android:background="?attr/selectableItemBackgroundBorderless"
+ android:contentDescription="@string/rewind_label"
+ android:scaleType="fitCenter"
+ app:srcCompat="@drawable/ic_fast_rewind"
+ tools:srcCompat="@drawable/ic_fast_rewind" />
<TextView
- android:id="@+id/txtvRev"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_below="@id/butRev"
- android:layout_alignLeft="@id/butRev"
- android:layout_alignStart="@id/butRev"
- android:layout_alignRight="@id/butRev"
- android:layout_alignEnd="@id/butRev"
- android:gravity="center"
- android:text="30"
- android:textSize="12sp"
- android:textColor="?android:attr/textColorSecondary"
- android:clickable="false"/>
+ android:id="@+id/txtvRev"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/butRev"
+ android:layout_alignStart="@id/butRev"
+ android:layout_alignLeft="@id/butRev"
+ android:layout_alignEnd="@id/butRev"
+ android:layout_alignRight="@id/butRev"
+ android:clickable="false"
+ android:gravity="center"
+ android:text="30"
+ android:textColor="?android:attr/textColorSecondary"
+ android:textSize="12sp" />
<de.danoeh.antennapod.ui.common.PlaybackSpeedIndicatorView
- android:id="@+id/butPlaybackSpeed"
- android:layout_width="@dimen/audioplayer_playercontrols_length"
- android:layout_height="@dimen/audioplayer_playercontrols_length"
- android:layout_toLeftOf="@id/butRev"
- android:layout_toStartOf="@id/butRev"
- android:layout_centerVertical="true"
- android:background="?attr/selectableItemBackgroundBorderless"
- android:contentDescription="@string/playback_speed"
- tools:srcCompat="@drawable/ic_playback_speed"
- app:foregroundColor="?attr/action_icon_color"/>
+ android:id="@+id/butPlaybackSpeed"
+ android:layout_width="@dimen/audioplayer_playercontrols_length"
+ android:layout_height="@dimen/audioplayer_playercontrols_length"
+ android:layout_centerVertical="true"
+ android:layout_toStartOf="@id/butRev"
+ android:layout_toLeftOf="@id/butRev"
+ android:background="?attr/selectableItemBackgroundBorderless"
+ android:contentDescription="@string/playback_speed"
+ app:foregroundColor="?attr/action_icon_color"
+ tools:srcCompat="@drawable/ic_playback_speed" />
<TextView
- android:id="@+id/txtvPlaybackSpeed"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_below="@id/butPlaybackSpeed"
- android:layout_alignLeft="@id/butPlaybackSpeed"
- android:layout_alignStart="@id/butPlaybackSpeed"
- android:layout_alignRight="@id/butPlaybackSpeed"
- android:layout_alignEnd="@id/butPlaybackSpeed"
- android:gravity="center"
- android:text="1.00"
- android:textSize="12sp"
- android:textColor="?android:attr/textColorSecondary"
- android:clickable="false"/>
+ android:id="@+id/txtvPlaybackSpeed"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/butPlaybackSpeed"
+ android:layout_alignStart="@id/butPlaybackSpeed"
+ android:layout_alignLeft="@id/butPlaybackSpeed"
+ android:layout_alignEnd="@id/butPlaybackSpeed"
+ android:layout_alignRight="@id/butPlaybackSpeed"
+ android:clickable="false"
+ android:gravity="center"
+ android:text="1.00"
+ android:textColor="?android:attr/textColorSecondary"
+ android:textSize="12sp" />
<ImageButton
- android:id="@+id/butFF"
- android:layout_width="@dimen/audioplayer_playercontrols_length"
- android:layout_height="@dimen/audioplayer_playercontrols_length"
- android:layout_toRightOf="@id/butPlay"
- android:layout_toEndOf="@id/butPlay"
- android:layout_marginRight="@dimen/audioplayer_playercontrols_margin"
- android:layout_marginEnd="@dimen/audioplayer_playercontrols_margin"
- android:layout_centerVertical="true"
- android:background="?attr/selectableItemBackgroundBorderless"
- android:contentDescription="@string/fast_forward_label"
- app:srcCompat="@drawable/ic_fast_forward"
- android:scaleType="fitCenter"
- tools:srcCompat="@drawable/ic_fast_forward"/>
+ android:id="@+id/butFF"
+ android:layout_width="@dimen/audioplayer_playercontrols_length"
+ android:layout_height="@dimen/audioplayer_playercontrols_length"
+ android:layout_centerVertical="true"
+ android:layout_marginEnd="@dimen/audioplayer_playercontrols_margin"
+ android:layout_marginRight="@dimen/audioplayer_playercontrols_margin"
+ android:layout_toEndOf="@id/butPlay"
+ android:layout_toRightOf="@id/butPlay"
+ android:background="?attr/selectableItemBackgroundBorderless"
+ android:contentDescription="@string/fast_forward_label"
+ android:scaleType="fitCenter"
+ app:srcCompat="@drawable/ic_fast_forward"
+ tools:srcCompat="@drawable/ic_fast_forward" />
<TextView
- android:id="@+id/txtvFF"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_below="@id/butFF"
- android:layout_alignLeft="@id/butFF"
- android:layout_alignStart="@id/butFF"
- android:layout_alignRight="@id/butFF"
- android:layout_alignEnd="@id/butFF"
- android:gravity="center"
- android:text="30"
- android:textSize="12sp"
- android:textColor="?android:attr/textColorSecondary"
- android:clickable="false"/>
+ android:id="@+id/txtvFF"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/butFF"
+ android:layout_alignStart="@id/butFF"
+ android:layout_alignLeft="@id/butFF"
+ android:layout_alignEnd="@id/butFF"
+ android:layout_alignRight="@id/butFF"
+ android:clickable="false"
+ android:gravity="center"
+ android:text="30"
+ android:textColor="?android:attr/textColorSecondary"
+ android:textSize="12sp" />
<ImageButton
- android:id="@+id/butSkip"
- android:layout_width="@dimen/audioplayer_playercontrols_length"
- android:layout_height="@dimen/audioplayer_playercontrols_length"
- android:layout_toRightOf="@id/butFF"
- android:layout_toEndOf="@id/butFF"
- android:layout_centerVertical="true"
- android:background="?attr/selectableItemBackgroundBorderless"
- android:scaleType="fitCenter"
- app:srcCompat="@drawable/ic_skip_48dp"
- android:contentDescription="@string/skip_episode_label"
- tools:srcCompat="@drawable/ic_skip_48dp"/>
+ android:id="@+id/butSkip"
+ android:layout_width="@dimen/audioplayer_playercontrols_length"
+ android:layout_height="@dimen/audioplayer_playercontrols_length"
+ android:layout_centerVertical="true"
+ android:layout_toEndOf="@id/butFF"
+ android:layout_toRightOf="@id/butFF"
+ android:background="?attr/selectableItemBackgroundBorderless"
+ android:contentDescription="@string/skip_episode_label"
+ android:scaleType="fitCenter"
+ app:srcCompat="@drawable/ic_skip_48dp"
+ tools:srcCompat="@drawable/ic_skip_48dp" />
+
</RelativeLayout>
</LinearLayout>
diff --git a/app/src/main/res/layout/feed_item_list_fragment.xml b/app/src/main/res/layout/feed_item_list_fragment.xml
index 734ce64dd..2f175770f 100644
--- a/app/src/main/res/layout/feed_item_list_fragment.xml
+++ b/app/src/main/res/layout/feed_item_list_fragment.xml
@@ -22,19 +22,16 @@
<ImageView
android:id="@+id/imgvBackground"
android:layout_width="match_parent"
- android:layout_height="232dp"
+ android:layout_height="match_parent"
android:background="@color/image_readability_tint"
style="@style/BigBlurryBackground"
app:layout_collapseMode="parallax"
app:layout_collapseParallaxMultiplier="0.6" />
<include
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_gravity="bottom"
+ android:id="@+id/header"
layout="@layout/feeditemlist_header"
- app:layout_collapseMode="parallax"
- app:layout_collapseParallaxMultiplier="0.6" />
+ app:layout_collapseMode="parallax" />
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
@@ -73,6 +70,7 @@
android:visibility="gone" />
<include
+ android:id="@+id/more_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
diff --git a/app/src/main/res/layout/feed_refresh_dialog.xml b/app/src/main/res/layout/feed_refresh_dialog.xml
index 02b49a6c6..5a6770a80 100644
--- a/app/src/main/res/layout/feed_refresh_dialog.xml
+++ b/app/src/main/res/layout/feed_refresh_dialog.xml
@@ -10,7 +10,7 @@
android:id="@+id/intervalRadioButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:text="@string/feed_refresh_interval"/>
+ android:text="@string/feed_refresh_interval" />
<Spinner
android:id="@+id/spinner"
@@ -24,7 +24,7 @@
android:layout_height="wrap_content"
android:text="@string/feed_refresh_time" />
- <TimePicker
+ <de.danoeh.antennapod.view.TimePicker
android:id="@+id/timePicker"
android:layout_width="match_parent"
android:layout_height="wrap_content"
@@ -36,4 +36,5 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/feed_refresh_never" />
+
</RadioGroup>
diff --git a/app/src/main/res/layout/feedinfo.xml b/app/src/main/res/layout/feedinfo.xml
index b0a73cb97..ac9db8fa2 100644
--- a/app/src/main/res/layout/feedinfo.xml
+++ b/app/src/main/res/layout/feedinfo.xml
@@ -23,19 +23,16 @@
<ImageView
android:id="@+id/imgvBackground"
android:layout_width="match_parent"
- android:layout_height="232dp"
+ android:layout_height="match_parent"
android:background="@color/image_readability_tint"
style="@style/BigBlurryBackground"
app:layout_collapseMode="parallax"
app:layout_collapseParallaxMultiplier="0.6" />
<include
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_gravity="bottom"
+ android:id="@+id/header"
layout="@layout/feeditemlist_header"
- app:layout_collapseMode="parallax"
- app:layout_collapseParallaxMultiplier="0.6" />
+ app:layout_collapseMode="parallax" />
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
diff --git a/app/src/main/res/layout/feeditemlist_header.xml b/app/src/main/res/layout/feeditemlist_header.xml
index 9e83051d6..6d96141a2 100644
--- a/app/src/main/res/layout/feeditemlist_header.xml
+++ b/app/src/main/res/layout/feeditemlist_header.xml
@@ -1,134 +1,181 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
- xmlns:tools="http://schemas.android.com/tools"
- xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/headerContainer"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingHorizontal="@dimen/additional_horizontal_spacing"
+ android:layout_marginTop="?attr/actionBarSize"
+ android:orientation="vertical">
+
+ <RelativeLayout
android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:id="@+id/headerContainer"
- android:paddingHorizontal="@dimen/additional_horizontal_spacing"
- android:orientation="vertical">
+ android:layout_height="156dp"
+ android:orientation="horizontal"
+ android:gravity="bottom">
- <LinearLayout
+ <LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
+ android:padding="8dp"
+ android:background="@color/image_readability_tint"
android:orientation="horizontal"
- android:padding="16dp"
- android:layout_marginBottom="16dp"
- android:gravity="center_vertical">
+ android:layout_alignParentBottom="true">
+
+ <View
+ android:layout_width="148dp"
+ android:layout_height="match_parent" />
+
+ <ImageButton
+ android:id="@+id/butShowInfo"
+ android:layout_width="40dp"
+ android:layout_height="40dp"
+ android:background="?attr/selectableItemBackground"
+ android:contentDescription="@string/show_info_label"
+ android:layout_marginLeft="-8dp"
+ android:layout_marginStart="-8dp"
+ android:scaleType="fitXY"
+ android:padding="8dp"
+ app:srcCompat="@drawable/ic_info_white"
+ tools:visibility="visible" />
+
+ <ImageButton
+ android:id="@+id/butFilter"
+ android:layout_width="40dp"
+ android:layout_height="40dp"
+ android:background="?attr/selectableItemBackground"
+ android:contentDescription="@string/filter"
+ android:scaleType="fitXY"
+ android:padding="8dp"
+ app:srcCompat="@drawable/ic_filter_white"
+ tools:visibility="visible" />
+
+ <ImageButton
+ android:id="@+id/butShowSettings"
+ android:layout_width="40dp"
+ android:layout_height="40dp"
+ android:background="?attr/selectableItemBackground"
+ android:contentDescription="@string/show_feed_settings_label"
+ android:scaleType="fitXY"
+ android:padding="8dp"
+ app:srcCompat="@drawable/ic_settings_white"
+ tools:visibility="visible" />
+
+ </LinearLayout>
<ImageView
+ android:layout_width="12dp"
+ android:layout_height="12dp"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentLeft="true"
+ android:scaleType="fitXY"
+ android:src="@drawable/ic_rounded_corner_left" />
+
+ <ImageView
+ android:layout_width="12dp"
+ android:layout_height="12dp"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentRight="true"
+ android:scaleType="fitXY"
+ android:src="@drawable/ic_rounded_corner_right" />
+
+ <androidx.cardview.widget.CardView
+ android:id="@+id/coverHolder"
+ android:layout_width="124dp"
+ android:layout_height="124dp"
+ android:layout_marginBottom="24dp"
+ android:layout_marginLeft="16dp"
+ android:layout_marginRight="16dp"
+ android:layout_alignParentBottom="true"
+ app:cardBackgroundColor="@color/non_square_icon_background"
+ app:cardCornerRadius="8dp"
+ app:cardPreventCornerOverlap="false"
+ app:cardElevation="0dp">
+
+ <ImageView
android:id="@+id/imgvCover"
- android:layout_width="100dp"
- android:layout_height="100dp"
- android:layout_marginRight="16dp"
- android:layout_marginEnd="16dp"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_centerVertical="true"
android:importantForAccessibility="no"
- tools:src="@tools:sample/avatars"
- tools:background="@android:color/holo_green_dark"/>
+ tools:src="@tools:sample/avatars" />
+
+ </androidx.cardview.widget.CardView>
<LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_toEndOf="@id/coverHolder"
+ android:layout_alignTop="@id/coverHolder"
+ android:layout_marginEnd="16dp"
+ android:orientation="vertical">
+
+ <TextView
+ android:id="@+id/txtvTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:orientation="vertical">
- <TextView
- android:id="@+id/txtvTitle"
- style="@style/AntennaPod.TextView.Heading"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:ellipsize="end"
- android:maxLines="2"
- android:shadowColor="@color/black"
- android:shadowRadius="2"
- android:textColor="@color/white"
- tools:text="Podcast title"
- tools:background="@android:color/holo_green_dark"/>
+ android:ellipsize="end"
+ android:maxLines="2"
+ android:shadowColor="@color/black"
+ android:shadowRadius="2"
+ android:textColor="@color/white"
+ style="@style/AntennaPod.TextView.Heading"
+ tools:text="Podcast title" />
<TextView
- android:id="@+id/txtvAuthor"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:ellipsize="end"
- android:maxLines="2"
- android:shadowColor="@color/black"
- android:shadowRadius="2"
- android:textColor="@color/white"
- android:textSize="@dimen/text_size_small"
- tools:text="Podcast author"
- tools:background="@android:color/holo_green_dark"/>
-
- <LinearLayout
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:orientation="horizontal">
- <ImageButton
- android:id="@+id/butShowInfo"
- android:layout_width="40dp"
- android:layout_height="40dp"
- android:background="?attr/selectableItemBackground"
- android:contentDescription="@string/show_info_label"
- app:srcCompat="@drawable/ic_info_white"
- tools:background="@android:color/holo_green_dark"
- android:layout_marginLeft="-8dp"
- android:layout_marginStart="-8dp"
- android:scaleType="fitXY"
- android:visibility="invisible"
- tools:visibility="visible"
- android:padding="8dp"/>
-
- <ImageButton
- android:id="@+id/butShowSettings"
- android:layout_width="40dp"
- android:layout_height="40dp"
- android:background="?attr/selectableItemBackground"
- android:contentDescription="@string/show_feed_settings_label"
- app:srcCompat="@drawable/ic_settings_white"
- tools:background="@android:color/holo_green_dark"
- android:scaleType="fitXY"
- android:visibility="invisible"
- tools:visibility="visible"
- android:padding="8dp"/>
- </LinearLayout>
+ android:id="@+id/txtvAuthor"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:ellipsize="end"
+ android:maxLines="2"
+ android:shadowColor="@color/black"
+ android:shadowRadius="2"
+ android:textColor="@color/white"
+ android:textSize="@dimen/text_size_small"
+ tools:text="Podcast author" />
</LinearLayout>
- </LinearLayout>
+ </RelativeLayout>
<com.joanzapata.iconify.widget.IconTextView
- android:id="@+id/txtvFailure"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:padding="2dp"
- android:background="@color/download_failed_red"
- android:gravity="center"
- android:textColor="@color/white"
- android:visibility="gone"
- android:text="@string/refresh_failed_msg"
- tools:visibility="visible"
- tools:text="(!) Last refresh failed"/>
+ android:id="@+id/txtvFailure"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:padding="2dp"
+ android:background="@color/download_failed_red"
+ android:gravity="center"
+ android:textColor="@color/white"
+ android:visibility="gone"
+ android:text="@string/refresh_failed_msg"
+ tools:visibility="visible"
+ tools:text="(!) Last refresh failed" />
<TextView
- android:id="@+id/txtvInformation"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:padding="2dp"
- android:background="?android:attr/windowBackground"
- android:foreground="?android:attr/selectableItemBackground"
- android:visibility="gone"
- android:gravity="center"
- android:textColor="?attr/colorAccent"
- tools:visibility="visible"
- tools:text="(i) Information"/>
+ android:id="@+id/txtvInformation"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:padding="2dp"
+ android:background="?android:attr/windowBackground"
+ android:foreground="?android:attr/selectableItemBackground"
+ android:visibility="gone"
+ android:gravity="center"
+ android:textColor="?attr/colorAccent"
+ tools:visibility="visible"
+ tools:text="(i) Information" />
<TextView
- android:id="@+id/txtvUpdatesDisabled"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:padding="2dp"
- android:background="?android:attr/windowBackground"
- android:visibility="gone"
- android:gravity="center"
- android:textColor="?attr/colorAccent"
- tools:visibility="visible"
- tools:text="Updates disabled"/>
+ android:id="@+id/txtvUpdatesDisabled"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:padding="2dp"
+ android:background="?android:attr/windowBackground"
+ android:visibility="gone"
+ android:gravity="center"
+ android:textColor="?attr/colorAccent"
+ tools:visibility="visible"
+ tools:text="Updates disabled" />
+
</LinearLayout>
diff --git a/app/src/main/res/layout/filter_dialog_row.xml b/app/src/main/res/layout/filter_dialog_row.xml
index 914525387..7ecfc8223 100644
--- a/app/src/main/res/layout/filter_dialog_row.xml
+++ b/app/src/main/res/layout/filter_dialog_row.xml
@@ -12,6 +12,7 @@
android:layout_height="wrap_content"
android:layout_weight="1"
android:clipChildren="true"
+ android:layout_gravity="center_vertical"
app:cardCornerRadius="32dp"
app:cardElevation="0dp">
@@ -28,34 +29,38 @@
android:layout_marginRight="2dp"
android:layout_weight="1"
android:background="?attr/filter_dialog_button_background"
- style="@style/NoButtonRadio"
+ android:minHeight="40dp"
android:foreground="?android:attr/selectableItemBackground"
android:checked="false"
android:gravity="center"
- android:textColor="@color/filter_dialog_button_text" />
+ android:textColor="@color/filter_dialog_button_text"
+ style="@style/NoButtonRadio" />
<RadioButton
android:id="@+id/filter_dialog_radioButton2"
android:layout_width="0dp"
android:layout_height="match_parent"
+ android:minHeight="40dp"
android:layout_weight="1"
android:background="?attr/filter_dialog_button_background"
- style="@style/NoButtonRadio"
android:foreground="?android:attr/selectableItemBackground"
android:checked="false"
android:gravity="center"
- android:textColor="@color/filter_dialog_button_text" />
+ android:textColor="@color/filter_dialog_button_text"
+ style="@style/NoButtonRadio" />
+
</LinearLayout>
+
</androidx.cardview.widget.CardView>
<RadioButton
android:id="@+id/filter_dialog_clear"
- android:layout_width="48dp"
- android:layout_height="48dp"
+ android:layout_width="40dp"
+ android:layout_height="40dp"
android:background="@drawable/ic_filter_close"
- style="@style/NoButtonRadio"
android:foreground="?android:attr/selectableItemBackground"
android:layout_gravity="center_vertical"
- android:checked="true" />
+ android:checked="true"
+ style="@style/NoButtonRadio" />
</de.danoeh.antennapod.ui.common.RecursiveRadioGroup>
diff --git a/app/src/main/res/layout/main.xml b/app/src/main/res/layout/main.xml
index 10b560faf..c5cfb494e 100644
--- a/app/src/main/res/layout/main.xml
+++ b/app/src/main/res/layout/main.xml
@@ -17,7 +17,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
- <FrameLayout
+ <androidx.fragment.app.FragmentContainerView
android:id="@+id/main_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
@@ -25,7 +25,7 @@
android:foreground="?android:windowContentOverlay"
tools:background="@android:color/holo_red_dark" />
- <FrameLayout
+ <androidx.fragment.app.FragmentContainerView
android:id="@+id/audioplayerFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
@@ -36,7 +36,7 @@
</androidx.coordinatorlayout.widget.CoordinatorLayout>
- <FrameLayout
+ <androidx.fragment.app.FragmentContainerView
android:id="@+id/navDrawerFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
diff --git a/app/src/main/res/layout/share_episode_dialog.xml b/app/src/main/res/layout/share_episode_dialog.xml
index d79854972..f03899671 100644
--- a/app/src/main/res/layout/share_episode_dialog.xml
+++ b/app/src/main/res/layout/share_episode_dialog.xml
@@ -11,17 +11,22 @@
android:id="@+id/share_dialog_radio_group"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:layout_marginBottom="16dp"
android:orientation="vertical">
<RadioButton
- android:id="@+id/share_link_to_episode_radio"
+ android:id="@+id/share_social_radio"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:text="@string/share_dialog_link_to_episode"
+ android:text="@string/share_dialog_for_social"
android:checked="true" />
<RadioButton
+ android:id="@+id/share_media_receiver_radio"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/share_dialog_media_address" />
+
+ <RadioButton
android:id="@+id/share_media_file_radio"
android:layout_width="match_parent"
android:layout_height="wrap_content"
@@ -29,13 +34,14 @@
</RadioGroup>
- <TextView
+ <View
android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:text="@string/share_dialog_include_label" />
+ android:layout_height="1dp"
+ android:layout_marginVertical="8dp"
+ android:background="?android:attr/listDivider" />
<CheckBox
- android:id="@+id/share_start_at_timer_dialog"
+ android:id="@+id/share_position_checkbox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/share_playback_position_dialog_label" />
diff --git a/app/src/main/res/layout/statistics_mode_select_dialog.xml b/app/src/main/res/layout/statistics_mode_select_dialog.xml
deleted file mode 100644
index 8f8e1e657..000000000
--- a/app/src/main/res/layout/statistics_mode_select_dialog.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<RadioGroup xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:orientation="vertical"
- android:padding="16dp">
-
- <TextView
- android:text="@string/statistics_speed_not_counted"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_marginBottom="16dp"/>
-
- <RadioButton
- android:id="@+id/statistics_mode_normal"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:text="@string/statistics_mode_normal"/>
-
- <RadioButton
- android:id="@+id/statistics_mode_count_all"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:text="@string/statistics_mode_count_all"/>
-</RadioGroup>
diff --git a/app/src/main/res/menu/feedinfo.xml b/app/src/main/res/menu/feedinfo.xml
index 83ad079b4..254e1834b 100644
--- a/app/src/main/res/menu/feedinfo.xml
+++ b/app/src/main/res/menu/feedinfo.xml
@@ -8,22 +8,11 @@
android:title="@string/visit_website_label"
android:visible="true"/>
<item
- android:id="@+id/share_parent"
+ android:id="@+id/share_item"
custom:showAsAction="ifRoom"
- android:title="@string/share_label_with_ellipses"
+ android:title="@string/share_label"
android:icon="@drawable/ic_share"
- android:visible="true">
- <menu android:id="@+id/share_submenu">
- <item
- android:id="@+id/share_link_item"
- custom:showAsAction="collapseActionView"
- android:title="@string/share_website_url_label"/>
- <item
- android:id="@+id/share_download_url_item"
- custom:showAsAction="collapseActionView"
- android:title="@string/share_feed_url_label"/>
- </menu>
- </item>
+ android:visible="true" />
<item
android:id="@+id/reconnect_local_folder"
custom:showAsAction="collapseActionView"
diff --git a/app/src/main/res/menu/feeditem_options.xml b/app/src/main/res/menu/feeditem_options.xml
index 70400fe55..bcb2a59b8 100644
--- a/app/src/main/res/menu/feeditem_options.xml
+++ b/app/src/main/res/menu/feeditem_options.xml
@@ -59,7 +59,7 @@
<item
android:id="@+id/share_item"
android:menuCategory="container"
- android:title="@string/share_label_with_ellipses">
+ android:title="@string/share_label">
</item>
<item
diff --git a/app/src/main/res/menu/feedlist.xml b/app/src/main/res/menu/feedlist.xml
index 3c66ec4ba..2086ff547 100644
--- a/app/src/main/res/menu/feedlist.xml
+++ b/app/src/main/res/menu/feedlist.xml
@@ -4,17 +4,9 @@
<item
android:id="@+id/sort_items"
- android:icon="@drawable/ic_sort"
android:menuCategory="container"
android:title="@string/sort"
- custom:showAsAction="always">
- </item>
- <item
- android:id="@+id/filter_items"
- android:icon="@drawable/ic_filter"
- android:menuCategory="container"
- android:title="@string/filter"
- custom:showAsAction="always">
+ custom:showAsAction="never">
</item>
<item
android:id="@+id/refresh_item"
@@ -49,19 +41,7 @@
android:id="@+id/share_item"
android:menuCategory="container"
custom:showAsAction="never"
- android:title="@string/share_label_with_ellipses">
- <menu>
- <item
- android:id="@+id/share_link_item"
- android:title="@string/share_website_url_label">
- </item>
- <item
- android:id="@+id/share_download_url_item"
- android:title="@string/share_feed_url_label">
- </item>
- </menu>
- </item>
-
+ android:title="@string/share_label" />
<item
android:id="@+id/rename_item"
diff --git a/app/src/main/res/menu/mediaplayer.xml b/app/src/main/res/menu/mediaplayer.xml
index a9f15317b..7c66a4d10 100644
--- a/app/src/main/res/menu/mediaplayer.xml
+++ b/app/src/main/res/menu/mediaplayer.xml
@@ -52,16 +52,16 @@
</item>
<item
- android:id="@+id/player_go_to_picture_in_picture"
- custom:showAsAction="collapseActionView"
- android:title="@string/player_go_to_picture_in_picture"
- android:visible="false">
+ android:id="@+id/player_switch_to_audio_only"
+ custom:showAsAction="collapseActionView"
+ android:title="@string/player_switch_to_audio_only"
+ android:visible="false">
</item>
<item
android:id="@+id/share_item"
android:menuCategory="container"
custom:showAsAction="never"
- android:title="@string/share_label_with_ellipses">
+ android:title="@string/share_label">
</item>
</menu> \ No newline at end of file
diff --git a/app/src/main/res/menu/subscriptions.xml b/app/src/main/res/menu/subscriptions.xml
index 9276db562..fa63bf583 100644
--- a/app/src/main/res/menu/subscriptions.xml
+++ b/app/src/main/res/menu/subscriptions.xml
@@ -7,6 +7,11 @@
custom:showAsAction="always"
android:title="@string/search_label"/>
<item
+ android:id="@+id/action_statistics"
+ android:icon="@drawable/chart_box_outline"
+ android:title="@string/statistics_label"
+ custom:showAsAction="always" />
+ <item
android:id="@+id/refresh_item"
android:title="@string/refresh_label"
android:menuCategory="container"
diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml
index 7c5012899..9967d7fd1 100644
--- a/app/src/main/res/xml/preferences.xml
+++ b/app/src/main/res/xml/preferences.xml
@@ -44,11 +44,6 @@
android:title="@string/notification_pref_fragment"
android:icon="@drawable/ic_notifications"/>
- <Preference
- android:key="statistics"
- android:title="@string/statistics_label"
- android:icon="@drawable/ic_statistics" />
-
<PreferenceCategory
android:key="project"
android:title="@string/project_pref">
diff --git a/app/src/main/res/xml/preferences_playback.xml b/app/src/main/res/xml/preferences_playback.xml
index add9e8d4c..969013056 100644
--- a/app/src/main/res/xml/preferences_playback.xml
+++ b/app/src/main/res/xml/preferences_playback.xml
@@ -34,13 +34,6 @@
android:key="prefResumeAfterCall"
android:summary="@string/pref_resumeAfterCall_sum"
android:title="@string/pref_resumeAfterCall_title"/>
- <ListPreference
- android:defaultValue="pip"
- android:entries="@array/video_background_behavior_options"
- android:entryValues="@array/video_background_behavior_values"
- android:key="prefVideoBehavior"
- android:summary="@string/pref_videoBehavior_sum"
- android:title="@string/pref_videoBehavior_title"/>
</PreferenceCategory>
<PreferenceCategory android:title="@string/playback_control">
diff --git a/build.gradle b/build.gradle
index 44b1daaf5..aa2bb3bae 100644
--- a/build.gradle
+++ b/build.gradle
@@ -48,7 +48,7 @@ project.ext {
recyclerViewVersion = "1.2.1"
viewPager2Version = "1.1.0-beta01"
workManagerVersion = "2.3.4"
- googleMaterialVersion = "1.1.0"
+ googleMaterialVersion = "1.4.0"
// Third-party
commonslangVersion = "3.6"
diff --git a/common.gradle b/common.gradle
index 4f023212b..2bfbb2f61 100644
--- a/common.gradle
+++ b/common.gradle
@@ -1,11 +1,11 @@
android {
- compileSdkVersion 31
+ compileSdk 31
defaultConfig {
- minSdkVersion 19
- targetSdkVersion 30
+ minSdk 19
+ targetSdk 30
- multiDexEnabled false
+ multiDexEnabled true
vectorDrawables.useSupportLibrary true
vectorDrawables.generatedDensities = []
@@ -18,12 +18,6 @@ android {
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard.cfg"
}
debug {
- // debug build has method count over 64k single-dex threshold.
- // For building debug build to use on Android < 21 (pre-Android 5) devices,
- // you need to manually change class
- // de.danoeh.antennapod.PodcastApp to extend MultiDexApplication .
- // See Issue #2813
- multiDexEnabled true
}
}
diff --git a/core/build.gradle b/core/build.gradle
index 700487701..9eb9c8c90 100644
--- a/core/build.gradle
+++ b/core/build.gradle
@@ -1,13 +1,14 @@
-apply plugin: "com.android.library"
+plugins {
+ id("com.android.library")
+}
apply from: "../common.gradle"
apply from: "../playFlavor.gradle"
android {
lintOptions {
disable "InvalidPeriodicWorkRequestInterval", "ObsoleteLintCustomCheck", "DefaultLocale", "UnusedAttribute",
- "ParcelClassLoader", "Typos", "ExtraTranslation", "ImpliedQuantity", "CheckResult",
- "PluralsCandidate", "UnusedQuantity", "StringFormatCount", "TrustAllX509TrustManager",
- "StaticFieldLeak", "TypographyEllipsis", "IconDensities", "IconDuplicates"
+ "ParcelClassLoader", "CheckResult", "TrustAllX509TrustManager",
+ "StaticFieldLeak", "IconDensities", "IconDuplicates"
}
defaultConfig {
@@ -29,8 +30,10 @@ dependencies {
implementation project(':parser:media')
implementation project(':playback:base')
implementation project(':playback:cast')
+ implementation project(':storage:database')
implementation project(':ui:app-start-intent')
implementation project(':ui:common')
+ implementation project(':ui:i18n')
implementation project(':ui:png-icons')
annotationProcessor "androidx.annotation:annotation:$annotationVersion"
diff --git a/core/lint.xml b/core/lint.xml
index aa2c50677..f3cb24810 100644
--- a/core/lint.xml
+++ b/core/lint.xml
@@ -4,12 +4,4 @@
<ignore path="res/drawable/ic_settings.xml" />
<ignore path="res/drawable/ic_settings_white.xml" />
</issue>
-
- <issue id="MissingDefaultResource">
- <ignore path="**/values-**/strings.xml" />
- </issue>
-
- <issue id="UnusedResources" severity="error">
- <ignore path="**/values-**/strings.xml" />
- </issue>
</lint>
diff --git a/core/src/main/AndroidManifest.xml b/core/src/main/AndroidManifest.xml
index 7478eb48a..3dd15a588 100644
--- a/core/src/main/AndroidManifest.xml
+++ b/core/src/main/AndroidManifest.xml
@@ -46,20 +46,10 @@
android:label="@string/feed_update_receiver_name"
android:exported="true"
tools:ignore="ExportedReceiver" /> <!-- allow feeds update to be triggered by external apps -->
-
- <service
- android:name=".widget.WidgetUpdaterJobService"
- android:permission="android.permission.BIND_JOB_SERVICE"
- android:exported="true"/>
</application>
<queries>
<intent>
- <action android:name="android.intent.action.SEND" />
- <data android:mimeType="*/*" />
- </intent>
-
- <intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="https" />
</intent>
diff --git a/core/src/main/java/androidx/core/app/SafeJobIntentService.java b/core/src/main/java/androidx/core/app/SafeJobIntentService.java
deleted file mode 100644
index aedc9418b..000000000
--- a/core/src/main/java/androidx/core/app/SafeJobIntentService.java
+++ /dev/null
@@ -1,118 +0,0 @@
-package androidx.core.app;
-
-import android.app.job.JobParameters;
-import android.app.job.JobServiceEngine;
-import android.app.job.JobWorkItem;
-import android.content.Intent;
-import android.os.Build;
-import android.os.IBinder;
-import androidx.annotation.RequiresApi;
-import android.util.Log;
-
-
-public abstract class SafeJobIntentService extends JobIntentService {
-
- @Override
- public void onCreate() {
- super.onCreate();
- if (Build.VERSION.SDK_INT >= 26) {
- mJobImpl = new SafeJobServiceEngineImpl(this);
- }
- }
-
- /**
- * Implementation of a safe JobServiceEngine for interaction with JobIntentService.
- */
- @RequiresApi(26)
- static final class SafeJobServiceEngineImpl extends JobServiceEngine
- implements JobIntentService.CompatJobEngine {
- static final String TAG = "JobServiceEngineImpl";
-
- static final boolean DEBUG = false;
-
- final JobIntentService mService;
- final Object mLock = new Object();
- JobParameters mParams;
-
- final class WrapperWorkItem implements JobIntentService.GenericWorkItem {
- final JobWorkItem mJobWork;
-
- WrapperWorkItem(JobWorkItem jobWork) {
- mJobWork = jobWork;
- }
-
- @Override
- public Intent getIntent() {
- return mJobWork.getIntent();
- }
-
- @Override
- public void complete() {
- synchronized (mLock) {
- if (mParams != null) {
- try {
- mParams.completeWork(mJobWork);
- } catch (SecurityException e) {
- Log.e(TAG, Log.getStackTraceString(e));
- }
- }
- }
- }
- }
-
- SafeJobServiceEngineImpl(JobIntentService service) {
- super(service);
- mService = service;
- }
-
- @Override
- public IBinder compatGetBinder() {
- return getBinder();
- }
-
- @Override
- public boolean onStartJob(JobParameters params) {
- if (DEBUG) Log.d(TAG, "onStartJob: " + params);
- mParams = params;
- // We can now start dequeuing work!
- mService.ensureProcessorRunningLocked(false);
- return true;
- }
-
- @Override
- public boolean onStopJob(JobParameters params) {
- if (DEBUG) Log.d(TAG, "onStartJob: " + params);
- boolean result = mService.doStopCurrentWork();
- synchronized (mLock) {
- // Once we return, the job is stopped, so its JobParameters are no
- // longer valid and we should not be doing anything with them.
- mParams = null;
- }
- return result;
- }
-
- /**
- * Dequeue some work.
- */
- @Override
- public JobIntentService.GenericWorkItem dequeueWork() {
- JobWorkItem work = null;
- synchronized (mLock) {
- if (mParams == null) {
- return null;
- }
- try {
- work = mParams.dequeueWork();
- } catch (SecurityException e) {
- Log.e(TAG, Log.getStackTraceString(e));
- }
- }
- if (work != null) {
- work.getIntent().setExtrasClassLoader(mService.getClassLoader());
- return new WrapperWorkItem(work);
- } else {
- return null;
- }
- }
- }
-}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/ClientConfig.java b/core/src/main/java/de/danoeh/antennapod/core/ClientConfig.java
index ac67fb042..86326911d 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/ClientConfig.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/ClientConfig.java
@@ -9,9 +9,9 @@ import de.danoeh.antennapod.core.preferences.SleepTimerPreferences;
import de.danoeh.antennapod.core.preferences.UsageStatistics;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.service.download.AntennapodHttpClient;
-import de.danoeh.antennapod.core.storage.PodDBAdapter;
import de.danoeh.antennapod.core.util.NetworkUtils;
import de.danoeh.antennapod.core.util.gui.NotificationUtils;
+import de.danoeh.antennapod.storage.database.PodDBAdapter;
import java.io.File;
@@ -43,6 +43,7 @@ public class ClientConfig {
SslProviderInstaller.install(context);
NetworkUtils.init(context);
AntennapodHttpClient.setCacheDirectory(new File(context.getCacheDir(), "okhttp"));
+ AntennapodHttpClient.setProxyConfig(UserPreferences.getProxyConfig());
SleepTimerPreferences.init(context);
NotificationUtils.createChannels(context);
initialized = true;
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
index c9fe886fb..47b5dcd09 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/dialog/ConfirmationDialog.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/dialog/ConfirmationDialog.java
@@ -13,50 +13,43 @@ import de.danoeh.antennapod.core.R;
*/
public abstract class ConfirmationDialog {
- private static final String TAG = ConfirmationDialog.class.getSimpleName();
+ private static final String TAG = ConfirmationDialog.class.getSimpleName();
- private final Context context;
- private final int titleId;
- private final String message;
+ private final Context context;
+ private final int titleId;
+ private final String message;
- private int positiveText;
- private int negativeText;
+ private int positiveText;
- public ConfirmationDialog(Context context, int titleId, int messageId) {
- this(context, titleId, context.getString(messageId));
- }
+ 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;
- }
+ 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();
- }
+ private void onCancelButtonPressed(DialogInterface dialog) {
+ Log.d(TAG, "Dialog was cancelled");
+ dialog.dismiss();
+ }
public void setPositiveText(int id) {
this.positiveText = id;
}
- public void setNegativeText(int id) {
- this.negativeText = id;
+ public abstract void onConfirmButtonPressed(DialogInterface dialog);
+
+ public final AlertDialog createNewDialog() {
+ AlertDialog.Builder builder = new AlertDialog.Builder(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();
}
-
-
- public abstract void onConfirmButtonPressed(DialogInterface dialog);
-
- public final AlertDialog createNewDialog() {
- AlertDialog.Builder builder = new AlertDialog.Builder(context);
- builder.setTitle(titleId);
- builder.setMessage(message);
- builder.setPositiveButton(positiveText != 0 ? positiveText : R.string.confirm_label,
- (dialog, which) -> onConfirmButtonPressed(dialog));
- builder.setNegativeButton(negativeText != 0 ? negativeText : 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/LocalFeedUpdater.java b/core/src/main/java/de/danoeh/antennapod/core/feed/LocalFeedUpdater.java
index 5d685c24f..d37bc230d 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/feed/LocalFeedUpdater.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/feed/LocalFeedUpdater.java
@@ -1,15 +1,17 @@
package de.danoeh.antennapod.core.feed;
-import android.content.ContentResolver;
import android.content.Context;
import android.media.MediaMetadataRetriever;
import android.net.Uri;
import android.text.TextUtils;
+import android.util.Log;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.documentfile.provider.DocumentFile;
import java.io.IOException;
+import java.io.InputStream;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
@@ -23,25 +25,33 @@ import java.util.Set;
import java.util.UUID;
import de.danoeh.antennapod.core.R;
-import de.danoeh.antennapod.core.service.download.DownloadStatus;
+import de.danoeh.antennapod.model.download.DownloadStatus;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.storage.DBTasks;
import de.danoeh.antennapod.core.storage.DBWriter;
import de.danoeh.antennapod.parser.feed.util.DateUtils;
-import de.danoeh.antennapod.core.util.DownloadError;
+import de.danoeh.antennapod.model.download.DownloadError;
import de.danoeh.antennapod.model.feed.Feed;
import de.danoeh.antennapod.model.feed.FeedItem;
import de.danoeh.antennapod.model.feed.FeedMedia;
import de.danoeh.antennapod.model.feed.FeedPreferences;
import de.danoeh.antennapod.model.playback.MediaType;
+import de.danoeh.antennapod.parser.feed.util.MimeTypeUtils;
+import de.danoeh.antennapod.parser.media.id3.ID3ReaderException;
+import de.danoeh.antennapod.parser.media.id3.Id3MetadataReader;
+import de.danoeh.antennapod.parser.media.vorbis.VorbisCommentMetadataReader;
+import de.danoeh.antennapod.parser.media.vorbis.VorbisCommentReaderException;
+import org.apache.commons.io.input.CountingInputStream;
public class LocalFeedUpdater {
+ private static final String TAG = "LocalFeedUpdater";
static final String[] PREFERRED_FEED_IMAGE_FILENAMES = { "folder.jpg", "Folder.jpg", "folder.png", "Folder.png" };
- public static void updateFeed(Feed feed, Context context) {
+ public static void updateFeed(Feed feed, Context context,
+ @Nullable UpdaterProgressListener updaterProgressListener) {
try {
- tryUpdateFeed(feed, context);
+ tryUpdateFeed(feed, context, updaterProgressListener);
if (mustReportDownloadSuccessful(feed)) {
reportSuccess(feed);
@@ -52,7 +62,8 @@ public class LocalFeedUpdater {
}
}
- private static void tryUpdateFeed(Feed feed, Context context) throws IOException {
+ private static void tryUpdateFeed(Feed feed, Context context, UpdaterProgressListener updaterProgressListener)
+ throws IOException {
String uriString = feed.getDownload_url().replace(Feed.PREFIX_LOCAL_FOLDER, "");
DocumentFile documentFolder = DocumentFile.fromTreeUri(context, Uri.parse(uriString));
if (documentFolder == null) {
@@ -74,21 +85,8 @@ public class LocalFeedUpdater {
List<DocumentFile> mediaFiles = new ArrayList<>();
Set<String> mediaFileNames = new HashSet<>();
for (DocumentFile file : documentFolder.listFiles()) {
- String mime = file.getType();
- if (mime == null) {
- continue;
- }
-
- MediaType mediaType = MediaType.fromMimeType(mime);
- if (mediaType == MediaType.UNKNOWN) {
- String path = file.getUri().toString();
- int fileExtensionPosition = path.lastIndexOf('.');
- if (fileExtensionPosition >= 0) {
- String extensionWithoutDot = path.substring(fileExtensionPosition + 1);
- mediaType = MediaType.fromFileExtension(extensionWithoutDot);
- }
- }
-
+ String mimeType = MimeTypeUtils.getMimeType(file.getType(), file.getUri().toString());
+ MediaType mediaType = MediaType.fromMimeType(mimeType);
if (mediaType == MediaType.AUDIO || mediaType == MediaType.VIDEO) {
mediaFiles.add(file);
mediaFileNames.add(file.getName());
@@ -97,14 +95,17 @@ public class LocalFeedUpdater {
// add new files to feed and update item data
List<FeedItem> newItems = feed.getItems();
- for (DocumentFile f : mediaFiles) {
- FeedItem oldItem = feedContainsFile(feed, f.getName());
- FeedItem newItem = createFeedItem(feed, f, context);
+ for (int i = 0; i < mediaFiles.size(); i++) {
+ FeedItem oldItem = feedContainsFile(feed, mediaFiles.get(i).getName());
+ FeedItem newItem = createFeedItem(feed, mediaFiles.get(i), context);
if (oldItem == null) {
newItems.add(newItem);
} else {
oldItem.updateFromOther(newItem);
}
+ if (updaterProgressListener != null) {
+ updaterProgressListener.onLocalFileScanned(i, mediaFiles.size());
+ }
}
// remove feed items without corresponding file
@@ -116,7 +117,7 @@ public class LocalFeedUpdater {
}
}
- feed.setImageUrl(getImageUrl(context, documentFolder));
+ feed.setImageUrl(getImageUrl(documentFolder));
feed.getPreferences().setAutoDownload(false);
feed.getPreferences().setAutoDeleteAction(FeedPreferences.AutoDeleteAction.NO);
@@ -134,7 +135,7 @@ public class LocalFeedUpdater {
* Returns the image URL for the local feed.
*/
@NonNull
- static String getImageUrl(@NonNull Context context, @NonNull DocumentFile documentFolder) {
+ static String getImageUrl(@NonNull DocumentFile documentFolder) {
// look for special file names
for (String iconLocation : PREFERRED_FEED_IMAGE_FILENAMES) {
DocumentFile image = documentFolder.findFile(iconLocation);
@@ -152,17 +153,7 @@ public class LocalFeedUpdater {
}
// use default icon as fallback
- return getDefaultIconUrl(context);
- }
-
- /**
- * Returns the URL of the default icon for a local feed. The URL refers to an app resource file.
- */
- public static String getDefaultIconUrl(Context context) {
- String resourceEntryName = context.getResources().getResourceEntryName(R.raw.local_feed_default_icon);
- return ContentResolver.SCHEME_ANDROID_RESOURCE + "://"
- + context.getPackageName() + "/raw/"
- + resourceEntryName;
+ return Feed.PREFIX_GENERATIVE_COVER + documentFolder.getUri();
}
private static FeedItem feedContainsFile(Feed feed, String filename) {
@@ -220,6 +211,22 @@ public class LocalFeedUpdater {
item.getMedia().setDuration((int) Long.parseLong(durationStr));
item.getMedia().setHasEmbeddedPicture(mediaMetadataRetriever.getEmbeddedPicture() != null);
+
+ try (InputStream inputStream = context.getContentResolver().openInputStream(file.getUri())) {
+ Id3MetadataReader reader = new Id3MetadataReader(new CountingInputStream(inputStream));
+ reader.readInputStream();
+ item.setDescriptionIfLonger(reader.getComment());
+ } catch (IOException | ID3ReaderException e) {
+ Log.d(TAG, "Unable to parse ID3 of " + file.getUri() + ": " + e.getMessage());
+
+ try (InputStream inputStream = context.getContentResolver().openInputStream(file.getUri())) {
+ VorbisCommentMetadataReader reader = new VorbisCommentMetadataReader(inputStream);
+ reader.readInputStream();
+ item.setDescriptionIfLonger(reader.getDescription());
+ } catch (IOException | VorbisCommentReaderException e2) {
+ Log.d(TAG, "Unable to parse vorbis comments of " + file.getUri() + ": " + e2.getMessage());
+ }
+ }
}
private static void reportError(Feed feed, String reasonDetailed) {
@@ -259,4 +266,9 @@ public class LocalFeedUpdater {
// (avoid logging success again if the last update was ok)
return !lastDownloadStatus.isSuccessful();
}
+
+ @FunctionalInterface
+ public interface UpdaterProgressListener {
+ void onLocalFileScanned(int scanned, int totalFiles);
+ }
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/glide/ApGlideModule.java b/core/src/main/java/de/danoeh/antennapod/core/glide/ApGlideModule.java
index 797addcc1..593b683f7 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/glide/ApGlideModule.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/glide/ApGlideModule.java
@@ -42,6 +42,7 @@ public class ApGlideModule extends AppGlideModule {
@Override
public void registerComponents(@NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) {
registry.replace(String.class, InputStream.class, new MetadataRetrieverLoader.Factory(context));
+ registry.append(String.class, InputStream.class, new GenerativePlaceholderImageModelLoader.Factory());
registry.append(String.class, InputStream.class, new ApOkHttpUrlLoader.Factory());
registry.append(String.class, InputStream.class, new NoHttpStringLoader.StreamFactory());
diff --git a/core/src/main/java/de/danoeh/antennapod/core/glide/AudioCoverFetcher.java b/core/src/main/java/de/danoeh/antennapod/core/glide/AudioCoverFetcher.java
index b6b607904..9d270360a 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/glide/AudioCoverFetcher.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/glide/AudioCoverFetcher.java
@@ -16,9 +16,6 @@ import java.io.InputStream;
// see https://github.com/bumptech/glide/issues/699
class AudioCoverFetcher implements DataFetcher<InputStream> {
-
- private static final String TAG = "AudioCoverFetcher";
-
private final String path;
private final Context context;
diff --git a/core/src/main/java/de/danoeh/antennapod/core/glide/GenerativePlaceholderImageModelLoader.java b/core/src/main/java/de/danoeh/antennapod/core/glide/GenerativePlaceholderImageModelLoader.java
new file mode 100644
index 000000000..a2263bc28
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/glide/GenerativePlaceholderImageModelLoader.java
@@ -0,0 +1,139 @@
+package de.danoeh.antennapod.core.glide;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.LinearGradient;
+import android.graphics.Paint;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffColorFilter;
+import android.graphics.Shader;
+import androidx.annotation.NonNull;
+import com.bumptech.glide.Priority;
+import com.bumptech.glide.load.DataSource;
+import com.bumptech.glide.load.Options;
+import com.bumptech.glide.load.data.DataFetcher;
+import com.bumptech.glide.load.model.ModelLoader;
+import com.bumptech.glide.load.model.ModelLoaderFactory;
+import com.bumptech.glide.load.model.MultiModelLoaderFactory;
+import com.bumptech.glide.signature.ObjectKey;
+import de.danoeh.antennapod.model.feed.Feed;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+import java.util.Random;
+
+public final class GenerativePlaceholderImageModelLoader implements ModelLoader<String, InputStream> {
+
+ public static class Factory implements ModelLoaderFactory<String, InputStream> {
+ @NonNull
+ @Override
+ public ModelLoader<String, InputStream> build(@NonNull MultiModelLoaderFactory unused) {
+ return new GenerativePlaceholderImageModelLoader();
+ }
+
+ @Override
+ public void teardown() {
+ // Do nothing.
+ }
+ }
+
+ @Override
+ public LoadData<InputStream> buildLoadData(@NonNull String model, int width, int height, @NonNull Options options) {
+ return new LoadData<>(new ObjectKey(model), new EmbeddedImageFetcher(model, width, height));
+ }
+
+ @Override
+ public boolean handles(@NonNull String model) {
+ return model.startsWith(Feed.PREFIX_GENERATIVE_COVER);
+ }
+
+ static class EmbeddedImageFetcher implements DataFetcher<InputStream> {
+ private static final int[] PALETTES = {0xff78909c, 0xffff6f00, 0xff388e3c,
+ 0xff00838f, 0xff7b1fa2, 0xffb71c1c, 0xff2196f3};
+ private final String model;
+ private final int width;
+ private final int height;
+
+ public EmbeddedImageFetcher(String model, int width, int height) {
+ this.model = model;
+ this.width = width;
+ this.height = height;
+ }
+
+ @Override
+ public void loadData(@NonNull Priority priority, @NonNull DataCallback<? super InputStream> callback) {
+ final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+ final Canvas canvas = new Canvas(bitmap);
+ final Random generator = new Random(model.hashCode());
+ final int lineGridSteps = 4 + generator.nextInt(4);
+ final int slope = width / 4;
+ final float shadowWidth = width * 0.01f;
+ final float lineDistance = ((float) width / (lineGridSteps - 2));
+ final int baseColor = PALETTES[generator.nextInt(PALETTES.length)];
+
+ Paint paint = new Paint();
+ int color = randomShadeOfGrey(generator);
+ paint.setColor(color);
+ paint.setStrokeWidth(lineDistance);
+ paint.setColorFilter(new PorterDuffColorFilter(baseColor, PorterDuff.Mode.MULTIPLY));
+ Paint paintShadow = new Paint();
+ paintShadow.setColor(0xff000000);
+ paintShadow.setStrokeWidth(lineDistance);
+
+ int forcedColorChange = 1 + generator.nextInt(lineGridSteps - 2);
+ for (int i = lineGridSteps - 1; i >= 0; i--) {
+ float linePos = (i - 0.5f) * lineDistance;
+ boolean switchColor = generator.nextFloat() < 0.3f || i == forcedColorChange;
+ if (switchColor) {
+ int newColor = color;
+ while (newColor == color) {
+ newColor = randomShadeOfGrey(generator);
+ }
+ color = newColor;
+ paint.setColor(newColor);
+ canvas.drawLine(linePos + slope + shadowWidth, -slope,
+ linePos - slope + shadowWidth, height + slope, paintShadow);
+ }
+ canvas.drawLine(linePos + slope, -slope,
+ linePos - slope, height + slope, paint);
+ }
+
+ Paint gradientPaint = new Paint();
+ paint.setDither(true);
+ gradientPaint.setShader(new LinearGradient(0, 0, 0, height, 0x00000000, 0x55000000, Shader.TileMode.CLAMP));
+ canvas.drawRect(0, 0, width, height, gradientPaint);
+
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ bitmap.compress(Bitmap.CompressFormat.JPEG, 100, baos);
+ InputStream is = new ByteArrayInputStream(baos.toByteArray());
+ callback.onDataReady(is);
+ }
+
+ private static int randomShadeOfGrey(Random generator) {
+ return 0xff777777 + 0x222222 * generator.nextInt(5);
+ }
+
+ @Override
+ public void cleanup() {
+ // nothing to clean up
+ }
+
+ @Override
+ public void cancel() {
+ // cannot cancel
+ }
+
+ @NonNull
+ @Override
+ public Class<InputStream> getDataClass() {
+ return InputStream.class;
+ }
+
+ @NonNull
+ @Override
+ public DataSource getDataSource() {
+ return DataSource.LOCAL;
+ }
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/preferences/PlaybackPreferences.java b/core/src/main/java/de/danoeh/antennapod/core/preferences/PlaybackPreferences.java
index f0c61403f..b0321bff6 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/preferences/PlaybackPreferences.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/preferences/PlaybackPreferences.java
@@ -45,11 +45,6 @@ public class PlaybackPreferences implements SharedPreferences.OnSharedPreference
= "de.danoeh.antennapod.preferences.currentlyPlayingMedia";
/**
- * True if last played media was streamed.
- */
- private static final String PREF_CURRENT_EPISODE_IS_STREAM = "de.danoeh.antennapod.preferences.lastIsStream";
-
- /**
* True if last played media was a video.
*/
private static final String PREF_CURRENT_EPISODE_IS_VIDEO = "de.danoeh.antennapod.preferences.lastIsVideo";
@@ -113,10 +108,6 @@ public class PlaybackPreferences implements SharedPreferences.OnSharedPreference
return prefs.getLong(PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID, NO_MEDIA_PLAYING);
}
- public static boolean getCurrentEpisodeIsStream() {
- return prefs.getBoolean(PREF_CURRENT_EPISODE_IS_STREAM, true);
- }
-
public static boolean getCurrentEpisodeIsVideo() {
return prefs.getBoolean(PREF_CURRENT_EPISODE_IS_VIDEO, false);
}
@@ -138,7 +129,7 @@ public class PlaybackPreferences implements SharedPreferences.OnSharedPreference
editor.apply();
}
- public static void writeMediaPlaying(Playable playable, PlayerStatus playerStatus, boolean stream) {
+ public static void writeMediaPlaying(Playable playable, PlayerStatus playerStatus) {
Log.d(TAG, "Writing playback preferences");
SharedPreferences.Editor editor = prefs.edit();
@@ -146,7 +137,6 @@ public class PlaybackPreferences implements SharedPreferences.OnSharedPreference
writeNoMediaPlaying();
} else {
editor.putLong(PREF_CURRENTLY_PLAYING_MEDIA_TYPE, playable.getPlayableType());
- editor.putBoolean(PREF_CURRENT_EPISODE_IS_STREAM, stream);
editor.putBoolean(PREF_CURRENT_EPISODE_IS_VIDEO, playable.getMediaType() == MediaType.VIDEO);
if (playable instanceof FeedMedia) {
FeedMedia feedMedia = (FeedMedia) playable;
diff --git a/core/src/main/java/de/danoeh/antennapod/core/preferences/UsageStatistics.java b/core/src/main/java/de/danoeh/antennapod/core/preferences/UsageStatistics.java
index a5b00b08c..9835f9894 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/preferences/UsageStatistics.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/preferences/UsageStatistics.java
@@ -4,8 +4,6 @@ import android.content.Context;
import android.content.SharedPreferences;
import androidx.annotation.NonNull;
-import java.util.Calendar;
-
/**
* Collects statistics about the app usage. The statistics are used to allow on-demand configuration:
* "Looks like you stream a lot. Do you want to toggle the 'Prefer streaming' setting?".
@@ -22,8 +20,7 @@ public class UsageStatistics {
private static final String PREF_DB_NAME = "UsageStatistics";
private static final float MOVING_AVERAGE_WEIGHT = 0.8f;
private static final float MOVING_AVERAGE_BIAS_THRESHOLD = 0.1f;
- private static final long ASK_AGAIN_LATER_DELAY = 1000 * 3600 * 24 * 10; // 10 days
- private static final String SUFFIX_HIDDEN_UNTIL = "_hiddenUntil";
+ private static final String SUFFIX_HIDDEN = "_hidden";
private static SharedPreferences prefs;
public static final StatsAction ACTION_STREAM = new StatsAction("downloadVsStream", 0);
@@ -49,16 +46,16 @@ public class UsageStatistics {
}
public static boolean hasSignificantBiasTo(StatsAction action) {
- final float movingAverage = prefs.getFloat(action.type, 0.5f);
- final long askAfter = prefs.getLong(action.type + SUFFIX_HIDDEN_UNTIL, 0);
- return Math.abs(action.value - movingAverage) < MOVING_AVERAGE_BIAS_THRESHOLD
- && Calendar.getInstance().getTimeInMillis() > askAfter;
+ if (prefs.getBoolean(action.type + SUFFIX_HIDDEN, false)) {
+ return false;
+ } else {
+ final float movingAverage = prefs.getFloat(action.type, 0.5f);
+ return Math.abs(action.value - movingAverage) < MOVING_AVERAGE_BIAS_THRESHOLD;
+ }
}
- public static void askAgainLater(StatsAction action) {
- prefs.edit().putLong(action.type + SUFFIX_HIDDEN_UNTIL,
- Calendar.getInstance().getTimeInMillis() + ASK_AGAIN_LATER_DELAY)
- .apply();
+ public static void doNotAskAgain(StatsAction action) {
+ prefs.edit().putBoolean(action.type + SUFFIX_HIDDEN, true).apply();
}
public static final class StatsAction {
diff --git a/core/src/main/java/de/danoeh/antennapod/core/preferences/UserPreferences.java b/core/src/main/java/de/danoeh/antennapod/core/preferences/UserPreferences.java
index f29b13688..b1bc38ebc 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/preferences/UserPreferences.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/preferences/UserPreferences.java
@@ -24,7 +24,6 @@ import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.ArrayList;
import java.util.Arrays;
-import java.util.Calendar;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
@@ -32,16 +31,11 @@ import java.util.Set;
import java.util.concurrent.TimeUnit;
import de.danoeh.antennapod.core.R;
+import de.danoeh.antennapod.model.feed.FeedCounter;
import de.danoeh.antennapod.model.playback.MediaType;
-import de.danoeh.antennapod.core.feed.SubscriptionsFilter;
-import de.danoeh.antennapod.core.service.download.ProxyConfig;
-import de.danoeh.antennapod.core.storage.APCleanupAlgorithm;
-import de.danoeh.antennapod.core.storage.ExceptFavoriteCleanupAlgorithm;
-import de.danoeh.antennapod.core.storage.APNullCleanupAlgorithm;
-import de.danoeh.antennapod.core.storage.APQueueCleanupAlgorithm;
-import de.danoeh.antennapod.core.storage.EpisodeCleanupAlgorithm;
+import de.danoeh.antennapod.model.feed.SubscriptionsFilter;
+import de.danoeh.antennapod.model.download.ProxyConfig;
import de.danoeh.antennapod.model.feed.SortOrder;
-import de.danoeh.antennapod.core.util.download.AutoUpdateManager;
/**
* Provides access to preferences set by the user in the settings screen. A
@@ -89,7 +83,6 @@ public class UserPreferences {
private static final String PREF_PLAYBACK_SPEED_ARRAY = "prefPlaybackSpeedArray";
public static final String PREF_PAUSE_PLAYBACK_FOR_FOCUS_LOSS = "prefPauseForFocusLoss";
private static final String PREF_RESUME_AFTER_CALL = "prefResumeAfterCall";
- public static final String PREF_VIDEO_BEHAVIOR = "prefVideoBehavior";
private static final String PREF_TIME_RESPECTS_SPEED = "prefPlaybackTimeRespectsSpeed";
public static final String PREF_STREAM_OVER_DOWNLOAD = "prefStreamOverDownload";
@@ -128,12 +121,9 @@ public class UserPreferences {
private static final String PREF_FAST_FORWARD_SECS = "prefFastForwardSecs";
private static final String PREF_REWIND_SECS = "prefRewindSecs";
private static final String PREF_QUEUE_LOCKED = "prefQueueLocked";
- private static final String PREF_LEFT_VOLUME = "prefLeftVolume";
- private static final String PREF_RIGHT_VOLUME = "prefRightVolume";
// Experimental
private static final String PREF_STEREO_TO_MONO = "PrefStereoToMono";
- public static final String PREF_CAST_ENABLED = "prefCast"; //Used for enabling Chromecast support
public static final int EPISODE_CLEANUP_QUEUE = -1;
public static final int EPISODE_CLEANUP_NULL = -2;
public static final int EPISODE_CLEANUP_EXCEPT_FAVORITE = -3;
@@ -147,11 +137,6 @@ public class UserPreferences {
public static final int FEED_ORDER_COUNTER = 0;
public static final int FEED_ORDER_ALPHABETICAL = 1;
public static final int FEED_ORDER_MOST_PLAYED = 3;
- public static final int FEED_COUNTER_SHOW_NEW_UNPLAYED_SUM = 0;
- public static final int FEED_COUNTER_SHOW_NEW = 1;
- public static final int FEED_COUNTER_SHOW_UNPLAYED = 2;
- public static final int FEED_COUNTER_SHOW_NONE = 3;
- public static final int FEED_COUNTER_SHOW_DOWNLOADED = 4;
private static Context context;
private static SharedPreferences prefs;
@@ -253,9 +238,9 @@ public class UserPreferences {
.apply();
}
- public static int getFeedCounterSetting() {
- String value = prefs.getString(PREF_DRAWER_FEED_COUNTER, "" + FEED_COUNTER_SHOW_NEW);
- return Integer.parseInt(value);
+ public static FeedCounter getFeedCounterSetting() {
+ String value = prefs.getString(PREF_DRAWER_FEED_COUNTER, "" + FeedCounter.SHOW_NEW.id);
+ return FeedCounter.fromOrdinal(Integer.parseInt(value));
}
/**
@@ -701,33 +686,22 @@ public class UserPreferences {
.apply();
}
- /**
- * Sets the update interval value.
- */
public static void setUpdateInterval(long hours) {
prefs.edit()
.putString(PREF_UPDATE_INTERVAL, String.valueOf(hours))
.apply();
- // when updating with an interval, we assume the user wants
- // to update *now* and then every 'hours' interval thereafter.
- AutoUpdateManager.restartUpdateAlarm(context);
}
- /**
- * Sets the update interval value.
- */
public static void setUpdateTimeOfDay(int hourOfDay, int minute) {
prefs.edit()
.putString(PREF_UPDATE_INTERVAL, hourOfDay + ":" + minute)
.apply();
- AutoUpdateManager.restartUpdateAlarm(context);
}
- public static void disableAutoUpdate(Context context) {
+ public static void disableAutoUpdate() {
prefs.edit()
.putString(PREF_UPDATE_INTERVAL, "0")
.apply();
- AutoUpdateManager.disableAutoUpdate(context);
}
public static boolean gpodnetNotificationsEnabled() {
@@ -830,10 +804,6 @@ public class UserPreferences {
return getMediaPlayer().equals(PREF_MEDIA_PLAYER_EXOPLAYER);
}
- public static void enableSonic() {
- prefs.edit().putString(PREF_MEDIA_PLAYER, "sonic").apply();
- }
-
public static void enableExoplayer() {
prefs.edit().putString(PREF_MEDIA_PLAYER, PREF_MEDIA_PLAYER_EXOPLAYER).apply();
}
@@ -848,31 +818,6 @@ public class UserPreferences {
.apply();
}
- public static VideoBackgroundBehavior getVideoBackgroundBehavior() {
- switch (prefs.getString(PREF_VIDEO_BEHAVIOR, "pip")) {
- case "stop": return VideoBackgroundBehavior.STOP;
- case "continue": return VideoBackgroundBehavior.CONTINUE_PLAYING;
- case "pip": //Deliberate fall-through
- default: return VideoBackgroundBehavior.PICTURE_IN_PICTURE;
- }
- }
-
- public static EpisodeCleanupAlgorithm getEpisodeCleanupAlgorithm() {
- if (!isEnableAutodownload()) {
- return new APNullCleanupAlgorithm();
- }
- int cleanupValue = getEpisodeCleanupValue();
- if (cleanupValue == EPISODE_CLEANUP_EXCEPT_FAVORITE) {
- return new ExceptFavoriteCleanupAlgorithm();
- } else if (cleanupValue == EPISODE_CLEANUP_QUEUE) {
- return new APQueueCleanupAlgorithm();
- } else if (cleanupValue == EPISODE_CLEANUP_NULL) {
- return new APNullCleanupAlgorithm();
- } else {
- return new APCleanupAlgorithm(cleanupValue);
- }
- }
-
public static int getEpisodeCleanupValue() {
return Integer.parseInt(prefs.getString(PREF_EPISODE_CLEANUP, "" + EPISODE_CLEANUP_NULL));
}
@@ -956,17 +901,6 @@ public class UserPreferences {
return getUpdateTimeOfDay().length == 2;
}
- /**
- * Evaluates whether Cast support (Chromecast, Audio Cast, etc) is enabled on the preferences.
- */
- public static boolean isCastEnabled() {
- return prefs.getBoolean(PREF_CAST_ENABLED, false);
- }
-
- public enum VideoBackgroundBehavior {
- STOP, PICTURE_IN_PICTURE, CONTINUE_PLAYING
- }
-
public enum BackButtonBehavior {
DEFAULT, OPEN_DRAWER, DOUBLE_TAP, SHOW_PROMPT, GO_TO_PAGE
}
@@ -1060,28 +994,7 @@ public class UserPreferences {
.apply();
}
- public static long getUsageCountingDateMillis() {
- return prefs.getLong(PREF_USAGE_COUNTING_DATE, -1);
- }
-
- private static void setUsageCountingDateMillis(long value) {
- prefs.edit().putLong(PREF_USAGE_COUNTING_DATE, value).apply();
- }
-
- public static void resetUsageCountingDate() {
- setUsageCountingDateMillis(Calendar.getInstance().getTimeInMillis());
- }
-
- public static void unsetUsageCountingDate() {
- setUsageCountingDateMillis(-1);
- }
-
public static boolean shouldShowSubscriptionTitle() {
return prefs.getBoolean(PREF_SUBSCRIPTION_TITLE, false);
}
-
- public static void setSubscriptionTitleSetting(boolean showTitle) {
- prefs.edit().putBoolean(PREF_SUBSCRIPTION_TITLE, showTitle).apply();
- }
-
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/receiver/PlayerWidget.java b/core/src/main/java/de/danoeh/antennapod/core/receiver/PlayerWidget.java
index cf0debed2..4e5aff696 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/receiver/PlayerWidget.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/receiver/PlayerWidget.java
@@ -3,44 +3,36 @@ package de.danoeh.antennapod.core.receiver;
import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.content.Context;
-import android.content.Intent;
import android.content.SharedPreferences;
import android.util.Log;
+import de.danoeh.antennapod.core.widget.WidgetUpdaterWorker;
import java.util.Arrays;
-import de.danoeh.antennapod.core.widget.WidgetUpdaterJobService;
-
public class PlayerWidget extends AppWidgetProvider {
private static final String TAG = "PlayerWidget";
public static final String PREFS_NAME = "PlayerWidgetPrefs";
private static final String KEY_ENABLED = "WidgetEnabled";
public static final String KEY_WIDGET_COLOR = "widget_color";
+ public static final String KEY_WIDGET_PLAYBACK_SPEED = "widget_playback_speed";
public static final String KEY_WIDGET_SKIP = "widget_skip";
public static final String KEY_WIDGET_FAST_FORWARD = "widget_fast_forward";
public static final String KEY_WIDGET_REWIND = "widget_rewind";
public static final int DEFAULT_COLOR = 0x00262C31;
@Override
- public void onReceive(Context context, Intent intent) {
- Log.d(TAG, "onReceive");
- super.onReceive(context, intent);
- WidgetUpdaterJobService.performBackgroundUpdate(context);
- }
-
- @Override
public void onEnabled(Context context) {
super.onEnabled(context);
Log.d(TAG, "Widget enabled");
setEnabled(context, true);
- WidgetUpdaterJobService.performBackgroundUpdate(context);
+ WidgetUpdaterWorker.enqueueWork(context);
}
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
Log.d(TAG, "onUpdate() called with: " + "context = [" + context + "], appWidgetManager = ["
+ appWidgetManager + "], appWidgetIds = [" + Arrays.toString(appWidgetIds) + "]");
- WidgetUpdaterJobService.performBackgroundUpdate(context);
+ WidgetUpdaterWorker.enqueueWork(context);
}
@Override
@@ -56,6 +48,7 @@ public class PlayerWidget extends AppWidgetProvider {
for (int appWidgetId : appWidgetIds) {
SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
prefs.edit().remove(KEY_WIDGET_COLOR + appWidgetId).apply();
+ prefs.edit().remove(KEY_WIDGET_PLAYBACK_SPEED + appWidgetId).apply();
prefs.edit().remove(KEY_WIDGET_REWIND + appWidgetId).apply();
prefs.edit().remove(KEY_WIDGET_FAST_FORWARD + appWidgetId).apply();
prefs.edit().remove(KEY_WIDGET_SKIP + appWidgetId).apply();
diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/BasicAuthorizationInterceptor.java b/core/src/main/java/de/danoeh/antennapod/core/service/BasicAuthorizationInterceptor.java
index 667d6afeb..53160e242 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/service/BasicAuthorizationInterceptor.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/service/BasicAuthorizationInterceptor.java
@@ -4,7 +4,7 @@ import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.NonNull;
import de.danoeh.antennapod.core.service.download.DownloadRequest;
-import de.danoeh.antennapod.core.service.download.HttpDownloader;
+import de.danoeh.antennapod.core.service.download.HttpCredentialEncoder;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.util.URIUtil;
import java.io.IOException;
@@ -47,7 +47,9 @@ public class BasicAuthorizationInterceptor implements Interceptor {
if (request.tag() instanceof DownloadRequest) {
DownloadRequest downloadRequest = (DownloadRequest) request.tag();
userInfo = URIUtil.getURIFromRequestUrl(downloadRequest.getSource()).getUserInfo();
- if (TextUtils.isEmpty(userInfo)) {
+ if (TextUtils.isEmpty(userInfo)
+ && (!TextUtils.isEmpty(downloadRequest.getUsername())
+ || !TextUtils.isEmpty(downloadRequest.getPassword()))) {
userInfo = downloadRequest.getUsername() + ":" + downloadRequest.getPassword();
}
} else {
@@ -59,15 +61,15 @@ public class BasicAuthorizationInterceptor implements Interceptor {
return response;
}
- String[] parts = userInfo.split(":");
- if (parts.length != 2) {
+ if (!userInfo.contains(":")) {
Log.d(TAG, "Invalid credentials for '" + request.url() + "'");
return response;
}
+ String username = userInfo.substring(0, userInfo.indexOf(':'));
+ String password = userInfo.substring(userInfo.indexOf(':') + 1);
Log.d(TAG, "Authorization failed, re-trying with ISO-8859-1 encoded credentials");
- String credentials = HttpDownloader.encodeCredentials(parts[0], parts[1], "ISO-8859-1");
- newRequest.header(HEADER_AUTHORIZATION, credentials);
+ newRequest.header(HEADER_AUTHORIZATION, HttpCredentialEncoder.encode(username, password, "ISO-8859-1"));
response = chain.proceed(newRequest.build());
if (response.code() != HttpURLConnection.HTTP_UNAUTHORIZED) {
@@ -75,8 +77,7 @@ public class BasicAuthorizationInterceptor implements Interceptor {
}
Log.d(TAG, "Authorization failed, re-trying with UTF-8 encoded credentials");
- credentials = HttpDownloader.encodeCredentials(parts[0], parts[1], "UTF-8");
- newRequest.header(HEADER_AUTHORIZATION, credentials);
+ newRequest.header(HEADER_AUTHORIZATION, HttpCredentialEncoder.encode(username, password, "UTF-8"));
return chain.proceed(newRequest.build());
}
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/AntennapodHttpClient.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/AntennapodHttpClient.java
index b26c57963..9f1f97bf0 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/service/download/AntennapodHttpClient.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/AntennapodHttpClient.java
@@ -3,23 +3,17 @@ package de.danoeh.antennapod.core.service.download;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.NonNull;
-import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.service.BasicAuthorizationInterceptor;
import de.danoeh.antennapod.core.service.UserAgentInterceptor;
-import de.danoeh.antennapod.core.storage.DBWriter;
+import de.danoeh.antennapod.model.download.ProxyConfig;
import de.danoeh.antennapod.net.ssl.SslClientSetup;
import okhttp3.Cache;
import okhttp3.Credentials;
-import okhttp3.HttpUrl;
import okhttp3.JavaNetCookieJar;
import okhttp3.OkHttpClient;
-import okhttp3.Request;
-import okhttp3.Response;
-import okhttp3.internal.http.StatusLine;
import java.io.File;
import java.net.CookieManager;
import java.net.CookiePolicy;
-import java.net.HttpURLConnection;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.SocketAddress;
@@ -34,6 +28,7 @@ public class AntennapodHttpClient {
private static final int READ_TIMEOUT = 30000;
private static final int MAX_CONNECTIONS = 8;
private static File cacheDirectory;
+ private static ProxyConfig proxyConfig;
private static volatile OkHttpClient httpClient = null;
@@ -69,36 +64,6 @@ public class AntennapodHttpClient {
System.setProperty("http.maxConnections", String.valueOf(MAX_CONNECTIONS));
OkHttpClient.Builder builder = new OkHttpClient.Builder();
-
- // detect 301 Moved permanently and 308 Permanent Redirect
- builder.networkInterceptors().add(chain -> {
- Request request = chain.request();
- Response response = chain.proceed(request);
- if (response.code() == HttpURLConnection.HTTP_MOVED_PERM
- || response.code() == StatusLine.HTTP_PERM_REDIRECT) {
- String location = response.header("Location");
- if (location == null) {
- return response;
- }
- if (location.startsWith("/")) { // URL is not absolute, but relative
- HttpUrl url = request.url();
- location = url.scheme() + "://" + url.host() + location;
- } else if (!location.toLowerCase().startsWith("http://")
- && !location.toLowerCase().startsWith("https://")) {
- // Reference is relative to current path
- HttpUrl url = request.url();
- String path = url.encodedPath();
- String newPath = path.substring(0, path.lastIndexOf("/") + 1) + location;
- location = url.scheme() + "://" + url.host() + newPath;
- }
- try {
- DBWriter.updateFeedDownloadURL(request.url().toString(), location).get();
- } catch (Exception e) {
- Log.e(TAG, Log.getStackTraceString(e));
- }
- }
- return response;
- });
builder.interceptors().add(new BasicAuthorizationInterceptor());
builder.networkInterceptors().add(new UserAgentInterceptor());
@@ -117,14 +82,13 @@ public class AntennapodHttpClient {
builder.followRedirects(true);
builder.followSslRedirects(true);
- ProxyConfig config = UserPreferences.getProxyConfig();
- if (config.type != Proxy.Type.DIRECT && !TextUtils.isEmpty(config.host)) {
- int port = config.port > 0 ? config.port : ProxyConfig.DEFAULT_PORT;
- SocketAddress address = InetSocketAddress.createUnresolved(config.host, port);
- builder.proxy(new Proxy(config.type, address));
- if (!TextUtils.isEmpty(config.username) && config.password != null) {
+ if (proxyConfig != null && proxyConfig.type != Proxy.Type.DIRECT && !TextUtils.isEmpty(proxyConfig.host)) {
+ int port = proxyConfig.port > 0 ? proxyConfig.port : ProxyConfig.DEFAULT_PORT;
+ SocketAddress address = InetSocketAddress.createUnresolved(proxyConfig.host, port);
+ builder.proxy(new Proxy(proxyConfig.type, address));
+ if (!TextUtils.isEmpty(proxyConfig.username) && proxyConfig.password != null) {
builder.proxyAuthenticator((route, response) -> {
- String credentials = Credentials.basic(config.username, config.password);
+ String credentials = Credentials.basic(proxyConfig.username, proxyConfig.password);
return response.request().newBuilder()
.header("Proxy-Authorization", credentials)
.build();
@@ -139,4 +103,8 @@ public class AntennapodHttpClient {
public static void setCacheDirectory(File cacheDirectory) {
AntennapodHttpClient.cacheDirectory = cacheDirectory;
}
+
+ public static void setProxyConfig(ProxyConfig proxyConfig) {
+ AntennapodHttpClient.proxyConfig = proxyConfig;
+ }
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadService.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadService.java
index 3efb8b47b..8c26a6d74 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadService.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadService.java
@@ -21,6 +21,8 @@ import androidx.core.content.ContextCompat;
import de.danoeh.antennapod.core.BuildConfig;
import de.danoeh.antennapod.core.R;
import de.danoeh.antennapod.core.feed.LocalFeedUpdater;
+import de.danoeh.antennapod.core.storage.EpisodeCleanupAlgorithmFactory;
+import de.danoeh.antennapod.model.download.DownloadStatus;
import org.apache.commons.io.FileUtils;
import org.greenrobot.eventbus.EventBus;
@@ -51,7 +53,7 @@ import de.danoeh.antennapod.core.service.download.handler.PostDownloaderTask;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.storage.DBTasks;
import de.danoeh.antennapod.core.storage.DBWriter;
-import de.danoeh.antennapod.core.util.DownloadError;
+import de.danoeh.antennapod.model.download.DownloadError;
/**
* Manages the download of feedfiles in the app. Downloads can be enqueued via the startService intent.
@@ -306,7 +308,11 @@ public class DownloadService extends Service {
private void performLocalFeedRefresh(Downloader downloader, DownloadRequest request) {
try {
Feed feed = DBReader.getFeed(request.getFeedfileId());
- LocalFeedUpdater.updateFeed(feed, DownloadService.this);
+ LocalFeedUpdater.updateFeed(feed, DownloadService.this, (scanned, totalFiles) -> {
+ request.setSize(totalFiles);
+ request.setSoFar(scanned);
+ request.setProgressPercent((int) (100.0 * scanned / totalFiles));
+ });
} catch (Exception e) {
e.printStackTrace();
}
@@ -340,6 +346,9 @@ public class DownloadService extends Service {
// Was stored in the database before and not initiated manually
newEpisodesNotification.showIfNeeded(DownloadService.this, task.getSavedFeed());
}
+ if (downloader.permanentRedirectUrl != null) {
+ DBWriter.updateFeedDownloadURL(request.getSource(), downloader.permanentRedirectUrl);
+ }
} else {
DBWriter.setFeedLastUpdateFailed(request.getFeedfileId(), true);
saveDownloadStatus(task.getDownloadStatus());
@@ -453,7 +462,7 @@ public class DownloadService extends Service {
Log.d(TAG, "Received enqueue request. #requests=" + requests.size());
if (intent.getBooleanExtra(EXTRA_CLEANUP_MEDIA, false)) {
- UserPreferences.getEpisodeCleanupAlgorithm().makeRoomForEpisodes(getApplicationContext(), requests.size());
+ EpisodeCleanupAlgorithmFactory.build().makeRoomForEpisodes(getApplicationContext(), requests.size());
}
for (DownloadRequest request : requests) {
diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadServiceNotification.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadServiceNotification.java
index 44a30da81..96ac08c6d 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadServiceNotification.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadServiceNotification.java
@@ -9,6 +9,8 @@ import android.util.Log;
import androidx.core.app.NotificationCompat;
import de.danoeh.antennapod.core.ClientConfig;
import de.danoeh.antennapod.core.R;
+import de.danoeh.antennapod.core.util.DownloadErrorLabel;
+import de.danoeh.antennapod.model.download.DownloadStatus;
import de.danoeh.antennapod.model.feed.Feed;
import de.danoeh.antennapod.model.feed.FeedMedia;
import de.danoeh.antennapod.core.util.gui.NotificationUtils;
@@ -50,18 +52,41 @@ public class DownloadServiceNotification {
return null;
}
- String contentTitle = context.getString(R.string.download_notification_title);
- String downloadsLeft = (downloads.size() > 0)
+ String contentTitle;
+ if (typeIsOnly(downloads, Feed.FEEDFILETYPE_FEED)) {
+ contentTitle = context.getString(R.string.download_notification_title_feeds);
+ } else if (typeIsOnly(downloads, FeedMedia.FEEDFILETYPE_FEEDMEDIA)) {
+ contentTitle = context.getString(R.string.download_notification_title_episodes);
+ } else {
+ contentTitle = context.getString(R.string.download_notification_title);
+ }
+ String contentText = (downloads.size() > 0)
? context.getResources().getQuantityString(R.plurals.downloads_left, downloads.size(), downloads.size())
: context.getString(R.string.completing);
String bigText = compileNotificationString(downloads);
+ if (!bigText.contains("\n")) {
+ contentText = bigText;
+ }
notificationCompatBuilder.setContentTitle(contentTitle);
- notificationCompatBuilder.setContentText(downloadsLeft);
+ notificationCompatBuilder.setContentText(contentText);
notificationCompatBuilder.setStyle(new NotificationCompat.BigTextStyle().bigText(bigText));
return notificationCompatBuilder.build();
}
+ private boolean typeIsOnly(List<Downloader> downloads, int feedFileType) {
+ for (Downloader downloader : downloads) {
+ if (downloader.cancelled) {
+ continue;
+ }
+ DownloadRequest request = downloader.getDownloadRequest();
+ if (request.getFeedfileType() != feedFileType) {
+ return false;
+ }
+ }
+ return true;
+ }
+
private static String compileNotificationString(List<Downloader> downloads) {
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < downloads.size(); i++) {
@@ -71,24 +96,18 @@ public class DownloadServiceNotification {
}
stringBuilder.append("• ");
DownloadRequest request = downloader.getDownloadRequest();
- switch (request.getFeedfileType()) {
- case Feed.FEEDFILETYPE_FEED:
- if (request.getTitle() != null) {
- stringBuilder.append(request.getTitle());
- }
- break;
- case FeedMedia.FEEDFILETYPE_FEEDMEDIA:
- if (request.getTitle() != null) {
- stringBuilder.append(request.getTitle())
- .append(" (")
- .append(request.getProgressPercent())
- .append("%)");
- }
- break;
- default:
- stringBuilder.append("Unknown: ").append(request.getFeedfileType());
+ if (request.getTitle() != null) {
+ stringBuilder.append(request.getTitle());
+ } else {
+ stringBuilder.append(request.getSource());
+ }
+ if (request.getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA) {
+ stringBuilder.append(" (").append(request.getProgressPercent()).append("%)");
+ } else if (request.getSource().startsWith(Feed.PREFIX_LOCAL_FOLDER)) {
+ stringBuilder.append(" (").append(request.getSoFar())
+ .append("/").append(request.getSize()).append(")");
}
- if (i != downloads.size()) {
+ if (i != downloads.size() - 1) {
stringBuilder.append("\n");
}
}
@@ -118,7 +137,7 @@ public class DownloadServiceNotification {
}
sb.append("• ").append(statuses.get(i).getTitle());
if (statuses.get(i).getReason() != null) {
- sb.append(": ").append(statuses.get(i).getReason().getErrorString(context));
+ sb.append(": ").append(context.getString(DownloadErrorLabel.from(statuses.get(i).getReason())));
}
if (i != statuses.size() - 1) {
sb.append("\n");
diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadStatus.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadStatus.java
deleted file mode 100644
index 8c95dab85..000000000
--- a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadStatus.java
+++ /dev/null
@@ -1,171 +0,0 @@
-package de.danoeh.antennapod.core.service.download;
-
-import android.database.Cursor;
-import androidx.annotation.NonNull;
-
-import java.util.Date;
-
-import de.danoeh.antennapod.model.feed.FeedFile;
-import de.danoeh.antennapod.core.storage.PodDBAdapter;
-import de.danoeh.antennapod.core.util.DownloadError;
-
-/** Contains status attributes for one download */
-public class DownloadStatus {
- /**
- * Downloaders should use this constant for the size attribute if necessary
- * so that the listadapters etc. can react properly.
- */
- public static final int SIZE_UNKNOWN = -1;
-
- // ----------------------------------- ATTRIBUTES STORED IN DB
- /** Unique id for storing the object in database. */
- private long id;
- /**
- * A human-readable string which is shown to the user so that he can
- * identify the download. Should be the title of the item/feed/media or the
- * URL if the download has no other title.
- */
- private final String title;
- private DownloadError reason;
- /**
- * A message which can be presented to the user to give more information.
- * Should be null if Download was successful.
- */
- private String reasonDetailed;
- private boolean successful;
- private Date completionDate;
- private final long feedfileId;
- /**
- * Is used to determine the type of the feedfile even if the feedfile does
- * not exist anymore. The value should be FEEDFILETYPE_FEED,
- * FEEDFILETYPE_FEEDIMAGE or FEEDFILETYPE_FEEDMEDIA
- */
- private final int feedfileType;
- private final boolean initiatedByUser;
-
- // ------------------------------------ NOT STORED IN DB
- private boolean done;
- private boolean cancelled;
-
- public DownloadStatus(@NonNull DownloadRequest request, DownloadError reason, boolean successful, boolean cancelled,
- String reasonDetailed) {
- this(0, request.getTitle(), request.getFeedfileId(), request.getFeedfileType(), successful, cancelled, false,
- reason, new Date(), reasonDetailed, request.isInitiatedByUser());
- }
-
- /** Constructor for creating new completed downloads. */
- public DownloadStatus(@NonNull FeedFile feedfile, String title, DownloadError reason, boolean successful,
- String reasonDetailed, boolean initiatedByUser) {
- this(0, title, feedfile.getId(), feedfile.getTypeAsInt(), successful, false, true, reason, new Date(),
- reasonDetailed, initiatedByUser);
- }
-
- public static DownloadStatus fromCursor(Cursor cursor) {
- int indexId = cursor.getColumnIndex(PodDBAdapter.KEY_ID);
- int indexTitle = cursor.getColumnIndex(PodDBAdapter.KEY_DOWNLOADSTATUS_TITLE);
- int indexFeedFile = cursor.getColumnIndex(PodDBAdapter.KEY_FEEDFILE);
- int indexFileFileType = cursor.getColumnIndex(PodDBAdapter.KEY_FEEDFILETYPE);
- int indexSuccessful = cursor.getColumnIndex(PodDBAdapter.KEY_SUCCESSFUL);
- int indexReason = cursor.getColumnIndex(PodDBAdapter.KEY_REASON);
- int indexCompletionDate = cursor.getColumnIndex(PodDBAdapter.KEY_COMPLETION_DATE);
- int indexReasonDetailed = cursor.getColumnIndex(PodDBAdapter.KEY_REASON_DETAILED);
-
- return new DownloadStatus(cursor.getLong(indexId), cursor.getString(indexTitle), cursor.getLong(indexFeedFile),
- cursor.getInt(indexFileFileType), cursor.getInt(indexSuccessful) > 0, false, true,
- DownloadError.fromCode(cursor.getInt(indexReason)),
- new Date(cursor.getLong(indexCompletionDate)),
- cursor.getString(indexReasonDetailed), false);
- }
-
- private DownloadStatus(long id, String title, long feedfileId, int feedfileType, boolean successful,
- boolean cancelled, boolean done, DownloadError reason, Date completionDate,
- String reasonDetailed, boolean initiatedByUser) {
- this.id = id;
- this.title = title;
- this.feedfileId = feedfileId;
- this.reason = reason;
- this.successful = successful;
- this.cancelled = cancelled;
- this.done = done;
- this.completionDate = (Date) completionDate.clone();
- this.reasonDetailed = reasonDetailed;
- this.feedfileType = feedfileType;
- this.initiatedByUser = initiatedByUser;
- }
-
- @Override
- public String toString() {
- return "DownloadStatus [id=" + id + ", title=" + title + ", reason="
- + reason + ", reasonDetailed=" + reasonDetailed
- + ", successful=" + successful + ", completionDate="
- + completionDate + ", feedfileId=" + feedfileId
- + ", feedfileType=" + feedfileType + ", done=" + done
- + ", cancelled=" + cancelled + "]";
- }
-
- public long getId() {
- return id;
- }
-
- public String getTitle() {
- return title;
- }
-
- public DownloadError getReason() {
- return reason;
- }
-
- public String getReasonDetailed() {
- return reasonDetailed;
- }
-
- public boolean isSuccessful() {
- return successful;
- }
-
- public Date getCompletionDate() {
- return (Date) completionDate.clone();
- }
-
- public long getFeedfileId() {
- return feedfileId;
- }
-
- public int getFeedfileType() {
- return feedfileType;
- }
-
- public boolean isInitiatedByUser() { return initiatedByUser; }
-
- public boolean isDone() {
- return done;
- }
-
- public boolean isCancelled() {
- return cancelled;
- }
-
- public void setSuccessful() {
- this.successful = true;
- this.reason = DownloadError.SUCCESS;
- this.done = true;
- }
-
- public void setFailed(DownloadError reason, String reasonDetailed) {
- this.successful = false;
- this.reason = reason;
- this.reasonDetailed = reasonDetailed;
- this.done = true;
- }
-
- public void setCancelled() {
- this.successful = false;
- this.reason = DownloadError.ERROR_DOWNLOAD_CANCELLED;
- this.done = true;
- this.cancelled = true;
- }
-
- public void setId(long id) {
- this.id = id;
- }
-} \ No newline at end of file
diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/Downloader.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/Downloader.java
index 862852710..22c4e9b87 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/service/download/Downloader.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/Downloader.java
@@ -4,10 +4,12 @@ import android.content.Context;
import android.net.wifi.WifiManager;
import androidx.annotation.NonNull;
+import java.util.Date;
import java.util.concurrent.Callable;
import de.danoeh.antennapod.core.ClientConfig;
import de.danoeh.antennapod.core.R;
+import de.danoeh.antennapod.model.download.DownloadStatus;
/**
* Downloads files
@@ -16,8 +18,8 @@ public abstract class Downloader implements Callable<Downloader> {
private static final String TAG = "Downloader";
private volatile boolean finished;
-
public volatile boolean cancelled;
+ public String permanentRedirectUrl = null;
@NonNull
final DownloadRequest request;
@@ -29,7 +31,8 @@ public abstract class Downloader implements Callable<Downloader> {
this.request = request;
this.request.setStatusMsg(R.string.download_pending);
this.cancelled = false;
- this.result = new DownloadStatus(request, null, false, false, null);
+ this.result = new DownloadStatus(0, request.getTitle(), request.getFeedfileId(), request.getFeedfileType(),
+ false, cancelled, false, null, new Date(), null, request.isInitiatedByUser());
}
protected abstract void download();
diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/HttpCredentialEncoder.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/HttpCredentialEncoder.java
new file mode 100644
index 000000000..c1b63e873
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/HttpCredentialEncoder.java
@@ -0,0 +1,18 @@
+package de.danoeh.antennapod.core.service.download;
+
+import okio.ByteString;
+
+import java.io.UnsupportedEncodingException;
+
+public abstract class HttpCredentialEncoder {
+ public static String encode(String username, String password, String charset) {
+ try {
+ String credentials = username + ":" + password;
+ byte[] bytes = credentials.getBytes(charset);
+ String encoded = ByteString.of(bytes).base64();
+ return "Basic " + encoded;
+ } catch (UnsupportedEncodingException e) {
+ throw new AssertionError(e);
+ }
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/HttpDownloader.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/HttpDownloader.java
index 698278af9..3238ce5f1 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/service/download/HttpDownloader.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/HttpDownloader.java
@@ -5,7 +5,9 @@ import android.text.TextUtils;
import android.util.Log;
import de.danoeh.antennapod.core.util.NetworkUtils;
+import de.danoeh.antennapod.model.download.DownloadStatus;
import okhttp3.CacheControl;
+import okhttp3.internal.http.StatusLine;
import org.apache.commons.io.IOUtils;
import java.io.BufferedInputStream;
@@ -13,18 +15,18 @@ import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
-import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.SocketTimeoutException;
import java.net.URI;
import java.net.UnknownHostException;
+import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import de.danoeh.antennapod.core.R;
import de.danoeh.antennapod.model.feed.FeedMedia;
import de.danoeh.antennapod.parser.feed.util.DateUtils;
-import de.danoeh.antennapod.core.util.DownloadError;
+import de.danoeh.antennapod.model.download.DownloadError;
import de.danoeh.antennapod.core.util.StorageUtils;
import de.danoeh.antennapod.core.util.URIUtil;
import okhttp3.OkHttpClient;
@@ -32,11 +34,9 @@ import okhttp3.Protocol;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
-import okio.ByteString;
public class HttpDownloader extends Downloader {
private static final String TAG = "HttpDownloader";
-
private static final int BUFFER_SIZE = 8 * 1024;
public HttpDownloader(@NonNull DownloadRequest request) {
@@ -54,7 +54,6 @@ public class HttpDownloader extends Downloader {
return;
}
- OkHttpClient httpClient = AntennapodHttpClient.getHttpClient();
RandomAccessFile out = null;
InputStream connection;
ResponseBody responseBody = null;
@@ -72,6 +71,10 @@ public class HttpDownloader extends Downloader {
httpReq.cacheControl(new CacheControl.Builder().noCache().build()); // noStore breaks CDNs
}
+ if (uri.getScheme().equals("http")) {
+ httpReq.addHeader("Upgrade-Insecure-Requests", "1");
+ }
+
if (!TextUtils.isEmpty(request.getLastModified())) {
String lastModified = request.getLastModified();
Date lastModifiedDate = DateUtils.parse(lastModified);
@@ -87,7 +90,6 @@ public class HttpDownloader extends Downloader {
}
}
-
// add range header if necessary
if (fileExists && destination.length() > 0) {
request.setSoFar(destination.length());
@@ -95,22 +97,7 @@ public class HttpDownloader extends Downloader {
Log.d(TAG, "Adding range header: " + request.getSoFar());
}
- Response response;
-
- try {
- response = httpClient.newCall(httpReq.build()).execute();
- } catch (IOException e) {
- Log.e(TAG, e.toString());
- if (e.getMessage().contains("PROTOCOL_ERROR")) {
- httpClient = httpClient.newBuilder()
- .protocols(Collections.singletonList(Protocol.HTTP_1_1))
- .build();
- response = httpClient.newCall(httpReq.build()).execute();
- } else {
- throw e;
- }
- }
-
+ Response response = newCall(httpReq);
responseBody = response.body();
String contentEncodingHeader = response.header("Content-Encoding");
boolean isGzip = false;
@@ -119,64 +106,26 @@ public class HttpDownloader extends Downloader {
}
Log.d(TAG, "Response code is " + response.code());
-
if (!response.isSuccessful() && response.code() == HttpURLConnection.HTTP_NOT_MODIFIED) {
Log.d(TAG, "Feed '" + request.getSource() + "' not modified since last update, Download canceled");
onCancelled();
return;
- }
-
- if (!response.isSuccessful() || response.body() == null) {
- final DownloadError error;
- final String details;
- if (response.code() == HttpURLConnection.HTTP_UNAUTHORIZED) {
- error = DownloadError.ERROR_UNAUTHORIZED;
- details = String.valueOf(response.code());
- } else if (response.code() == HttpURLConnection.HTTP_FORBIDDEN) {
- error = DownloadError.ERROR_FORBIDDEN;
- details = String.valueOf(response.code());
- } else if (response.code() == HttpURLConnection.HTTP_NOT_FOUND) {
- error = DownloadError.ERROR_NOT_FOUND;
- details = String.valueOf(response.code());
- } else {
- error = DownloadError.ERROR_HTTP_DATA_ERROR;
- details = String.valueOf(response.code());
- }
- onFail(error, details);
+ } else if (!response.isSuccessful() || response.body() == null) {
+ callOnFailByResponseCode(response);
return;
- }
-
- if (!StorageUtils.storageAvailable()) {
+ } else if (!StorageUtils.storageAvailable()) {
onFail(DownloadError.ERROR_DEVICE_NOT_FOUND, null);
return;
+ } else if (request.getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA
+ && isContentTypeTextAndSmallerThan100kb(response)) {
+ onFail(DownloadError.ERROR_FILE_TYPE, null);
+ return;
}
-
- // fail with a file type error when the content type is text and
- // the reported content length is less than 100kb (or no length is given)
- if (request.getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA) {
- int contentLength = -1;
- String contentLen = response.header("Content-Length");
- if (contentLen != null) {
- try {
- contentLength = Integer.parseInt(contentLen);
- } catch (NumberFormatException e) {
- e.printStackTrace();
- }
- }
- Log.d(TAG, "content length: " + contentLength);
- String contentType = response.header("Content-Type");
- Log.d(TAG, "content type: " + contentType);
- if (contentType != null && contentType.startsWith("text/") &&
- contentLength < 100 * 1024) {
- onFail(DownloadError.ERROR_FILE_TYPE, null);
- return;
- }
- }
+ checkIfRedirect(response);
connection = new BufferedInputStream(responseBody.byteStream());
String contentRangeHeader = (fileExists) ? response.header("Content-Range") : null;
-
if (fileExists && response.code() == HttpURLConnection.HTTP_PARTIAL
&& !TextUtils.isEmpty(contentRangeHeader)) {
String start = contentRangeHeader.substring("bytes ".length(),
@@ -207,7 +156,6 @@ public class HttpDownloader extends Downloader {
long freeSpace = StorageUtils.getFreeSpaceAvailable();
Log.d(TAG, "Free space is " + freeSpace);
-
if (request.getSize() != DownloadStatus.SIZE_UNKNOWN && request.getSize() > freeSpace) {
onFail(DownloadError.ERROR_NOT_ENOUGH_SPACE, null);
return;
@@ -229,10 +177,10 @@ public class HttpDownloader extends Downloader {
} else {
// check if size specified in the response header is the same as the size of the
// written file. This check cannot be made if compression was used
- if (!isGzip && request.getSize() != DownloadStatus.SIZE_UNKNOWN &&
- request.getSoFar() != request.getSize()) {
- onFail(DownloadError.ERROR_IO_WRONG_SIZE, "Download completed but size: " +
- request.getSoFar() + " does not equal expected size " + request.getSize());
+ if (!isGzip && request.getSize() != DownloadStatus.SIZE_UNKNOWN
+ && request.getSoFar() != request.getSize()) {
+ onFail(DownloadError.ERROR_IO_WRONG_SIZE, "Download completed but size: "
+ + request.getSoFar() + " does not equal expected size " + request.getSize());
return;
} else if (request.getSize() > 0 && request.getSoFar() == 0) {
onFail(DownloadError.ERROR_IO_ERROR, "Download completed, but nothing was read");
@@ -278,14 +226,89 @@ public class HttpDownloader extends Downloader {
}
}
+ private Response newCall(Request.Builder httpReq) throws IOException {
+ OkHttpClient httpClient = AntennapodHttpClient.getHttpClient();
+ try {
+ return httpClient.newCall(httpReq.build()).execute();
+ } catch (IOException e) {
+ Log.e(TAG, e.toString());
+ if (e.getMessage() != null && e.getMessage().contains("PROTOCOL_ERROR")) {
+ // Apparently some servers announce they support SPDY but then actually don't.
+ httpClient = httpClient.newBuilder()
+ .protocols(Collections.singletonList(Protocol.HTTP_1_1))
+ .build();
+ return httpClient.newCall(httpReq.build()).execute();
+ } else {
+ throw e;
+ }
+ }
+ }
+
+ private boolean isContentTypeTextAndSmallerThan100kb(Response response) {
+ int contentLength = -1;
+ String contentLen = response.header("Content-Length");
+ if (contentLen != null) {
+ try {
+ contentLength = Integer.parseInt(contentLen);
+ } catch (NumberFormatException e) {
+ e.printStackTrace();
+ }
+ }
+ Log.d(TAG, "content length: " + contentLength);
+ String contentType = response.header("Content-Type");
+ Log.d(TAG, "content type: " + contentType);
+ return contentType != null && contentType.startsWith("text/") && contentLength < 100 * 1024;
+ }
+
+ private void callOnFailByResponseCode(Response response) {
+ final DownloadError error;
+ final String details;
+ if (response.code() == HttpURLConnection.HTTP_UNAUTHORIZED) {
+ error = DownloadError.ERROR_UNAUTHORIZED;
+ details = String.valueOf(response.code());
+ } else if (response.code() == HttpURLConnection.HTTP_FORBIDDEN) {
+ error = DownloadError.ERROR_FORBIDDEN;
+ details = String.valueOf(response.code());
+ } else if (response.code() == HttpURLConnection.HTTP_NOT_FOUND) {
+ error = DownloadError.ERROR_NOT_FOUND;
+ details = String.valueOf(response.code());
+ } else {
+ error = DownloadError.ERROR_HTTP_DATA_ERROR;
+ details = String.valueOf(response.code());
+ }
+ onFail(error, details);
+ }
+
+ private void checkIfRedirect(Response response) {
+ // detect 301 Moved permanently and 308 Permanent Redirect
+ ArrayList<Response> responses = new ArrayList<>();
+ while (response != null) {
+ responses.add(response);
+ response = response.priorResponse();
+ }
+ if (responses.size() < 2) {
+ return;
+ }
+ Collections.reverse(responses);
+ int firstCode = responses.get(0).code();
+ String firstUrl = responses.get(0).request().url().toString();
+ String secondUrl = responses.get(1).request().url().toString();
+ if (firstCode == HttpURLConnection.HTTP_MOVED_PERM || firstCode == StatusLine.HTTP_PERM_REDIRECT) {
+ Log.d(TAG, "Detected permanent redirect from " + request.getSource() + " to " + secondUrl);
+ permanentRedirectUrl = secondUrl;
+ } else if (secondUrl.equals(firstUrl.replace("http://", "https://"))) {
+ Log.d(TAG, "Treating http->https non-permanent redirect as permanent: " + firstUrl);
+ permanentRedirectUrl = secondUrl;
+ }
+ }
+
private void onSuccess() {
Log.d(TAG, "Download was successful");
result.setSuccessful();
}
private void onFail(DownloadError reason, String reasonDetailed) {
- Log.d(TAG, "onFail() called with: " + "reason = [" + reason + "], " +
- "reasonDetailed = [" + reasonDetailed + "]");
+ Log.d(TAG, "onFail() called with: " + "reason = [" + reason + "], reasonDetailed = [" + reasonDetailed + "]");
result.setFailed(reason, reasonDetailed);
if (request.isDeleteOnFailure()) {
cleanup();
@@ -313,15 +336,4 @@ public class HttpDownloader extends Downloader {
}
}
}
-
- public static String encodeCredentials(String username, String password, String charset) {
- try {
- String credentials = username + ":" + password;
- byte[] bytes = credentials.getBytes(charset);
- String encoded = ByteString.of(bytes).base64();
- return "Basic " + encoded;
- } catch (UnsupportedEncodingException e) {
- throw new AssertionError(e);
- }
- }
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/NewEpisodesNotification.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/NewEpisodesNotification.java
index f7ed049cd..8f955e3c5 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/service/download/NewEpisodesNotification.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/NewEpisodesNotification.java
@@ -17,18 +17,19 @@ import com.bumptech.glide.Glide;
import com.bumptech.glide.request.RequestOptions;
import de.danoeh.antennapod.core.R;
import de.danoeh.antennapod.model.feed.Feed;
+import de.danoeh.antennapod.model.feed.FeedCounter;
import de.danoeh.antennapod.model.feed.FeedPreferences;
import de.danoeh.antennapod.core.glide.ApGlideSettings;
-import de.danoeh.antennapod.core.preferences.UserPreferences;
-import de.danoeh.antennapod.core.storage.PodDBAdapter;
-import de.danoeh.antennapod.core.util.LongIntMap;
import de.danoeh.antennapod.core.util.gui.NotificationUtils;
+import de.danoeh.antennapod.storage.database.PodDBAdapter;
+
+import java.util.Map;
public class NewEpisodesNotification {
private static final String TAG = "NewEpisodesNotification";
private static final String GROUP_KEY = "de.danoeh.antennapod.EPISODES";
- private LongIntMap countersBefore;
+ private Map<Long, Integer> countersBefore;
public NewEpisodesNotification() {
}
@@ -36,7 +37,7 @@ public class NewEpisodesNotification {
public void loadCountersBeforeRefresh() {
PodDBAdapter adapter = PodDBAdapter.getInstance();
adapter.open();
- countersBefore = adapter.getFeedCounters(UserPreferences.FEED_COUNTER_SHOW_NEW);
+ countersBefore = adapter.getFeedCounters(FeedCounter.SHOW_NEW);
adapter.close();
}
@@ -46,7 +47,7 @@ public class NewEpisodesNotification {
return;
}
- int newEpisodesBefore = countersBefore.get(feed.getId());
+ int newEpisodesBefore = countersBefore.containsKey(feed.getId()) ? countersBefore.get(feed.getId()) : 0;
int newEpisodesAfter = getNewEpisodeCount(feed.getId());
Log.d(TAG, "New episodes before: " + newEpisodesBefore + ", after: " + newEpisodesAfter);
@@ -130,7 +131,8 @@ public class NewEpisodesNotification {
private static int getNewEpisodeCount(long feedId) {
PodDBAdapter adapter = PodDBAdapter.getInstance();
adapter.open();
- int episodeCount = adapter.getFeedCounters(UserPreferences.FEED_COUNTER_SHOW_NEW, feedId).get(feedId);
+ Map<Long, Integer> counters = adapter.getFeedCounters(FeedCounter.SHOW_NEW, feedId);
+ int episodeCount = counters.containsKey(feedId) ? counters.get(feedId) : 0;
adapter.close();
return episodeCount;
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedParserTask.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedParserTask.java
index 125804669..dc5893b23 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedParserTask.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedParserTask.java
@@ -1,5 +1,6 @@
package de.danoeh.antennapod.core.service.download.handler;
+import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.NonNull;
import de.danoeh.antennapod.model.feed.Feed;
@@ -7,17 +8,18 @@ import de.danoeh.antennapod.model.feed.FeedItem;
import de.danoeh.antennapod.model.feed.FeedPreferences;
import de.danoeh.antennapod.model.feed.VolumeAdaptionSetting;
import de.danoeh.antennapod.core.service.download.DownloadRequest;
-import de.danoeh.antennapod.core.service.download.DownloadStatus;
+import de.danoeh.antennapod.model.download.DownloadStatus;
import de.danoeh.antennapod.parser.feed.FeedHandler;
import de.danoeh.antennapod.parser.feed.FeedHandlerResult;
import de.danoeh.antennapod.parser.feed.UnsupportedFeedtypeException;
-import de.danoeh.antennapod.core.util.DownloadError;
+import de.danoeh.antennapod.model.download.DownloadError;
import de.danoeh.antennapod.core.util.InvalidFeedException;
import org.xml.sax.SAXException;
import javax.xml.parsers.ParserConfigurationException;
import java.io.File;
import java.io.IOException;
+import java.util.Date;
import java.util.concurrent.Callable;
public class FeedParserTask implements Callable<FeedHandlerResult> {
@@ -28,8 +30,10 @@ public class FeedParserTask implements Callable<FeedHandlerResult> {
public FeedParserTask(DownloadRequest request) {
this.request = request;
- downloadStatus = new DownloadStatus(request, DownloadError.ERROR_REQUEST_ERROR,
- false, false, "Unknown error: Status not set");
+ downloadStatus = new DownloadStatus(
+ 0, request.getTitle(), 0, request.getFeedfileType(), false,
+ false, true, DownloadError.ERROR_REQUEST_ERROR, new Date(),
+ "Unknown error: Status not set", request.isInitiatedByUser());
}
@Override
@@ -51,6 +55,9 @@ public class FeedParserTask implements Callable<FeedHandlerResult> {
result = feedHandler.parseFeed(feed);
Log.d(TAG, feed.getTitle() + " parsed");
checkFeedData(feed);
+ if (TextUtils.isEmpty(feed.getImageUrl())) {
+ feed.setImageUrl(Feed.PREFIX_GENERATIVE_COVER + feed.getDownload_url());
+ }
} catch (SAXException | IOException | ParserConfigurationException e) {
successful = false;
e.printStackTrace();
@@ -83,8 +90,8 @@ public class FeedParserTask implements Callable<FeedHandlerResult> {
successful, reasonDetailed, request.isInitiatedByUser());
return result;
} else {
- downloadStatus = new DownloadStatus(feed, feed.getTitle(), reason, successful,
- reasonDetailed, request.isInitiatedByUser());
+ downloadStatus = new DownloadStatus(feed, feed.getHumanReadableIdentifier(), reason,
+ successful, reasonDetailed, request.isInitiatedByUser());
return null;
}
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedSyncTask.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedSyncTask.java
index 57bcecf2e..5e97c233f 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedSyncTask.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedSyncTask.java
@@ -5,7 +5,7 @@ import android.content.Context;
import androidx.annotation.NonNull;
import de.danoeh.antennapod.model.feed.Feed;
import de.danoeh.antennapod.core.service.download.DownloadRequest;
-import de.danoeh.antennapod.core.service.download.DownloadStatus;
+import de.danoeh.antennapod.model.download.DownloadStatus;
import de.danoeh.antennapod.core.storage.DBTasks;
import de.danoeh.antennapod.parser.feed.FeedHandlerResult;
diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/MediaDownloadedHandler.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/MediaDownloadedHandler.java
index f4f6c5a08..b30f44eec 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/MediaDownloadedHandler.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/MediaDownloadedHandler.java
@@ -13,12 +13,12 @@ import java.util.concurrent.ExecutionException;
import de.danoeh.antennapod.event.UnreadItemsUpdateEvent;
import de.danoeh.antennapod.core.service.download.DownloadRequest;
-import de.danoeh.antennapod.core.service.download.DownloadStatus;
+import de.danoeh.antennapod.model.download.DownloadStatus;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.storage.DBWriter;
import de.danoeh.antennapod.core.sync.queue.SynchronizationQueueSink;
import de.danoeh.antennapod.core.util.ChapterUtils;
-import de.danoeh.antennapod.core.util.DownloadError;
+import de.danoeh.antennapod.model.download.DownloadError;
import de.danoeh.antennapod.model.feed.FeedItem;
import de.danoeh.antennapod.model.feed.FeedMedia;
import de.danoeh.antennapod.net.sync.model.EpisodeAction;
@@ -58,6 +58,9 @@ public class MediaDownloadedHandler implements Runnable {
media.setChapters(ChapterUtils.loadChaptersFromMediaFile(media, context));
}
+ if (media.getItem() != null && media.getItem().getPodcastIndexChapterUrl() != null) {
+ ChapterUtils.loadChaptersFromUrl(media.getItem().getPodcastIndexChapterUrl());
+ }
// Get duration
MediaMetadataRetriever mmr = new MediaMetadataRetriever();
String durationStr = null;
diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/ExoPlayerWrapper.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/ExoPlayerWrapper.java
index 0c64ea2b3..37f49a416 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/ExoPlayerWrapper.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/ExoPlayerWrapper.java
@@ -38,7 +38,7 @@ import de.danoeh.antennapod.core.ClientConfig;
import de.danoeh.antennapod.core.R;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.service.download.AntennapodHttpClient;
-import de.danoeh.antennapod.core.service.download.HttpDownloader;
+import de.danoeh.antennapod.core.service.download.HttpCredentialEncoder;
import de.danoeh.antennapod.core.util.NetworkUtils;
import de.danoeh.antennapod.core.util.playback.IPlayer;
import de.danoeh.antennapod.playback.base.PlaybackServiceMediaPlayer;
@@ -222,7 +222,7 @@ public class ExoPlayerWrapper implements IPlayer {
final HashMap<String, String> requestProperties = new HashMap<>();
requestProperties.put(
"Authorization",
- HttpDownloader.encodeCredentials(user, password, "ISO-8859-1")
+ HttpCredentialEncoder.encode(user, password, "ISO-8859-1")
);
httpDataSourceFactory.setDefaultRequestProperties(requestProperties);
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/LocalPSMP.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/LocalPSMP.java
index db6088d8d..f1b9510d9 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/LocalPSMP.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/LocalPSMP.java
@@ -713,7 +713,6 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer {
return stream;
}
-
/**
* Releases internally used resources. This method should only be called when the object is not used anymore.
*/
@@ -738,16 +737,6 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer {
releaseWifiLockIfNecessary();
}
- /**
- * Releases internally used resources. This method should only be called when the object is not used anymore.
- * This method is executed on an internal executor service.
- */
- @Override
- public void shutdownQuietly() {
- executor.submit(this::shutdown);
- executor.shutdown();
- }
-
@Override
public void setVideoSurface(final SurfaceHolder surface) {
executor.submit(() -> {
@@ -867,8 +856,6 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer {
if (focusChange == AudioManager.AUDIOFOCUS_GAIN && pausedBecauseOfTransientAudiofocusLoss) {
pausedBecauseOfTransientAudiofocusLoss = false;
new PlaybackServiceStarter(context, getPlayable())
- .startWhenPrepared(true)
- .streamIfLastWasStream()
.callEvenIfRunning(false)
.start();
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java
index 76fdc1040..5bee39970 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java
@@ -82,7 +82,6 @@ import de.danoeh.antennapod.core.util.gui.NotificationUtils;
import de.danoeh.antennapod.core.util.playback.PlayableUtils;
import de.danoeh.antennapod.core.util.playback.PlaybackServiceStarter;
import de.danoeh.antennapod.core.widget.WidgetUpdater;
-import de.danoeh.antennapod.model.feed.Chapter;
import de.danoeh.antennapod.model.feed.Feed;
import de.danoeh.antennapod.model.feed.FeedItem;
import de.danoeh.antennapod.model.feed.FeedMedia;
@@ -108,12 +107,8 @@ public class PlaybackService extends MediaBrowserServiceCompat {
private static final String TAG = "PlaybackService";
public static final String EXTRA_PLAYABLE = "PlaybackService.PlayableExtra";
- public static final String EXTRA_SHOULD_STREAM = "extra.de.danoeh.antennapod.core.service.shouldStream";
public static final String EXTRA_ALLOW_STREAM_THIS_TIME = "extra.de.danoeh.antennapod.core.service.allowStream";
public static final String EXTRA_ALLOW_STREAM_ALWAYS = "extra.de.danoeh.antennapod.core.service.allowStreamAlways";
- public static final String EXTRA_START_WHEN_PREPARED = "extra.de.danoeh.antennapod.core.service.startWhenPrepared";
-
- public static final String EXTRA_PREPARE_IMMEDIATELY = "extra.de.danoeh.antennapod.core.service.prepareImmediately";
public static final String ACTION_PLAYER_STATUS_CHANGED = "action.de.danoeh.antennapod.core.service.playerStatusChanged";
private static final String AVRCP_ACTION_PLAYER_STATUS_CHANGED = "com.android.music.playstatechanged";
@@ -522,23 +517,12 @@ public class PlaybackService extends MediaBrowserServiceCompat {
}
} else {
stateManager.validStartCommandWasReceived();
- boolean stream = intent.getBooleanExtra(EXTRA_SHOULD_STREAM, true);
boolean allowStreamThisTime = intent.getBooleanExtra(EXTRA_ALLOW_STREAM_THIS_TIME, false);
boolean allowStreamAlways = intent.getBooleanExtra(EXTRA_ALLOW_STREAM_ALWAYS, false);
- boolean startWhenPrepared = intent.getBooleanExtra(EXTRA_START_WHEN_PREPARED, false);
- boolean prepareImmediately = intent.getBooleanExtra(EXTRA_PREPARE_IMMEDIATELY, false);
sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, 0);
if (allowStreamAlways) {
UserPreferences.setAllowMobileStreaming(true);
}
- boolean localFeed = URLUtil.isContentUrl(playable.getStreamUrl());
- if (stream && !NetworkUtils.isStreamingAllowed() && !allowStreamThisTime && !localFeed) {
- displayStreamingNotAllowedNotification(intent);
- PlaybackPreferences.writeNoMediaPlaying();
- stateManager.stopService();
- return Service.START_NOT_STICKY;
- }
-
Observable.fromCallable(
() -> {
if (playable instanceof FeedMedia) {
@@ -550,15 +534,8 @@ public class PlaybackService extends MediaBrowserServiceCompat {
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
- playableLoaded -> {
- if (!playable.getIdentifier().equals(
- PlaybackPreferences.getCurrentlyPlayingFeedMediaId())) {
- PlaybackPreferences.clearCurrentlyPlayingTemporaryPlaybackSpeed();
- }
- mediaPlayer.playMediaObject(playableLoaded, stream, startWhenPrepared,
- prepareImmediately);
- addPlayableToQueue(playableLoaded);
- }, error -> {
+ loadedPlayable -> startPlaying(loadedPlayable, allowStreamThisTime),
+ error -> {
Log.d(TAG, "Playable was not found. Stopping service.");
error.printStackTrace();
stateManager.stopService();
@@ -741,32 +718,36 @@ public class PlaybackService extends MediaBrowserServiceCompat {
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
- playable -> {
- boolean localFeed = URLUtil.isContentUrl(playable.getStreamUrl());
- if (PlaybackPreferences.getCurrentEpisodeIsStream()
- && !NetworkUtils.isStreamingAllowed() && !localFeed) {
- displayStreamingNotAllowedNotification(
- new PlaybackServiceStarter(this, playable)
- .prepareImmediately(true)
- .startWhenPrepared(true)
- .shouldStream(true)
- .getIntent());
- PlaybackPreferences.writeNoMediaPlaying();
- stateManager.stopService();
- return;
- }
- mediaPlayer.playMediaObject(playable, PlaybackPreferences.getCurrentEpisodeIsStream(),
- true, true);
- stateManager.validStartCommandWasReceived();
- updateNotificationAndMediaSession(playable);
- addPlayableToQueue(playable);
- }, error -> {
+ playable -> startPlaying(playable, false),
+ error -> {
Log.d(TAG, "Playable was not loaded from preferences. Stopping service.");
error.printStackTrace();
stateManager.stopService();
});
}
+ private void startPlaying(Playable playable, boolean allowStreamThisTime) {
+ boolean localFeed = URLUtil.isContentUrl(playable.getStreamUrl());
+ boolean stream = !playable.localFileAvailable() || localFeed;
+ if (stream && !localFeed && !NetworkUtils.isStreamingAllowed() && !allowStreamThisTime) {
+ displayStreamingNotAllowedNotification(
+ new PlaybackServiceStarter(this, playable)
+ .getIntent());
+ PlaybackPreferences.writeNoMediaPlaying();
+ stateManager.stopService();
+ return;
+ }
+
+ if (!playable.getIdentifier().equals(PlaybackPreferences.getCurrentlyPlayingFeedMediaId())) {
+ PlaybackPreferences.clearCurrentlyPlayingTemporaryPlaybackSpeed();
+ }
+
+ mediaPlayer.playMediaObject(playable, stream, true, true);
+ stateManager.validStartCommandWasReceived();
+ updateNotificationAndMediaSession(playable);
+ addPlayableToQueue(playable);
+ }
+
/**
* Called by a mediaplayer Activity as soon as it has prepared its
* mediaplayer.
@@ -792,7 +773,7 @@ public class PlaybackService extends MediaBrowserServiceCompat {
@Override
public WidgetUpdater.WidgetState requestWidgetState() {
return new WidgetUpdater.WidgetState(getPlayable(), getStatus(),
- getCurrentPosition(), getDuration(), getCurrentPlaybackSpeed(), isCasting());
+ getCurrentPosition(), getDuration(), getCurrentPlaybackSpeed());
}
@Override
@@ -814,7 +795,7 @@ public class PlaybackService extends MediaBrowserServiceCompat {
switch (newInfo.playerStatus) {
case INITIALIZED:
PlaybackPreferences.writeMediaPlaying(mediaPlayer.getPSMPInfo().playable,
- mediaPlayer.getPSMPInfo().playerStatus, mediaPlayer.isStreaming());
+ mediaPlayer.getPSMPInfo().playerStatus);
updateNotificationAndMediaSession(newInfo.playable);
break;
case PREPARED:
@@ -1005,7 +986,7 @@ public class PlaybackService extends MediaBrowserServiceCompat {
if (!UserPreferences.isFollowQueue()) {
Log.d(TAG, "getNextInQueue(), but follow queue is not enabled.");
- PlaybackPreferences.writeMediaPlaying(nextItem.getMedia(), PlayerStatus.STOPPED, false);
+ PlaybackPreferences.writeMediaPlaying(nextItem.getMedia(), PlayerStatus.STOPPED);
updateNotificationAndMediaSession(nextItem.getMedia());
return null;
}
@@ -1014,9 +995,6 @@ public class PlaybackService extends MediaBrowserServiceCompat {
&& UserPreferences.isFollowQueue() && !nextItem.getFeed().isLocalFeed()) {
displayStreamingNotAllowedNotification(
new PlaybackServiceStarter(this, nextItem.getMedia())
- .prepareImmediately(true)
- .startWhenPrepared(true)
- .shouldStream(true)
.getIntent());
PlaybackPreferences.writeNoMediaPlaying();
stateManager.stopService();
@@ -1112,7 +1090,7 @@ public class PlaybackService extends MediaBrowserServiceCompat {
|| autoSkipped
|| (skipped && !UserPreferences.shouldSkipKeepEpisode())) {
// only mark the item as played if we're not keeping it anyways
- DBWriter.markItemPlayed(item, FeedItem.PLAYED, ended);
+ DBWriter.markItemPlayed(item, FeedItem.PLAYED, ended || (skipped && smartMarkAsPlayed));
// don't know if it actually matters to not autodownload when smart mark as played is triggered
DBWriter.removeQueueItem(PlaybackService.this, ended, item);
// Delete episode if enabled
@@ -1684,24 +1662,16 @@ public class PlaybackService extends MediaBrowserServiceCompat {
mediaPlayer.setStartWhenPrepared(s);
}
-
public void seekTo(final int t) {
mediaPlayer.seekTo(t);
+ EventBus.getDefault().post(new PlaybackPositionEvent(t, getDuration()));
}
-
private void seekDelta(final int d) {
mediaPlayer.seekDelta(d);
}
/**
- * Seek to the start of the specified chapter.
- */
- public void seekToChapter(Chapter c) {
- seekTo((int) c.getStart());
- }
-
- /**
* call getDuration() on mediaplayer or return INVALID_TIME if player is in
* an invalid state.
*/
@@ -1805,8 +1775,7 @@ public class PlaybackService extends MediaBrowserServiceCompat {
Log.d(TAG, "onPlayFromMediaId: mediaId: " + mediaId + " extras: " + extras.toString());
FeedMedia p = DBReader.getFeedMedia(Long.parseLong(mediaId));
if (p != null) {
- mediaPlayer.playMediaObject(p, !p.localFileAvailable(), true, true);
- addPlayableToQueue(p);
+ startPlaying(p, false);
}
}
@@ -1814,11 +1783,10 @@ public class PlaybackService extends MediaBrowserServiceCompat {
public void onPlayFromSearch(String query, Bundle extras) {
Log.d(TAG, "onPlayFromSearch query=" + query + " extras=" + extras.toString());
- List<FeedItem> results = FeedSearcher.searchFeedItems(getBaseContext(), query, 0);
+ List<FeedItem> results = FeedSearcher.searchFeedItems(query, 0);
if (results.size() > 0 && results.get(0).getMedia() != null) {
FeedMedia media = results.get(0).getMedia();
- mediaPlayer.playMediaObject(media, !media.localFileAvailable(), true, true);
- addPlayableToQueue(media);
+ startPlaying(media, false);
return;
}
onPlay();
diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceStateManager.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceStateManager.java
index 83c065e0e..addc6b996 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceStateManager.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceStateManager.java
@@ -36,10 +36,6 @@ class PlaybackServiceStateManager {
isInForeground = false;
}
- boolean isInForeground() {
- return isInForeground;
- }
-
boolean hasReceivedValidStartCommand() {
return hasReceivedValidStartCommand;
}
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 52638ca77..c410376c2 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
@@ -76,7 +76,7 @@ public class AutomaticDownloadAlgorithm {
int autoDownloadableEpisodes = candidates.size();
int downloadedEpisodes = DBReader.getNumberOfDownloadedEpisodes();
- int deletedEpisodes = UserPreferences.getEpisodeCleanupAlgorithm()
+ int deletedEpisodes = EpisodeCleanupAlgorithmFactory.build()
.makeRoomForEpisodes(context, autoDownloadableEpisodes);
boolean cacheIsUnlimited =
UserPreferences.getEpisodeCacheSize() == UserPreferences.getEpisodeCacheSizeUnlimited();
diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java b/core/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java
index 81f3af7df..62a461dfe 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java
@@ -21,15 +21,16 @@ import de.danoeh.antennapod.model.feed.FeedItem;
import de.danoeh.antennapod.model.feed.FeedItemFilter;
import de.danoeh.antennapod.model.feed.FeedMedia;
import de.danoeh.antennapod.model.feed.FeedPreferences;
-import de.danoeh.antennapod.core.feed.SubscriptionsFilter;
+import de.danoeh.antennapod.model.feed.SubscriptionsFilter;
import de.danoeh.antennapod.core.preferences.UserPreferences;
-import de.danoeh.antennapod.core.service.download.DownloadStatus;
-import de.danoeh.antennapod.core.storage.mapper.ChapterCursorMapper;
-import de.danoeh.antennapod.core.storage.mapper.FeedCursorMapper;
-import de.danoeh.antennapod.core.storage.mapper.FeedItemCursorMapper;
-import de.danoeh.antennapod.core.storage.mapper.FeedMediaCursorMapper;
-import de.danoeh.antennapod.core.storage.mapper.FeedPreferencesCursorMapper;
-import de.danoeh.antennapod.core.util.LongIntMap;
+import de.danoeh.antennapod.model.download.DownloadStatus;
+import de.danoeh.antennapod.storage.database.PodDBAdapter;
+import de.danoeh.antennapod.storage.database.mapper.DownloadStatusCursorMapper;
+import de.danoeh.antennapod.storage.database.mapper.ChapterCursorMapper;
+import de.danoeh.antennapod.storage.database.mapper.FeedCursorMapper;
+import de.danoeh.antennapod.storage.database.mapper.FeedItemCursorMapper;
+import de.danoeh.antennapod.storage.database.mapper.FeedMediaCursorMapper;
+import de.danoeh.antennapod.storage.database.mapper.FeedPreferencesCursorMapper;
import de.danoeh.antennapod.core.util.LongList;
import de.danoeh.antennapod.core.util.comparator.DownloadStatusComparator;
import de.danoeh.antennapod.core.util.comparator.FeedItemPubdateComparator;
@@ -454,7 +455,7 @@ public final class DBReader {
try (Cursor cursor = adapter.getDownloadLogCursor(DOWNLOAD_LOG_SIZE)) {
List<DownloadStatus> downloadLog = new ArrayList<>(cursor.getCount());
while (cursor.moveToNext()) {
- downloadLog.add(DownloadStatus.fromCursor(cursor));
+ downloadLog.add(DownloadStatusCursorMapper.convert(cursor));
}
Collections.sort(downloadLog, new DownloadStatusComparator());
return downloadLog;
@@ -478,7 +479,7 @@ public final class DBReader {
try (Cursor cursor = adapter.getDownloadLog(Feed.FEEDFILETYPE_FEED, feedId)) {
List<DownloadStatus> downloadLog = new ArrayList<>(cursor.getCount());
while (cursor.moveToNext()) {
- downloadLog.add(DownloadStatus.fromCursor(cursor));
+ downloadLog.add(DownloadStatusCursorMapper.convert(cursor));
}
Collections.sort(downloadLog, new DownloadStatusComparator());
return downloadLog;
@@ -774,26 +775,56 @@ public final class DBReader {
}
}
+ public static class MonthlyStatisticsItem {
+ public int year = 0;
+ public int month = 0;
+ public long timePlayed = 0;
+ }
+
+ @NonNull
+ public static List<MonthlyStatisticsItem> getMonthlyTimeStatistics() {
+ List<MonthlyStatisticsItem> months = new ArrayList<>();
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ try (Cursor cursor = adapter.getMonthlyStatisticsCursor()) {
+ int indexMonth = cursor.getColumnIndexOrThrow("month");
+ int indexYear = cursor.getColumnIndexOrThrow("year");
+ int indexTotalDuration = cursor.getColumnIndexOrThrow("total_duration");
+ while (cursor.moveToNext()) {
+ MonthlyStatisticsItem item = new MonthlyStatisticsItem();
+ item.month = Integer.parseInt(cursor.getString(indexMonth));
+ item.year = Integer.parseInt(cursor.getString(indexYear));
+ item.timePlayed = cursor.getLong(indexTotalDuration);
+ months.add(item);
+ }
+ }
+ adapter.close();
+ return months;
+ }
+
+ public static class StatisticsResult {
+ public List<StatisticsItem> feedTime = new ArrayList<>();
+ public long oldestDate = System.currentTimeMillis();
+ }
+
/**
* Searches the DB for statistics.
*
* @return The list of statistics objects
*/
@NonNull
- public static List<StatisticsItem> getStatistics() {
+ public static StatisticsResult getStatistics(boolean includeMarkedAsPlayed,
+ long timeFilterFrom, long timeFilterTo) {
PodDBAdapter adapter = PodDBAdapter.getInstance();
adapter.open();
- List<StatisticsItem> feedTime = new ArrayList<>();
-
+ StatisticsResult result = new StatisticsResult();
List<Feed> feeds = getFeedList();
for (Feed feed : feeds) {
- long feedPlayedTimeCountAll = 0;
long feedPlayedTime = 0;
long feedTotalTime = 0;
long episodes = 0;
long episodesStarted = 0;
- long episodesStartedIncludingMarked = 0;
long totalDownloadSize = 0;
long episodesDownloadCount = 0;
List<FeedItem> items = getFeed(feed.getId()).getItems();
@@ -803,20 +834,22 @@ public final class DBReader {
continue;
}
- feedPlayedTime += media.getPlayedDuration() / 1000;
-
- if (item.isPlayed()) {
- feedPlayedTimeCountAll += media.getDuration() / 1000;
- } else {
- feedPlayedTimeCountAll += media.getPosition() / 1000;
+ if (media.getLastPlayedTime() > 0 && media.getPlayedDuration() != 0) {
+ result.oldestDate = Math.min(result.oldestDate, media.getLastPlayedTime());
}
-
- if (media.getPlaybackCompletionDate() != null || media.getPlayedDuration() > 0) {
- episodesStarted++;
+ if (media.getLastPlayedTime() >= timeFilterFrom
+ && media.getLastPlayedTime() <= timeFilterTo) {
+ if (media.getPlayedDuration() != 0) {
+ feedPlayedTime += media.getPlayedDuration() / 1000;
+ } else if (includeMarkedAsPlayed && item.isPlayed()) {
+ feedPlayedTime += media.getDuration() / 1000;
+ }
}
- if (item.isPlayed() || media.getPosition() != 0) {
- episodesStartedIncludingMarked++;
+ boolean markedAsStarted = item.isPlayed() || media.getPosition() != 0;
+ boolean hasStatistics = media.getPlaybackCompletionDate() != null || media.getPlayedDuration() > 0;
+ if (hasStatistics || (includeMarkedAsPlayed && markedAsStarted)) {
+ episodesStarted++;
}
feedTotalTime += media.getDuration() / 1000;
@@ -828,13 +861,12 @@ public final class DBReader {
episodes++;
}
- feedTime.add(new StatisticsItem(
- feed, feedTotalTime, feedPlayedTime, feedPlayedTimeCountAll, episodes,
- episodesStarted, episodesStartedIncludingMarked, totalDownloadSize, episodesDownloadCount));
+ result.feedTime.add(new StatisticsItem(feed, feedTotalTime, feedPlayedTime, episodes,
+ episodesStarted, totalDownloadSize, episodesDownloadCount));
}
adapter.close();
- return feedTime;
+ return result;
}
/**
@@ -848,7 +880,7 @@ public final class DBReader {
PodDBAdapter adapter = PodDBAdapter.getInstance();
adapter.open();
- final LongIntMap feedCounters = adapter.getFeedCounters();
+ final Map<Long, Integer> feedCounters = adapter.getFeedCounters(UserPreferences.getFeedCounterSetting());
SubscriptionsFilter subscriptionsFilter = UserPreferences.getSubscriptionsFilter();
List<Feed> feeds = subscriptionsFilter.filter(getFeedList(adapter), feedCounters);
@@ -856,8 +888,8 @@ public final class DBReader {
int feedOrder = UserPreferences.getFeedOrder();
if (feedOrder == UserPreferences.FEED_ORDER_COUNTER) {
comparator = (lhs, rhs) -> {
- long counterLhs = feedCounters.get(lhs.getId());
- long counterRhs = feedCounters.get(rhs.getId());
+ long counterLhs = feedCounters.containsKey(lhs.getId()) ? feedCounters.get(lhs.getId()) : 0;
+ long counterRhs = feedCounters.containsKey(rhs.getId()) ? feedCounters.get(rhs.getId()) : 0;
if (counterLhs > counterRhs) {
// reverse natural order: podcast with most unplayed episodes first
return -1;
@@ -880,11 +912,11 @@ public final class DBReader {
}
};
} else if (feedOrder == UserPreferences.FEED_ORDER_MOST_PLAYED) {
- final LongIntMap playedCounters = adapter.getPlayedEpisodesCounters();
+ final Map<Long, Integer> playedCounters = adapter.getPlayedEpisodesCounters();
comparator = (lhs, rhs) -> {
- long counterLhs = playedCounters.get(lhs.getId());
- long counterRhs = playedCounters.get(rhs.getId());
+ long counterLhs = playedCounters.containsKey(lhs.getId()) ? playedCounters.get(lhs.getId()) : 0;
+ long counterRhs = playedCounters.containsKey(rhs.getId()) ? playedCounters.get(rhs.getId()) : 0;
if (counterLhs > counterRhs) {
// podcast with most played episodes first
return -1;
@@ -912,8 +944,8 @@ public final class DBReader {
Map<String, NavDrawerData.TagDrawerItem> folders = new HashMap<>();
for (Feed feed : feeds) {
for (String tag : feed.getPreferences().getTags()) {
- NavDrawerData.FeedDrawerItem drawerItem = new NavDrawerData.FeedDrawerItem(feed, feed.getId(),
- feedCounters.get(feed.getId()));
+ int counter = feedCounters.containsKey(feed.getId()) ? feedCounters.get(feed.getId()) : 0;
+ NavDrawerData.FeedDrawerItem drawerItem = new NavDrawerData.FeedDrawerItem(feed, feed.getId(), counter);
if (FeedPreferences.TAG_ROOT.equals(tag)) {
items.add(drawerItem);
continue;
@@ -934,7 +966,7 @@ public final class DBReader {
items.addAll(foldersSorted);
NavDrawerData result = new NavDrawerData(items, queueSize, numNewItems, numDownloadedItems,
- feedCounters, UserPreferences.getEpisodeCleanupAlgorithm().getReclaimableItems());
+ feedCounters, EpisodeCleanupAlgorithmFactory.build().getReclaimableItems());
adapter.close();
return result;
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java b/core/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java
index 2710a9112..e9fe3af1a 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java
@@ -13,6 +13,8 @@ import androidx.annotation.VisibleForTesting;
import de.danoeh.antennapod.core.service.download.DownloadRequest;
import de.danoeh.antennapod.core.service.download.DownloadRequestCreator;
import de.danoeh.antennapod.core.service.download.DownloadService;
+import de.danoeh.antennapod.storage.database.PodDBAdapter;
+import de.danoeh.antennapod.storage.database.mapper.FeedCursorMapper;
import org.greenrobot.eventbus.EventBus;
import java.util.ArrayList;
@@ -32,11 +34,10 @@ import de.danoeh.antennapod.event.FeedItemEvent;
import de.danoeh.antennapod.event.FeedListUpdateEvent;
import de.danoeh.antennapod.event.MessageEvent;
import de.danoeh.antennapod.core.preferences.UserPreferences;
-import de.danoeh.antennapod.core.service.download.DownloadStatus;
-import de.danoeh.antennapod.core.storage.mapper.FeedCursorMapper;
+import de.danoeh.antennapod.model.download.DownloadStatus;
import de.danoeh.antennapod.core.sync.SyncService;
import de.danoeh.antennapod.core.sync.queue.SynchronizationQueueSink;
-import de.danoeh.antennapod.core.util.DownloadError;
+import de.danoeh.antennapod.model.download.DownloadError;
import de.danoeh.antennapod.core.util.LongList;
import de.danoeh.antennapod.core.util.comparator.FeedItemPubdateComparator;
import de.danoeh.antennapod.model.feed.Feed;
@@ -176,7 +177,7 @@ public final class DBTasks {
media.setDownloaded(false);
media.setFile_url(null);
DBWriter.setFeedMedia(media);
- EventBus.getDefault().post(FeedItemEvent.deletedMedia(media.getItem()));
+ EventBus.getDefault().post(FeedItemEvent.updated(media.getItem()));
EventBus.getDefault().post(new MessageEvent(context.getString(R.string.error_file_not_found)));
}
@@ -227,39 +228,10 @@ public final class DBTasks {
* @param context Used for accessing the DB.
*/
public static void performAutoCleanup(final Context context) {
- UserPreferences.getEpisodeCleanupAlgorithm().performCleanup(context);
+ EpisodeCleanupAlgorithmFactory.build().performCleanup(context);
}
- /**
- * Returns the successor of a FeedItem in the queue.
- *
- * @param itemId ID of the FeedItem
- * @param queue Used for determining the successor of the item. If this parameter is null, the method will load
- * the queue from the database in the same thread.
- * @return Successor of the FeedItem or null if the FeedItem is not in the queue or has no successor.
- */
- public static FeedItem getQueueSuccessorOfItem(final long itemId, List<FeedItem> queue) {
- FeedItem result = null;
- if (queue == null) {
- queue = DBReader.getQueue();
- }
- if (queue != null) {
- Iterator<FeedItem> iterator = queue.iterator();
- while (iterator.hasNext()) {
- FeedItem item = iterator.next();
- if (item.getId() == itemId) {
- if (iterator.hasNext()) {
- result = iterator.next();
- }
- break;
- }
- }
- }
- return result;
- }
-
- private static Feed searchFeedByIdentifyingValueOrID(PodDBAdapter adapter,
- Feed feed) {
+ private static Feed searchFeedByIdentifyingValueOrID(Feed feed) {
if (feed.getId() != 0) {
return DBReader.getFeed(feed.getId());
} else {
@@ -322,7 +294,7 @@ public final class DBTasks {
adapter.open();
// Look up feed in the feedslist
- final Feed savedFeed = searchFeedByIdentifyingValueOrID(adapter, newFeed);
+ final Feed savedFeed = searchFeedByIdentifyingValueOrID(newFeed);
if (savedFeed == null) {
Log.d(TAG, "Found no existing Feed with title "
+ newFeed.getTitle() + ". Adding as new one.");
@@ -456,7 +428,7 @@ public final class DBTasks {
if (savedFeed == null) {
DBWriter.addNewFeed(context, newFeed).get();
// Update with default values that are set in database
- resultFeed = searchFeedByIdentifyingValueOrID(adapter, newFeed);
+ resultFeed = searchFeedByIdentifyingValueOrID(newFeed);
} else {
DBWriter.setCompleteFeed(savedFeed).get();
}
@@ -487,15 +459,13 @@ public final class DBTasks {
/**
* Searches the FeedItems of a specific Feed for a given string.
*
- * @param context Used for accessing the DB.
* @param feedID The id of the feed whose items should be searched.
* @param query The search string.
* @return A FutureTask object that executes the search request
* and returns the search result as a List of FeedItems.
*/
- public static FutureTask<List<FeedItem>> searchFeedItems(final Context context,
- final long feedID, final String query) {
- return new FutureTask<>(new QueryTask<List<FeedItem>>(context) {
+ public static FutureTask<List<FeedItem>> searchFeedItems(final long feedID, final String query) {
+ return new FutureTask<>(new QueryTask<List<FeedItem>>() {
@Override
public void execute(PodDBAdapter adapter) {
Cursor searchResult = adapter.searchItems(feedID, query);
@@ -507,8 +477,8 @@ public final class DBTasks {
});
}
- public static FutureTask<List<Feed>> searchFeeds(final Context context, final String query) {
- return new FutureTask<>(new QueryTask<List<Feed>>(context) {
+ public static FutureTask<List<Feed>> searchFeeds(final String query) {
+ return new FutureTask<>(new QueryTask<List<Feed>>() {
@Override
public void execute(PodDBAdapter adapter) {
Cursor cursor = adapter.searchFeeds(query);
@@ -533,7 +503,7 @@ public final class DBTasks {
abstract static class QueryTask<T> implements Callable<T> {
private T result;
- public QueryTask(Context context) {
+ public QueryTask() {
}
@Override
diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java b/core/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java
index af57cbdae..df4094590 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java
@@ -9,11 +9,11 @@ import androidx.annotation.Nullable;
import androidx.core.app.NotificationManagerCompat;
import de.danoeh.antennapod.core.service.download.DownloadService;
+import de.danoeh.antennapod.storage.database.PodDBAdapter;
import org.greenrobot.eventbus.EventBus;
import java.io.File;
import java.util.ArrayList;
-import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Locale;
@@ -35,7 +35,7 @@ import de.danoeh.antennapod.event.UnreadItemsUpdateEvent;
import de.danoeh.antennapod.core.feed.FeedEvent;
import de.danoeh.antennapod.core.preferences.PlaybackPreferences;
import de.danoeh.antennapod.core.preferences.UserPreferences;
-import de.danoeh.antennapod.core.service.download.DownloadStatus;
+import de.danoeh.antennapod.model.download.DownloadStatus;
import de.danoeh.antennapod.core.service.playback.PlaybackService;
import de.danoeh.antennapod.core.sync.queue.SynchronizationQueueSink;
import de.danoeh.antennapod.core.util.FeedItemPermutors;
@@ -142,7 +142,7 @@ public class DBWriter {
.build();
SynchronizationQueueSink.enqueueEpisodeActionIfSynchronizationIsActive(context, action);
}
- EventBus.getDefault().post(FeedItemEvent.deletedMedia(Collections.singletonList(media.getItem())));
+ EventBus.getDefault().post(FeedItemEvent.updated(media.getItem()));
return true;
}
@@ -550,7 +550,7 @@ public class DBWriter {
adapter.addFavoriteItem(item);
adapter.close();
item.addTag(FeedItem.TAG_FAVORITE);
- EventBus.getDefault().post(FavoritesEvent.added(item));
+ EventBus.getDefault().post(new FavoritesEvent());
EventBus.getDefault().post(FeedItemEvent.updated(item));
});
}
@@ -561,7 +561,7 @@ public class DBWriter {
adapter.removeFavoriteItem(item);
adapter.close();
item.removeTag(FeedItem.TAG_FAVORITE);
- EventBus.getDefault().post(FavoritesEvent.removed(item));
+ EventBus.getDefault().post(new FavoritesEvent());
EventBus.getDefault().post(FeedItemEvent.updated(item));
});
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/DatabaseExporter.java b/core/src/main/java/de/danoeh/antennapod/core/storage/DatabaseExporter.java
index a0fac0c74..d4a863b8b 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/storage/DatabaseExporter.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/storage/DatabaseExporter.java
@@ -8,6 +8,7 @@ import android.os.ParcelFileDescriptor;
import android.text.format.Formatter;
import android.util.Log;
import de.danoeh.antennapod.core.R;
+import de.danoeh.antennapod.storage.database.PodDBAdapter;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
@@ -26,7 +27,7 @@ public class DatabaseExporter {
ParcelFileDescriptor pfd = null;
FileOutputStream fileOutputStream = null;
try {
- pfd = context.getContentResolver().openFileDescriptor(uri, "w");
+ pfd = context.getContentResolver().openFileDescriptor(uri, "wt");
fileOutputStream = new FileOutputStream(pfd.getFileDescriptor());
exportToStream(fileOutputStream, context);
} catch (IOException e) {
diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/EpisodeCleanupAlgorithmFactory.java b/core/src/main/java/de/danoeh/antennapod/core/storage/EpisodeCleanupAlgorithmFactory.java
new file mode 100644
index 000000000..123ccba3f
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/storage/EpisodeCleanupAlgorithmFactory.java
@@ -0,0 +1,22 @@
+package de.danoeh.antennapod.core.storage;
+
+import de.danoeh.antennapod.core.preferences.UserPreferences;
+
+public abstract class EpisodeCleanupAlgorithmFactory {
+ public static EpisodeCleanupAlgorithm build() {
+ if (!UserPreferences.isEnableAutodownload()) {
+ return new APNullCleanupAlgorithm();
+ }
+ int cleanupValue = UserPreferences.getEpisodeCleanupValue();
+ switch (cleanupValue) {
+ case UserPreferences.EPISODE_CLEANUP_EXCEPT_FAVORITE:
+ return new ExceptFavoriteCleanupAlgorithm();
+ case UserPreferences.EPISODE_CLEANUP_QUEUE:
+ return new APQueueCleanupAlgorithm();
+ case UserPreferences.EPISODE_CLEANUP_NULL:
+ return new APNullCleanupAlgorithm();
+ default:
+ return new APCleanupAlgorithm(cleanupValue);
+ }
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/FeedSearcher.java b/core/src/main/java/de/danoeh/antennapod/core/storage/FeedSearcher.java
index c3dd52b49..68ce7b7ef 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/storage/FeedSearcher.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/storage/FeedSearcher.java
@@ -1,6 +1,5 @@
package de.danoeh.antennapod.core.storage;
-import android.content.Context;
import androidx.annotation.NonNull;
import de.danoeh.antennapod.model.feed.Feed;
import de.danoeh.antennapod.model.feed.FeedItem;
@@ -19,9 +18,9 @@ public class FeedSearcher {
}
@NonNull
- public static List<FeedItem> searchFeedItems(final Context context, final String query, final long selectedFeed) {
+ public static List<FeedItem> searchFeedItems(final String query, final long selectedFeed) {
try {
- FutureTask<List<FeedItem>> itemSearchTask = DBTasks.searchFeedItems(context, selectedFeed, query);
+ FutureTask<List<FeedItem>> itemSearchTask = DBTasks.searchFeedItems(selectedFeed, query);
itemSearchTask.run();
return itemSearchTask.get();
} catch (ExecutionException | InterruptedException e) {
@@ -31,9 +30,9 @@ public class FeedSearcher {
}
@NonNull
- public static List<Feed> searchFeeds(final Context context, final String query) {
+ public static List<Feed> searchFeeds(final String query) {
try {
- FutureTask<List<Feed>> feedSearchTask = DBTasks.searchFeeds(context, query);
+ FutureTask<List<Feed>> feedSearchTask = DBTasks.searchFeeds(query);
feedSearchTask.run();
return feedSearchTask.get();
} catch (ExecutionException | InterruptedException e) {
diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/NavDrawerData.java b/core/src/main/java/de/danoeh/antennapod/core/storage/NavDrawerData.java
index 1ec58216a..af02a7733 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/storage/NavDrawerData.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/storage/NavDrawerData.java
@@ -1,24 +1,24 @@
package de.danoeh.antennapod.core.storage;
import de.danoeh.antennapod.model.feed.Feed;
-import de.danoeh.antennapod.core.util.LongIntMap;
import java.util.ArrayList;
import java.util.List;
+import java.util.Map;
public class NavDrawerData {
public final List<DrawerItem> items;
public final int queueSize;
public final int numNewItems;
public final int numDownloadedItems;
- public final LongIntMap feedCounters;
+ public final Map<Long, Integer> feedCounters;
public final int reclaimableSpace;
public NavDrawerData(List<DrawerItem> feeds,
int queueSize,
int numNewItems,
int numDownloadedItems,
- LongIntMap feedIndicatorValues,
+ Map<Long, Integer> feedIndicatorValues,
int reclaimableSpace) {
this.items = feeds;
this.queueSize = queueSize;
diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/StatisticsItem.java b/core/src/main/java/de/danoeh/antennapod/core/storage/StatisticsItem.java
index 90978d6b8..1bc4997dd 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/storage/StatisticsItem.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/storage/StatisticsItem.java
@@ -12,11 +12,6 @@ public class StatisticsItem {
public final long timePlayed;
/**
- * Simply sums up time of podcasts that are marked as played.
- */
- public final long timePlayedCountAll;
-
- /**
* Number of episodes.
*/
public final long episodes;
@@ -27,11 +22,6 @@ public class StatisticsItem {
public final long episodesStarted;
/**
- * All episodes that are marked as played (or have position != 0).
- */
- public final long episodesStartedIncludingMarked;
-
- /**
* Simply sums up the size of download podcasts.
*/
public final long totalDownloadSize;
@@ -41,16 +31,14 @@ public class StatisticsItem {
*/
public final long episodesDownloadCount;
- public StatisticsItem(Feed feed, long time, long timePlayed, long timePlayedCountAll,
- long episodes, long episodesStarted, long episodesStartedIncludingMarked,
+ public StatisticsItem(Feed feed, long time, long timePlayed,
+ long episodes, long episodesStarted,
long totalDownloadSize, long episodesDownloadCount) {
this.feed = feed;
this.time = time;
this.timePlayed = timePlayed;
- this.timePlayedCountAll = timePlayedCountAll;
this.episodes = episodes;
this.episodesStarted = episodesStarted;
- this.episodesStartedIncludingMarked = episodesStartedIncludingMarked;
this.totalDownloadSize = totalDownloadSize;
this.episodesDownloadCount = episodesDownloadCount;
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/mapper/ChapterCursorMapper.java b/core/src/main/java/de/danoeh/antennapod/core/storage/mapper/ChapterCursorMapper.java
deleted file mode 100644
index 5fa376129..000000000
--- a/core/src/main/java/de/danoeh/antennapod/core/storage/mapper/ChapterCursorMapper.java
+++ /dev/null
@@ -1,51 +0,0 @@
-package de.danoeh.antennapod.core.storage.mapper;
-
-import android.database.Cursor;
-import androidx.annotation.NonNull;
-import de.danoeh.antennapod.model.feed.Chapter;
-import de.danoeh.antennapod.parser.feed.element.SimpleChapter;
-import de.danoeh.antennapod.core.storage.PodDBAdapter;
-import de.danoeh.antennapod.parser.media.id3.ID3Chapter;
-import de.danoeh.antennapod.parser.media.vorbis.VorbisCommentChapter;
-
-/**
- * Converts a {@link Cursor} to a {@link Chapter} object.
- */
-public abstract class ChapterCursorMapper {
- /**
- * Create a {@link Chapter} instance from a database row (cursor).
- */
- @NonNull
- public static Chapter convert(@NonNull Cursor cursor) {
- int indexId = cursor.getColumnIndex(PodDBAdapter.KEY_ID);
- int indexTitle = cursor.getColumnIndex(PodDBAdapter.KEY_TITLE);
- int indexStart = cursor.getColumnIndex(PodDBAdapter.KEY_START);
- int indexLink = cursor.getColumnIndex(PodDBAdapter.KEY_LINK);
- int indexImage = cursor.getColumnIndex(PodDBAdapter.KEY_IMAGE_URL);
- int indexChapterType = cursor.getColumnIndex(PodDBAdapter.KEY_CHAPTER_TYPE);
-
- long id = cursor.getLong(indexId);
- String title = cursor.getString(indexTitle);
- long start = cursor.getLong(indexStart);
- String link = cursor.getString(indexLink);
- String imageUrl = cursor.getString(indexImage);
- int chapterType = cursor.getInt(indexChapterType);
-
- Chapter chapter;
- switch (chapterType) {
- case SimpleChapter.CHAPTERTYPE_SIMPLECHAPTER:
- chapter = new SimpleChapter(start, title, link, imageUrl);
- break;
- case ID3Chapter.CHAPTERTYPE_ID3CHAPTER:
- chapter = new ID3Chapter(start, title, link, imageUrl);
- break;
- case VorbisCommentChapter.CHAPTERTYPE_VORBISCOMMENT_CHAPTER:
- chapter = new VorbisCommentChapter(start, title, link, imageUrl);
- break;
- default:
- throw new IllegalArgumentException("Unknown chapter type");
- }
- chapter.setId(id);
- return chapter;
- }
-}
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 4092087f4..723ea1d47 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
@@ -3,6 +3,7 @@ package de.danoeh.antennapod.core.util;
import android.content.ContentResolver;
import android.content.Context;
import android.net.Uri;
+import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.NonNull;
import de.danoeh.antennapod.model.feed.Chapter;
@@ -11,11 +12,13 @@ import de.danoeh.antennapod.model.feed.FeedMedia;
import de.danoeh.antennapod.core.service.download.AntennapodHttpClient;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.util.comparator.ChapterStartTimeComparator;
+import de.danoeh.antennapod.parser.feed.PodcastIndexChapterParser;
import de.danoeh.antennapod.parser.media.id3.ChapterReader;
import de.danoeh.antennapod.parser.media.id3.ID3ReaderException;
import de.danoeh.antennapod.model.playback.Playable;
import de.danoeh.antennapod.parser.media.vorbis.VorbisCommentChapterReader;
import de.danoeh.antennapod.parser.media.vorbis.VorbisCommentReaderException;
+import okhttp3.CacheControl;
import okhttp3.Request;
import okhttp3.Response;
import org.apache.commons.io.input.CountingInputStream;
@@ -57,6 +60,7 @@ public class ChapterUtils {
}
List<Chapter> chaptersFromDatabase = null;
+ List<Chapter> chaptersFromPodcastIndex = null;
if (playable instanceof FeedMedia) {
FeedMedia feedMedia = (FeedMedia) playable;
if (feedMedia.getItem() == null) {
@@ -65,10 +69,17 @@ public class ChapterUtils {
if (feedMedia.getItem().hasChapters()) {
chaptersFromDatabase = DBReader.loadChaptersOfFeedItem(feedMedia.getItem());
}
+
+ if (!TextUtils.isEmpty(feedMedia.getItem().getPodcastIndexChapterUrl())) {
+ chaptersFromPodcastIndex = ChapterUtils.loadChaptersFromUrl(
+ feedMedia.getItem().getPodcastIndexChapterUrl());
+ }
+
}
List<Chapter> chaptersFromMediaFile = ChapterUtils.loadChaptersFromMediaFile(playable, context);
- List<Chapter> chapters = ChapterMerger.merge(chaptersFromDatabase, chaptersFromMediaFile);
+ List<Chapter> chaptersMergePhase1 = ChapterMerger.merge(chaptersFromDatabase, chaptersFromMediaFile);
+ List<Chapter> chapters = ChapterMerger.merge(chaptersMergePhase1, chaptersFromPodcastIndex);
if (chapters == null) {
// Do not try loading again. There are no chapters.
playable.setChapters(Collections.emptyList());
@@ -123,6 +134,27 @@ public class ChapterUtils {
}
}
+ public static List<Chapter> loadChaptersFromUrl(String url) {
+ try {
+ Request request = new Request.Builder().url(url).cacheControl(CacheControl.FORCE_CACHE).build();
+ Response response = AntennapodHttpClient.getHttpClient().newCall(request).execute();
+ if (response.isSuccessful() && response.body() != null) {
+ List<Chapter> chapters = PodcastIndexChapterParser.parse(response.body().string());
+ if (chapters != null && !chapters.isEmpty()) {
+ return chapters;
+ }
+ }
+ request = new Request.Builder().url(url).build();
+ response = AntennapodHttpClient.getHttpClient().newCall(request).execute();
+ if (response.isSuccessful() && response.body() != null) {
+ return PodcastIndexChapterParser.parse(response.body().string());
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ return null;
+ }
+
@NonNull
private static List<Chapter> readId3ChaptersFrom(CountingInputStream in) throws IOException, ID3ReaderException {
ChapterReader reader = new ChapterReader(in);
@@ -139,8 +171,8 @@ public class ChapterUtils {
@NonNull
private static List<Chapter> readOggChaptersFromInputStream(InputStream input) throws VorbisCommentReaderException {
- VorbisCommentChapterReader reader = new VorbisCommentChapterReader();
- reader.readInputStream(input);
+ VorbisCommentChapterReader reader = new VorbisCommentChapterReader(input);
+ reader.readInputStream();
List<Chapter> chapters = reader.getChapters();
if (chapters == null) {
return Collections.emptyList();
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/DateFormatter.java b/core/src/main/java/de/danoeh/antennapod/core/util/DateFormatter.java
index 99628dfcc..dc7ed4508 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/util/DateFormatter.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/DateFormatter.java
@@ -37,7 +37,7 @@ public class DateFormatter {
return android.text.format.DateUtils.formatDateTime(context, date.getTime(), format);
}
- public static String formatForAccessibility(final Context context, final Date date) {
+ public static String formatForAccessibility(final Date date) {
if (date == null) {
return "";
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/DownloadError.java b/core/src/main/java/de/danoeh/antennapod/core/util/DownloadError.java
deleted file mode 100644
index 9e5282576..000000000
--- a/core/src/main/java/de/danoeh/antennapod/core/util/DownloadError.java
+++ /dev/null
@@ -1,60 +0,0 @@
-package de.danoeh.antennapod.core.util;
-
-import android.content.Context;
-
-import de.danoeh.antennapod.core.R;
-
-/** Utility class for Download Errors. */
-public enum DownloadError {
- SUCCESS(0, R.string.download_successful),
- ERROR_PARSER_EXCEPTION(1, R.string.download_error_parser_exception),
- ERROR_UNSUPPORTED_TYPE(2, R.string.download_error_unsupported_type),
- ERROR_CONNECTION_ERROR(3, R.string.download_error_connection_error),
- ERROR_MALFORMED_URL(4, R.string.download_error_error_unknown),
- ERROR_IO_ERROR(5, R.string.download_error_io_error),
- ERROR_FILE_EXISTS(6, R.string.download_error_error_unknown),
- ERROR_DOWNLOAD_CANCELLED(7, R.string.download_canceled_msg),
- ERROR_DEVICE_NOT_FOUND(8, R.string.download_error_device_not_found),
- ERROR_HTTP_DATA_ERROR(9, R.string.download_error_http_data_error),
- ERROR_NOT_ENOUGH_SPACE(10, R.string.download_error_insufficient_space),
- ERROR_UNKNOWN_HOST(11, R.string.download_error_unknown_host),
- ERROR_REQUEST_ERROR(12, R.string.download_error_request_error),
- ERROR_DB_ACCESS_ERROR(13, R.string.download_error_db_access),
- ERROR_UNAUTHORIZED(14, R.string.download_error_unauthorized),
- ERROR_FILE_TYPE(15, R.string.download_error_file_type_type),
- ERROR_FORBIDDEN(16, R.string.download_error_forbidden),
- ERROR_IO_WRONG_SIZE(17, R.string.download_error_wrong_size),
- ERROR_IO_BLOCKED(18, R.string.download_error_blocked),
- ERROR_UNSUPPORTED_TYPE_HTML(19, R.string.download_error_unsupported_type_html),
- ERROR_NOT_FOUND(20, R.string.download_error_not_found),
- ERROR_CERTIFICATE(21, R.string.download_error_certificate),
- ERROR_PARSER_EXCEPTION_DUPLICATE(22, R.string.download_error_parser_exception);
-
- private final int code;
- private final int resId;
-
- DownloadError(int code, int resId) {
- this.code = code;
- this.resId = resId;
- }
-
- /** Return DownloadError from its associated code. */
- public static DownloadError fromCode(int code) {
- for (DownloadError reason : values()) {
- if (reason.getCode() == code) {
- return reason;
- }
- }
- throw new IllegalArgumentException("unknown code: " + code);
- }
-
- /** Get machine-readable code. */
- public int getCode() {
- return code;
- }
-
- /** Get a human-readable string. */
- public String getErrorString(Context context) {
- return context.getString(resId);
- }
-}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/DownloadErrorLabel.java b/core/src/main/java/de/danoeh/antennapod/core/util/DownloadErrorLabel.java
new file mode 100644
index 000000000..3d2558a9f
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/DownloadErrorLabel.java
@@ -0,0 +1,46 @@
+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/LongIntMap.java b/core/src/main/java/de/danoeh/antennapod/core/util/LongIntMap.java
deleted file mode 100644
index 78ed002ac..000000000
--- a/core/src/main/java/de/danoeh/antennapod/core/util/LongIntMap.java
+++ /dev/null
@@ -1,263 +0,0 @@
-package de.danoeh.antennapod.core.util;
-
-
-import java.util.Arrays;
-
-/**
- * Fast and memory efficient long to long map
- */
-public class LongIntMap {
-
- private long[] keys;
- private int[] values;
- private int size;
-
- /**
- * Creates a new LongLongMap containing no mappings.
- */
- public LongIntMap() {
- this(10);
- }
-
- /**
- * Creates a new SparseLongArray containing no mappings that will not
- * require any additional memory allocation to store the specified
- * number of mappings. If you supply an initial capacity of 0, the
- * sparse array will be initialized with a light-weight representation
- * not requiring any additional array allocations.
- */
- public LongIntMap(int initialCapacity) {
- if(initialCapacity < 0) {
- throw new IllegalArgumentException("initial capacity must be 0 or higher");
- }
- keys = new long[initialCapacity];
- values = new int[initialCapacity];
- size = 0;
- }
-
- /**
- * Increases size of array if needed
- */
- private void growIfNeeded() {
- if (size == keys.length) {
- // Resize.
- long[] newKeysArray = new long[size * 3 / 2 + 10];
- int[] newValuesArray = new int[size * 3 / 2 + 10];
- System.arraycopy(keys, 0, newKeysArray, 0, size);
- System.arraycopy(values, 0, newValuesArray, 0, size);
- keys = newKeysArray;
- values = newValuesArray;
- }
- }
-
- /**
- * Gets the long mapped from the specified key, or <code>0</code>
- * if no such mapping has been made.
- */
- public int get(long key) {
- return get(key, 0);
- }
-
- /**
- * Gets the long mapped from the specified key, or the specified value
- * if no such mapping has been made.
- */
- public int get(long key, int valueIfKeyNotFound) {
- int index = indexOfKey(key);
- if(index >= 0) {
- return values[index];
- } else {
- return valueIfKeyNotFound;
- }
- }
-
- /**
- * Removes the mapping from the specified key, if there was any.
- */
- public boolean delete(long key) {
- int index = indexOfKey(key);
-
- if (index >= 0) {
- removeAt(index);
- return true;
- } else {
- return false;
- }
- }
-
- /**
- * Removes the mapping at the given index.
- */
- private void removeAt(int index) {
- System.arraycopy(keys, index + 1, keys, index, size - (index + 1));
- System.arraycopy(values, index + 1, values, index, size - (index + 1));
- size--;
- }
-
- /**
- * Adds a mapping from the specified key to the specified value,
- * replacing the previous mapping from the specified key if there
- * was one.
- */
- public void put(long key, int value) {
- int index = indexOfKey(key);
-
- if (index >= 0) {
- values[index] = value;
- } else {
- growIfNeeded();
- keys[size] = key;
- values[size] = value;
- size++;
- }
- }
-
- /**
- * Returns the number of key-value mappings that this SparseIntArray
- * currently stores.
- */
- public int size() {
- return size;
- }
-
- /**
- * Given an index in the range <code>0...size()-1</code>, returns
- * the key from the <code>index</code>th key-value mapping that this
- * SparseLongArray stores.
- *
- * <p>The keys corresponding to indices in ascending order are guaranteed to
- * be in ascending order, e.g., <code>keyAt(0)</code> will return the
- * smallest key and <code>keyAt(size()-1)</code> will return the largest
- * key.</p>
- */
- private long keyAt(int index) {
- if (index >= size) {
- throw new IndexOutOfBoundsException("n >= size()");
- } else if(index < 0) {
- throw new IndexOutOfBoundsException("n < 0");
- }
- return keys[index];
- }
-
- /**
- * Given an index in the range <code>0...size()-1</code>, returns
- * the value from the <code>index</code>th key-value mapping that this
- * SparseLongArray stores.
- *
- * <p>The values corresponding to indices in ascending order are guaranteed
- * to be associated with keys in ascending order, e.g.,
- * <code>valueAt(0)</code> will return the value associated with the
- * smallest key and <code>valueAt(size()-1)</code> will return the value
- * associated with the largest key.</p>
- */
- private int valueAt(int index) {
- if (index >= size) {
- throw new IndexOutOfBoundsException("n >= size()");
- } else if(index < 0) {
- throw new IndexOutOfBoundsException("n < 0");
- }
- return values[index];
- }
-
- /**
- * Returns the index for which {@link #keyAt} would return the
- * specified key, or a negative number if the specified
- * key is not mapped.
- */
- public int indexOfKey(long key) {
- for(int i=0; i < size; i++) {
- if(keys[i] == key) {
- return i;
- }
- }
- return -1;
- }
-
- /**
- * Returns an index for which {@link #valueAt} would return the
- * specified key, or a negative number if no keys map to the
- * specified value.
- * Beware that this is a linear search, unlike lookups by key,
- * and that multiple keys can map to the same value and this will
- * find only one of them.
- */
- public int indexOfValue(long value) {
- for (int i = 0; i < size; i++) {
- if (values[i] == value) {
- return i;
- }
- }
- return -1;
- }
-
- /**
- * Removes all key-value mappings from this SparseIntArray.
- */
- public void clear() {
- keys = new long[10];
- values = new int[10];
- size = 0;
- }
-
- /**
- * Returns a copy of the values contained in this map.
- *
- * @return a copy of the values contained in this map
- */
- public int[] values() {
- return Arrays.copyOf(values, size);
- }
-
- @Override
- public boolean equals(Object other) {
- if (other == this) {
- return true;
- }
- if (! (other instanceof LongIntMap)) {
- return false;
- }
- LongIntMap otherMap = (LongIntMap) other;
- if (size != otherMap.size) {
- return false;
- }
- for (int i = 0; i < size; i++) {
- if (keys[i] != otherMap.keys[i] ||
- values[i] != otherMap.values[i]) {
- return false;
- }
- }
- return true;
- }
-
- @Override
- public int hashCode() {
- int hashCode = 1;
- for (int i = 0; i < size; i++) {
- long value = values[i];
- hashCode = 31 * hashCode + (int)(value ^ (value >>> 32));
- }
- return hashCode;
- }
-
- @Override
- public String toString() {
- if (size() <= 0) {
- return "LongLongMap{}";
- }
-
- StringBuilder buffer = new StringBuilder(size * 28);
- buffer.append("LongLongMap{");
- for (int i=0; i < size; i++) {
- if (i > 0) {
- buffer.append(", ");
- }
- long key = keyAt(i);
- buffer.append(key);
- buffer.append('=');
- long value = valueAt(i);
- buffer.append(value);
- }
- buffer.append('}');
- return buffer.toString();
- }
-}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/NetworkUtils.java b/core/src/main/java/de/danoeh/antennapod/core/util/NetworkUtils.java
index 0bf301366..85be3c787 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/util/NetworkUtils.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/NetworkUtils.java
@@ -5,7 +5,6 @@ import android.net.ConnectivityManager;
import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.NetworkInfo;
-import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.os.Build;
import android.text.TextUtils;
@@ -146,18 +145,6 @@ public class NetworkUtils {
return selectedNetworks.contains(Integer.toString(wm.getConnectionInfo().getNetworkId()));
}
- /**
- * Returns the SSID of the wifi connection, or <code>null</code> if there is no wifi.
- */
- public static String getWifiSsid() {
- WifiManager wifiManager = (WifiManager) context.getApplicationContext().getSystemService(Context.WIFI_SERVICE);
- WifiInfo wifiInfo = wifiManager.getConnectionInfo();
- if (wifiInfo != null) {
- return wifiInfo.getSSID();
- }
- return null;
- }
-
public static boolean wasDownloadBlocked(Throwable throwable) {
String message = throwable.getMessage();
if (message != null) {
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
index 39deea36a..a1fadb4dc 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/util/PowerUtils.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/PowerUtils.java
@@ -10,8 +10,6 @@ import android.os.BatteryManager;
*/
public class PowerUtils {
- private static final String TAG = "PowerUtils";
-
private PowerUtils() {
}
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
index 974519400..13f2af762 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/util/ShareUtils.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/ShareUtils.java
@@ -2,8 +2,6 @@ package de.danoeh.antennapod.core.util;
import android.content.Context;
import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.util.Log;
@@ -12,7 +10,6 @@ import androidx.core.app.ShareCompat;
import androidx.core.content.FileProvider;
import java.io.File;
-import java.util.List;
import de.danoeh.antennapod.core.R;
import de.danoeh.antennapod.model.feed.Feed;
@@ -35,24 +32,26 @@ public class ShareUtils {
context.startActivity(intent);
}
- public static void shareFeedlink(Context context, Feed feed) {
- shareLink(context, feed.getTitle() + ": " + feed.getLink());
- }
-
- public static void shareFeedDownloadLink(Context context, Feed feed) {
- shareLink(context, feed.getTitle() + ": " + feed.getDownload_url());
- }
-
- private static String getItemShareText(FeedItem item) {
- return item.getFeed().getTitle() + ": " + item.getTitle();
+ public static void shareFeedLink(Context context, Feed feed) {
+ String text = feed.getTitle();
+ if (feed.getLink() != null) {
+ text += "\n" + feed.getLink();
+ }
+ text += "\n\n" + context.getResources().getString(R.string.share_rss_address_label)
+ + " " + feed.getDownload_url();
+ 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.getDownload_url());
+ }
+
public static void shareFeedItemLinkWithDownloadLink(Context context, FeedItem item, boolean withPosition) {
- String text = getItemShareText(item);
+ 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) + ": ";
@@ -76,20 +75,15 @@ public class ShareUtils {
}
public static void shareFeedItemFile(Context context, FeedMedia media) {
- Intent intent = new Intent(Intent.ACTION_SEND);
- intent.setType(media.getMime_type());
Uri fileUri = FileProvider.getUriForFile(context, context.getString(R.string.provider_authority),
new File(media.getLocalMediaUrl()));
- intent.putExtra(Intent.EXTRA_STREAM, fileUri);
- intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
- Intent chooserIntent = Intent.createChooser(intent, context.getString(R.string.share_file_label));
- List<ResolveInfo> resInfoList = context.getPackageManager()
- .queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
- for (ResolveInfo resolveInfo : resInfoList) {
- String packageName = resolveInfo.activityInfo.packageName;
- context.grantUriPermission(packageName, fileUri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
- }
- context.startActivity(chooserIntent);
+
+ new ShareCompat.IntentBuilder(context)
+ .setType(media.getMime_type())
+ .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/comparator/DownloadStatusComparator.java b/core/src/main/java/de/danoeh/antennapod/core/util/comparator/DownloadStatusComparator.java
index 868f3b835..68b38ec7f 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/util/comparator/DownloadStatusComparator.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/comparator/DownloadStatusComparator.java
@@ -2,7 +2,7 @@ package de.danoeh.antennapod.core.util.comparator;
import java.util.Comparator;
-import de.danoeh.antennapod.core.service.download.DownloadStatus;
+import de.danoeh.antennapod.model.download.DownloadStatus;
/** Compares the completion date of two Downloadstatus objects. */
public class DownloadStatusComparator implements Comparator<DownloadStatus> {
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java
index 549171c76..ea1c737a7 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java
@@ -329,10 +329,7 @@ public abstract class PlaybackController {
public void playPause() {
if (playbackService == null) {
- new PlaybackServiceStarter(activity, media)
- .startWhenPrepared(true)
- .streamIfLastWasStream()
- .start();
+ new PlaybackServiceStarter(activity, media).start();
Log.w(TAG, "Play/Pause button was pressed, but playbackservice was null!");
return;
}
@@ -353,8 +350,6 @@ public abstract class PlaybackController {
break;
default:
new PlaybackServiceStarter(activity, media)
- .startWhenPrepared(true)
- .streamIfLastWasStream()
.callEvenIfRunning(true)
.start();
Log.w(TAG, "Play/Pause button was pressed and PlaybackService state was unknown");
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackServiceStarter.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackServiceStarter.java
index 3efded9ed..62d981b55 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackServiceStarter.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackServiceStarter.java
@@ -5,18 +5,14 @@ import android.content.Intent;
import android.os.Parcelable;
import androidx.core.content.ContextCompat;
-import de.danoeh.antennapod.core.preferences.PlaybackPreferences;
import de.danoeh.antennapod.core.service.playback.PlaybackService;
import de.danoeh.antennapod.model.playback.Playable;
public class PlaybackServiceStarter {
private final Context context;
private final Playable media;
- private boolean startWhenPrepared = false;
- private boolean shouldStream = false;
private boolean shouldStreamThisTime = false;
private boolean callEvenIfRunning = false;
- private boolean prepareImmediately = true;
public PlaybackServiceStarter(Context context, Playable media) {
this.context = context;
@@ -26,40 +22,11 @@ public class PlaybackServiceStarter {
/**
* Default value: false
*/
- public PlaybackServiceStarter shouldStream(boolean shouldStream) {
- this.shouldStream = shouldStream;
- return this;
- }
-
- public PlaybackServiceStarter streamIfLastWasStream() {
- boolean lastIsStream = PlaybackPreferences.getCurrentEpisodeIsStream();
- return shouldStream(lastIsStream);
- }
-
- /**
- * Default value: false
- */
- public PlaybackServiceStarter startWhenPrepared(boolean startWhenPrepared) {
- this.startWhenPrepared = startWhenPrepared;
- return this;
- }
-
- /**
- * Default value: false
- */
public PlaybackServiceStarter callEvenIfRunning(boolean callEvenIfRunning) {
this.callEvenIfRunning = callEvenIfRunning;
return this;
}
- /**
- * Default value: true
- */
- public PlaybackServiceStarter prepareImmediately(boolean prepareImmediately) {
- this.prepareImmediately = prepareImmediately;
- return this;
- }
-
public PlaybackServiceStarter shouldStreamThisTime(boolean shouldStreamThisTime) {
this.shouldStreamThisTime = shouldStreamThisTime;
return this;
@@ -68,11 +35,7 @@ public class PlaybackServiceStarter {
public Intent getIntent() {
Intent launchIntent = new Intent(context, PlaybackService.class);
launchIntent.putExtra(PlaybackService.EXTRA_PLAYABLE, (Parcelable) media);
- launchIntent.putExtra(PlaybackService.EXTRA_START_WHEN_PREPARED, startWhenPrepared);
- launchIntent.putExtra(PlaybackService.EXTRA_SHOULD_STREAM, shouldStream);
- launchIntent.putExtra(PlaybackService.EXTRA_PREPARE_IMMEDIATELY, prepareImmediately);
launchIntent.putExtra(PlaybackService.EXTRA_ALLOW_STREAM_THIS_TIME, shouldStreamThisTime);
-
return launchIntent;
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/widget/WidgetUpdater.java b/core/src/main/java/de/danoeh/antennapod/core/widget/WidgetUpdater.java
index 2762fb9fe..8afba881a 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/widget/WidgetUpdater.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/widget/WidgetUpdater.java
@@ -31,6 +31,7 @@ import de.danoeh.antennapod.core.util.TimeSpeedConverter;
import de.danoeh.antennapod.model.playback.Playable;
import de.danoeh.antennapod.playback.base.PlayerStatus;
import de.danoeh.antennapod.ui.appstartintent.MainActivityStarter;
+import de.danoeh.antennapod.ui.appstartintent.PlaybackSpeedActivityStarter;
import de.danoeh.antennapod.ui.appstartintent.VideoPlayerActivityStarter;
/**
@@ -45,20 +46,17 @@ public abstract class WidgetUpdater {
final int position;
final int duration;
final float playbackSpeed;
- final boolean isCasting;
- public WidgetState(Playable media, PlayerStatus status, int position, int duration,
- float playbackSpeed, boolean isCasting) {
+ public WidgetState(Playable media, PlayerStatus status, int position, int duration, float playbackSpeed) {
this.media = media;
this.status = status;
this.position = position;
this.duration = duration;
this.playbackSpeed = playbackSpeed;
- this.isCasting = isCasting;
}
public WidgetState(PlayerStatus status) {
- this(null, status, Playable.INVALID_TIME, Playable.INVALID_TIME, 1.0f, false);
+ this(null, status, Playable.INVALID_TIME, Playable.INVALID_TIME, 1.0f);
}
}
@@ -71,12 +69,14 @@ public abstract class WidgetUpdater {
}
PendingIntent startMediaPlayer;
- if (widgetState.media != null && widgetState.media.getMediaType() == MediaType.VIDEO
- && !widgetState.isCasting) {
+ if (widgetState.media != null && widgetState.media.getMediaType() == MediaType.VIDEO) {
startMediaPlayer = new VideoPlayerActivityStarter(context).getPendingIntent();
} else {
startMediaPlayer = new MainActivityStarter(context).withOpenPlayer().getPendingIntent();
}
+
+ PendingIntent startPlaybackSpeedDialog = new PlaybackSpeedActivityStarter(context).getPendingIntent();
+
RemoteViews views;
views = new RemoteViews(context.getPackageName(), R.layout.player_widget);
@@ -85,6 +85,7 @@ public abstract class WidgetUpdater {
int iconSize = context.getResources().getDimensionPixelSize(android.R.dimen.app_icon_size);
views.setOnClickPendingIntent(R.id.layout_left, startMediaPlayer);
views.setOnClickPendingIntent(R.id.imgvCover, startMediaPlayer);
+ views.setOnClickPendingIntent(R.id.butPlaybackSpeed, startPlaybackSpeedDialog);
try {
icon = Glide.with(context)
@@ -169,13 +170,15 @@ public abstract class WidgetUpdater {
} else {
views.setViewVisibility(R.id.layout_center, View.VISIBLE);
}
+ boolean showPlaybackSpeed = prefs.getBoolean(PlayerWidget.KEY_WIDGET_PLAYBACK_SPEED + id, false);
boolean showRewind = prefs.getBoolean(PlayerWidget.KEY_WIDGET_REWIND + id, false);
boolean showFastForward = prefs.getBoolean(PlayerWidget.KEY_WIDGET_FAST_FORWARD + id, false);
boolean showSkip = prefs.getBoolean(PlayerWidget.KEY_WIDGET_SKIP + id, false);
- if (showRewind || showSkip || showFastForward) {
+ if (showPlaybackSpeed || showRewind || showSkip || showFastForward) {
views.setInt(R.id.extendedButtonsContainer, "setVisibility", View.VISIBLE);
views.setInt(R.id.butPlay, "setVisibility", View.GONE);
+ views.setInt(R.id.butPlaybackSpeed, "setVisibility", showPlaybackSpeed ? View.VISIBLE : View.GONE);
views.setInt(R.id.butRew, "setVisibility", showRewind ? View.VISIBLE : View.GONE);
views.setInt(R.id.butFastForward, "setVisibility", showFastForward ? View.VISIBLE : View.GONE);
views.setInt(R.id.butSkip, "setVisibility", showSkip ? View.VISIBLE : View.GONE);
diff --git a/core/src/main/java/de/danoeh/antennapod/core/widget/WidgetUpdaterJobService.java b/core/src/main/java/de/danoeh/antennapod/core/widget/WidgetUpdaterJobService.java
deleted file mode 100644
index 325c508c5..000000000
--- a/core/src/main/java/de/danoeh/antennapod/core/widget/WidgetUpdaterJobService.java
+++ /dev/null
@@ -1,35 +0,0 @@
-package de.danoeh.antennapod.core.widget;
-
-import android.content.Context;
-import android.content.Intent;
-import androidx.annotation.NonNull;
-import androidx.core.app.SafeJobIntentService;
-import de.danoeh.antennapod.core.feed.util.PlaybackSpeedUtils;
-import de.danoeh.antennapod.core.preferences.PlaybackPreferences;
-import de.danoeh.antennapod.model.playback.Playable;
-import de.danoeh.antennapod.core.util.playback.PlayableUtils;
-import de.danoeh.antennapod.playback.base.PlayerStatus;
-
-public class WidgetUpdaterJobService extends SafeJobIntentService {
- private static final int JOB_ID = -17001;
-
- /**
- * Loads the current media from the database and updates the widget in a background job.
- */
- public static void performBackgroundUpdate(Context context) {
- enqueueWork(context, WidgetUpdaterJobService.class,
- WidgetUpdaterJobService.JOB_ID, new Intent(context, WidgetUpdaterJobService.class));
- }
-
- @Override
- protected void onHandleWork(@NonNull Intent intent) {
- Playable media = PlayableUtils.createInstanceFromPreferences(getApplicationContext());
- if (media != null) {
- WidgetUpdater.updateWidget(this, new WidgetUpdater.WidgetState(media, PlayerStatus.STOPPED,
- media.getPosition(), media.getDuration(), PlaybackSpeedUtils.getCurrentPlaybackSpeed(media),
- PlaybackPreferences.getCurrentEpisodeIsStream()));
- } else {
- WidgetUpdater.updateWidget(this, new WidgetUpdater.WidgetState(PlayerStatus.STOPPED));
- }
- }
-} \ No newline at end of file
diff --git a/core/src/main/java/de/danoeh/antennapod/core/widget/WidgetUpdaterWorker.java b/core/src/main/java/de/danoeh/antennapod/core/widget/WidgetUpdaterWorker.java
new file mode 100644
index 000000000..5bf924ad9
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/widget/WidgetUpdaterWorker.java
@@ -0,0 +1,57 @@
+package de.danoeh.antennapod.core.widget;
+
+import android.content.Context;
+import android.util.Log;
+import androidx.annotation.NonNull;
+import androidx.work.ExistingWorkPolicy;
+import androidx.work.OneTimeWorkRequest;
+import androidx.work.WorkManager;
+import androidx.work.Worker;
+import androidx.work.WorkerParameters;
+import de.danoeh.antennapod.core.feed.util.PlaybackSpeedUtils;
+import de.danoeh.antennapod.core.util.playback.PlayableUtils;
+import de.danoeh.antennapod.model.playback.Playable;
+import de.danoeh.antennapod.playback.base.PlayerStatus;
+
+public class WidgetUpdaterWorker extends Worker {
+
+ private static final String TAG = "WidgetUpdaterWorker";
+
+ public WidgetUpdaterWorker(@NonNull final Context context,
+ @NonNull final WorkerParameters workerParams) {
+ super(context, workerParams);
+ }
+
+ public static void enqueueWork(final Context context) {
+ final OneTimeWorkRequest workRequest = new OneTimeWorkRequest.Builder(WidgetUpdaterWorker.class).build();
+ WorkManager.getInstance(context).enqueueUniqueWork(TAG, ExistingWorkPolicy.REPLACE, workRequest);
+ }
+
+ @NonNull
+ @Override
+ public Result doWork() {
+ try {
+ updateWidget();
+ } catch (final Exception e) {
+ Log.d(TAG, "Failed to update AntennaPod widget: ", e);
+ return Result.failure();
+ }
+ return Result.success();
+ }
+
+ /**
+ * Loads the current media from the database and updates the widget in a background job.
+ */
+ private void updateWidget() {
+ final Playable media = PlayableUtils.createInstanceFromPreferences(getApplicationContext());
+ if (media != null) {
+ WidgetUpdater.updateWidget(getApplicationContext(),
+ new WidgetUpdater.WidgetState(media, PlayerStatus.STOPPED,
+ media.getPosition(), media.getDuration(),
+ PlaybackSpeedUtils.getCurrentPlaybackSpeed(media)));
+ } else {
+ WidgetUpdater.updateWidget(getApplicationContext(),
+ new WidgetUpdater.WidgetState(PlayerStatus.STOPPED));
+ }
+ }
+}
diff --git a/core/src/main/res/drawable-xxxhdpi/chart_box_outline.xml b/core/src/main/res/drawable-xxxhdpi/chart_box_outline.xml
new file mode 100644
index 000000000..c6a9bf35d
--- /dev/null
+++ b/core/src/main/res/drawable-xxxhdpi/chart_box_outline.xml
@@ -0,0 +1,7 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:height="24dp"
+ android:width="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path android:fillColor="?attr/action_icon_color" android:pathData="M9 17H7V10H9V17M13 17H11V7H13V17M17 17H15V13H17V17M19 19H5V5H19V19.1M19 3H5C3.9 3 3 3.9 3 5V19C3 20.1 3.9 21 5 21H19C20.1 21 21 20.1 21 19V5C21 3.9 20.1 3 19 3Z" />
+</vector>
diff --git a/core/src/main/res/drawable/ic_filter_white.xml b/core/src/main/res/drawable/ic_filter_white.xml
new file mode 100644
index 000000000..e60023497
--- /dev/null
+++ b/core/src/main/res/drawable/ic_filter_white.xml
@@ -0,0 +1,7 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:height="24dp"
+ android:width="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path android:fillColor="#ffffff" android:pathData="M15,19.88C15.04,20.18 14.94,20.5 14.71,20.71C14.32,21.1 13.69,21.1 13.3,20.71L9.29,16.7C9.06,16.47 8.96,16.16 9,15.87V10.75L4.21,4.62C3.87,4.19 3.95,3.56 4.38,3.22C4.57,3.08 4.78,3 5,3V3H19V3C19.22,3 19.43,3.08 19.62,3.22C20.05,3.56 20.13,4.19 19.79,4.62L15,10.75V19.88M7.04,5L11,10.06V15.58L13,17.58V10.05L16.96,5H7.04Z" />
+</vector>
diff --git a/core/src/main/res/drawable/ic_rounded_corner_left.xml b/core/src/main/res/drawable/ic_rounded_corner_left.xml
new file mode 100644
index 000000000..8b1b90605
--- /dev/null
+++ b/core/src/main/res/drawable/ic_rounded_corner_left.xml
@@ -0,0 +1,7 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:height="24dp"
+ android:width="24dp"
+ android:viewportWidth="8"
+ android:viewportHeight="8">
+ <path android:fillColor="?attr/background_color" android:pathData="M0 8 0 0C0 4 4 8 8 8Z" />
+</vector>
diff --git a/core/src/main/res/drawable/ic_rounded_corner_right.xml b/core/src/main/res/drawable/ic_rounded_corner_right.xml
new file mode 100644
index 000000000..56decd91c
--- /dev/null
+++ b/core/src/main/res/drawable/ic_rounded_corner_right.xml
@@ -0,0 +1,7 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:height="24dp"
+ android:width="24dp"
+ android:viewportWidth="8"
+ android:viewportHeight="8">
+ <path android:fillColor="?attr/background_color" android:pathData="M8 8 0 8C4 8 8 4 8 0Z" />
+</vector>
diff --git a/core/src/main/res/drawable/ic_sort.xml b/core/src/main/res/drawable/ic_sort.xml
deleted file mode 100644
index 5a43ea587..000000000
--- a/core/src/main/res/drawable/ic_sort.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-<vector android:height="24dp"
- android:viewportHeight="24.0" android:viewportWidth="24.0"
- android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
- <path android:fillColor="?attr/action_icon_color" android:pathData="M3,18h6v-2L3,16v2zM3,6v2h18L21,6L3,6zM3,13h12v-2L3,11v2z"/>
-</vector>
diff --git a/core/src/main/res/drawable/ic_statistics.xml b/core/src/main/res/drawable/ic_statistics.xml
deleted file mode 100644
index 77272a527..000000000
--- a/core/src/main/res/drawable/ic_statistics.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-<vector android:height="24dp"
- android:viewportHeight="24.0" android:viewportWidth="24.0"
- android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
- <path android:fillColor="?attr/action_icon_color" android:pathData="M13,2.05V5.08C16.39,5.57 19,8.47 19,12C19,12.9 18.82,13.75 18.5,14.54L21.12,16.07C21.68,14.83 22,13.45 22,12C22,6.82 18.05,2.55 13,2.05M12,19A7,7 0 0,1 5,12C5,8.47 7.61,5.57 11,5.08V2.05C5.94,2.55 2,6.81 2,12A10,10 0 0,0 12,22C15.3,22 18.23,20.39 20.05,17.91L17.45,16.38C16.17,18 14.21,19 12,19Z"/>
-</vector>
diff --git a/core/src/main/res/layout/player_widget.xml b/core/src/main/res/layout/player_widget.xml
index 60d40e6b5..6f3842e8b 100644
--- a/core/src/main/res/layout/player_widget.xml
+++ b/core/src/main/res/layout/player_widget.xml
@@ -89,6 +89,16 @@
android:visibility="gone">
<ImageButton
+ android:id="@+id/butPlaybackSpeed"
+ android:layout_width="36dp"
+ android:layout_height="36dp"
+ android:background="?android:attr/selectableItemBackground"
+ android:contentDescription="@string/playback_speed"
+ android:layout_marginEnd="2dp"
+ android:scaleType="fitXY"
+ android:src="@drawable/ic_widget_playback_speed" />
+
+ <ImageButton
android:id="@+id/butRew"
android:layout_width="36dp"
android:layout_height="36dp"
diff --git a/core/src/main/res/layout/popup_bubble_view.xml b/core/src/main/res/layout/popup_bubble_view.xml
index cc93bec72..7dee08eb5 100644
--- a/core/src/main/res/layout/popup_bubble_view.xml
+++ b/core/src/main/res/layout/popup_bubble_view.xml
@@ -23,7 +23,7 @@
<Button
android:id="@+id/balloon_button_negative"
android:layout_width="wrap_content"
- android:layout_height="36dp"
+ android:layout_height="wrap_content"
android:textColor="?attr/colorOnSecondary"
android:text="@string/no"
style="@style/Widget.MaterialComponents.Button.TextButton" />
@@ -31,7 +31,7 @@
<Button
android:id="@+id/balloon_button_positive"
android:layout_width="wrap_content"
- android:layout_height="36dp"
+ android:layout_height="wrap_content"
android:textColor="?attr/colorOnSecondary"
android:text="@string/yes"
style="@style/Widget.MaterialComponents.Button.TextButton" />
diff --git a/core/src/main/res/raw/local_feed_default_icon.png b/core/src/main/res/raw/local_feed_default_icon.png
deleted file mode 100644
index c1b24a729..000000000
--- a/core/src/main/res/raw/local_feed_default_icon.png
+++ /dev/null
Binary files differ
diff --git a/core/src/main/res/values/arrays.xml b/core/src/main/res/values/arrays.xml
index de83bbf59..f3cf4361d 100644
--- a/core/src/main/res/values/arrays.xml
+++ b/core/src/main/res/values/arrays.xml
@@ -247,28 +247,6 @@
<item>@string/skip_episode_label</item>
</string-array>
- <string-array name="video_background_behavior_options">
- <item>@string/stop_playback</item>
- <item>@string/player_go_to_picture_in_picture</item>
- <item>@string/continue_playback</item>
- </string-array>
-
- <string-array name="video_background_behavior_values">
- <item>stop</item>
- <item>pip</item>
- <item>continue</item>
- </string-array>
-
- <string-array name="video_background_behavior_options_without_pip">
- <item>@string/stop_playback</item>
- <item>@string/continue_playback</item>
- </string-array>
-
- <string-array name="video_background_behavior_values_without_pip">
- <item>stop</item>
- <item>continue</item>
- </string-array>
-
<string-array name="back_button_behavior_options">
<item>@string/back_button_default</item>
<item>@string/back_button_go_to_page</item>
diff --git a/core/src/main/res/values/attrs.xml b/core/src/main/res/values/attrs.xml
index 596b1cca2..6f88b6d51 100644
--- a/core/src/main/res/values/attrs.xml
+++ b/core/src/main/res/values/attrs.xml
@@ -8,6 +8,7 @@
<attr name="drawer_activated_color" format="color"/>
<attr name="action_icon_color" format="color"/>
<attr name="scrollbar_thumb" format="reference"/>
+ <attr name="background_color" format="color"/>
<attr name="background_elevated" format="color"/>
<attr name="filter_dialog_clear" format="color"/>
<attr name="filter_dialog_button_background" format="reference"/>
diff --git a/core/src/main/res/values/styles.xml b/core/src/main/res/values/styles.xml
index 2fa76eae5..e597f8222 100644
--- a/core/src/main/res/values/styles.xml
+++ b/core/src/main/res/values/styles.xml
@@ -13,6 +13,7 @@
<item name="progressBarTheme">@style/ProgressBarLight</item>
<item name="colorPrimaryDark">@color/accent_light</item>
<item name="android:windowBackground">@color/background_light</item>
+ <item name="background_color">@color/background_light</item>
<item name="actionBarStyle">@style/Widget.AntennaPod.ActionBar.Light</item>
<item name="background_elevated">@color/background_elevated_light</item>
<item name="master_switch_background">@color/master_switch_background_light</item>
@@ -46,6 +47,7 @@
<item name="colorPrimary">@color/accent_dark</item>
<item name="colorPrimaryDark">@color/background_darktheme</item>
<item name="android:windowBackground">@color/background_darktheme</item>
+ <item name="background_color">@color/background_darktheme</item>
<item name="actionBarStyle">@style/Widget.AntennaPod.ActionBar.Dark</item>
<item name="background_elevated">@color/background_elevated_darktheme</item>
<item name="colorControlNormal">@color/white</item>
@@ -83,6 +85,7 @@
<item name="android:color">@color/white</item>
<item name="android:colorBackground">@color/black</item>
<item name="android:windowBackground">@color/black</item>
+ <item name="background_color">@color/black</item>
<item name="android:actionBarStyle">@color/black</item>
<item name="background_elevated">@color/black</item>
<item name="android:textColorHint">@color/medium_gray</item>
@@ -107,32 +110,35 @@
</style>
<style name="Theme.AntennaPod.Light.Translucent" parent="Theme.AntennaPod.Light.NoTitle">
+ <item name="android:statusBarColor" tools:targetApi="lollipop">@android:color/transparent</item>
+ <item name="android:windowLightStatusBar" tools:targetApi="M">false</item>
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowContentOverlay">@null</item>
<item name="android:backgroundDimEnabled">true</item>
<item name="android:windowAnimationStyle">@style/AnimationFade</item>
- <item name="android:windowTranslucentStatus" tools:targetApi="kitkat">true</item>
<item name="android:fitsSystemWindows">true</item>
</style>
<style name="Theme.AntennaPod.Dark.Translucent" parent="Theme.AntennaPod.Dark.NoTitle">
+ <item name="android:statusBarColor" tools:targetApi="lollipop">@android:color/transparent</item>
+ <item name="android:windowLightStatusBar" tools:targetApi="M">false</item>
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowContentOverlay">@null</item>
<item name="android:backgroundDimEnabled">true</item>
<item name="android:windowAnimationStyle">@style/AnimationFade</item>
- <item name="android:windowTranslucentStatus" tools:targetApi="kitkat">true</item>
<item name="android:fitsSystemWindows">true</item>
</style>
- <style name="Theme.AntennaPod.TrueBlack.Translucent" parent="Theme.AntennaPod.TrueBlack">
+ <style name="Theme.AntennaPod.TrueBlack.Translucent" parent="Theme.AntennaPod.TrueBlack.NoTitle">
+ <item name="android:statusBarColor" tools:targetApi="lollipop">@android:color/transparent</item>
+ <item name="android:windowLightStatusBar" tools:targetApi="M">false</item>
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowContentOverlay">@null</item>
<item name="android:backgroundDimEnabled">true</item>
<item name="android:windowAnimationStyle">@style/AnimationFade</item>
- <item name="android:windowTranslucentStatus" tools:targetApi="kitkat">true</item>
<item name="android:fitsSystemWindows">true</item>
</style>
diff --git a/core/src/play/java/de/danoeh/antennapod/core/service/playback/WearMediaSession.java b/core/src/play/java/de/danoeh/antennapod/core/service/playback/WearMediaSession.java
index 2167d9f2c..8df05d10d 100644
--- a/core/src/play/java/de/danoeh/antennapod/core/service/playback/WearMediaSession.java
+++ b/core/src/play/java/de/danoeh/antennapod/core/service/playback/WearMediaSession.java
@@ -6,8 +6,6 @@ import android.support.v4.media.session.PlaybackStateCompat;
import android.support.wearable.media.MediaControlConstants;
public class WearMediaSession {
- public static final String TAG = "WearMediaSession";
-
static void sessionStateAddActionForWear(PlaybackStateCompat.Builder sessionState, String actionName,
CharSequence name, int icon) {
PlaybackStateCompat.CustomAction.Builder actionBuilder =
diff --git a/core/src/test/java/de/danoeh/antennapod/core/feed/LocalFeedUpdaterTest.java b/core/src/test/java/de/danoeh/antennapod/core/feed/LocalFeedUpdaterTest.java
index eb56a1876..05b0584ed 100644
--- a/core/src/test/java/de/danoeh/antennapod/core/feed/LocalFeedUpdaterTest.java
+++ b/core/src/test/java/de/danoeh/antennapod/core/feed/LocalFeedUpdaterTest.java
@@ -14,6 +14,7 @@ import androidx.test.platform.app.InstrumentationRegistry;
import de.danoeh.antennapod.core.preferences.PlaybackPreferences;
import de.danoeh.antennapod.model.feed.Feed;
import de.danoeh.antennapod.model.feed.FeedItem;
+import de.danoeh.antennapod.storage.database.PodDBAdapter;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@@ -31,16 +32,15 @@ import java.util.List;
import de.danoeh.antennapod.core.ApplicationCallbacks;
import de.danoeh.antennapod.core.ClientConfig;
-import de.danoeh.antennapod.core.R;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.storage.DBWriter;
-import de.danoeh.antennapod.core.storage.PodDBAdapter;
import static org.hamcrest.CoreMatchers.endsWith;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.empty;
+import static org.hamcrest.Matchers.startsWith;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.mock;
@@ -158,8 +158,7 @@ public class LocalFeedUpdaterTest {
callUpdateFeed(LOCAL_FEED_DIR1);
Feed feedAfter = verifySingleFeedInDatabase();
- String resourceEntryName = context.getResources().getResourceEntryName(R.raw.local_feed_default_icon);
- assertThat(feedAfter.getImageUrl(), endsWith(resourceEntryName));
+ assertThat(feedAfter.getImageUrl(), startsWith(Feed.PREFIX_GENERATIVE_COVER));
}
/**
@@ -191,17 +190,15 @@ public class LocalFeedUpdaterTest {
@Test
public void testGetImageUrl_EmptyFolder() {
DocumentFile documentFolder = mockDocumentFolder();
- String imageUrl = LocalFeedUpdater.getImageUrl(context, documentFolder);
- String defaultImageName = context.getResources().getResourceEntryName(R.raw.local_feed_default_icon);
- assertThat(imageUrl, endsWith(defaultImageName));
+ String imageUrl = LocalFeedUpdater.getImageUrl(documentFolder);
+ assertThat(imageUrl, startsWith(Feed.PREFIX_GENERATIVE_COVER));
}
@Test
public void testGetImageUrl_NoImageButAudioFiles() {
DocumentFile documentFolder = mockDocumentFolder(mockDocumentFile("audio.mp3", "audio/mp3"));
- String imageUrl = LocalFeedUpdater.getImageUrl(context, documentFolder);
- String defaultImageName = context.getResources().getResourceEntryName(R.raw.local_feed_default_icon);
- assertThat(imageUrl, endsWith(defaultImageName));
+ String imageUrl = LocalFeedUpdater.getImageUrl(documentFolder);
+ assertThat(imageUrl, startsWith(Feed.PREFIX_GENERATIVE_COVER));
}
@Test
@@ -209,7 +206,7 @@ public class LocalFeedUpdaterTest {
for (String filename : LocalFeedUpdater.PREFERRED_FEED_IMAGE_FILENAMES) {
DocumentFile documentFolder = mockDocumentFolder(mockDocumentFile("audio.mp3", "audio/mp3"),
mockDocumentFile(filename, "image/jpeg")); // image MIME type doesn't matter
- String imageUrl = LocalFeedUpdater.getImageUrl(context, documentFolder);
+ String imageUrl = LocalFeedUpdater.getImageUrl(documentFolder);
assertThat(imageUrl, endsWith(filename));
}
}
@@ -218,7 +215,7 @@ public class LocalFeedUpdaterTest {
public void testGetImageUrl_OtherImageFilenameJpg() {
DocumentFile documentFolder = mockDocumentFolder(mockDocumentFile("audio.mp3", "audio/mp3"),
mockDocumentFile("my-image.jpg", "image/jpeg"));
- String imageUrl = LocalFeedUpdater.getImageUrl(context, documentFolder);
+ String imageUrl = LocalFeedUpdater.getImageUrl(documentFolder);
assertThat(imageUrl, endsWith("my-image.jpg"));
}
@@ -226,7 +223,7 @@ public class LocalFeedUpdaterTest {
public void testGetImageUrl_OtherImageFilenameJpeg() {
DocumentFile documentFolder = mockDocumentFolder(mockDocumentFile("audio.mp3", "audio/mp3"),
mockDocumentFile("my-image.jpeg", "image/jpeg"));
- String imageUrl = LocalFeedUpdater.getImageUrl(context, documentFolder);
+ String imageUrl = LocalFeedUpdater.getImageUrl(documentFolder);
assertThat(imageUrl, endsWith("my-image.jpeg"));
}
@@ -234,7 +231,7 @@ public class LocalFeedUpdaterTest {
public void testGetImageUrl_OtherImageFilenamePng() {
DocumentFile documentFolder = mockDocumentFolder(mockDocumentFile("audio.mp3", "audio/mp3"),
mockDocumentFile("my-image.png", "image/png"));
- String imageUrl = LocalFeedUpdater.getImageUrl(context, documentFolder);
+ String imageUrl = LocalFeedUpdater.getImageUrl(documentFolder);
assertThat(imageUrl, endsWith("my-image.png"));
}
@@ -242,9 +239,8 @@ public class LocalFeedUpdaterTest {
public void testGetImageUrl_OtherImageFilenameUnsupportedMimeType() {
DocumentFile documentFolder = mockDocumentFolder(mockDocumentFile("audio.mp3", "audio/mp3"),
mockDocumentFile("my-image.svg", "image/svg+xml"));
- String imageUrl = LocalFeedUpdater.getImageUrl(context, documentFolder);
- String defaultImageName = context.getResources().getResourceEntryName(R.raw.local_feed_default_icon);
- assertThat(imageUrl, endsWith(defaultImageName));
+ String imageUrl = LocalFeedUpdater.getImageUrl(documentFolder);
+ assertThat(imageUrl, startsWith(Feed.PREFIX_GENERATIVE_COVER));
}
/**
@@ -280,7 +276,7 @@ public class LocalFeedUpdaterTest {
// call method to test
Feed feed = new Feed(FEED_URL, null);
- LocalFeedUpdater.updateFeed(feed, context);
+ LocalFeedUpdater.updateFeed(feed, context, null);
}
}
diff --git a/core/src/test/java/de/danoeh/antennapod/core/storage/DbCleanupTests.java b/core/src/test/java/de/danoeh/antennapod/core/storage/DbCleanupTests.java
index b5c2e5c73..5aa670736 100644
--- a/core/src/test/java/de/danoeh/antennapod/core/storage/DbCleanupTests.java
+++ b/core/src/test/java/de/danoeh/antennapod/core/storage/DbCleanupTests.java
@@ -21,6 +21,7 @@ import de.danoeh.antennapod.model.feed.FeedMedia;
import de.danoeh.antennapod.core.preferences.PlaybackPreferences;
import de.danoeh.antennapod.core.preferences.UserPreferences;
+import de.danoeh.antennapod.storage.database.PodDBAdapter;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
diff --git a/core/src/test/java/de/danoeh/antennapod/core/storage/DbNullCleanupAlgorithmTest.java b/core/src/test/java/de/danoeh/antennapod/core/storage/DbNullCleanupAlgorithmTest.java
index 6485c9515..09a2580b1 100644
--- a/core/src/test/java/de/danoeh/antennapod/core/storage/DbNullCleanupAlgorithmTest.java
+++ b/core/src/test/java/de/danoeh/antennapod/core/storage/DbNullCleanupAlgorithmTest.java
@@ -15,6 +15,7 @@ import de.danoeh.antennapod.model.feed.Feed;
import de.danoeh.antennapod.model.feed.FeedItem;
import de.danoeh.antennapod.model.feed.FeedMedia;
import de.danoeh.antennapod.core.preferences.UserPreferences;
+import de.danoeh.antennapod.storage.database.PodDBAdapter;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
diff --git a/core/src/test/java/de/danoeh/antennapod/core/storage/DbReaderTest.java b/core/src/test/java/de/danoeh/antennapod/core/storage/DbReaderTest.java
index c9be06afd..834442fdd 100644
--- a/core/src/test/java/de/danoeh/antennapod/core/storage/DbReaderTest.java
+++ b/core/src/test/java/de/danoeh/antennapod/core/storage/DbReaderTest.java
@@ -13,6 +13,7 @@ import de.danoeh.antennapod.model.feed.FeedItem;
import de.danoeh.antennapod.model.feed.FeedMedia;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.util.LongList;
+import de.danoeh.antennapod.storage.database.PodDBAdapter;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
diff --git a/core/src/test/java/de/danoeh/antennapod/core/storage/DbTasksTest.java b/core/src/test/java/de/danoeh/antennapod/core/storage/DbTasksTest.java
index 0d98ba294..0779c6ae3 100644
--- a/core/src/test/java/de/danoeh/antennapod/core/storage/DbTasksTest.java
+++ b/core/src/test/java/de/danoeh/antennapod/core/storage/DbTasksTest.java
@@ -5,6 +5,7 @@ import android.content.Context;
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;
diff --git a/core/src/test/java/de/danoeh/antennapod/core/storage/DbTestUtils.java b/core/src/test/java/de/danoeh/antennapod/core/storage/DbTestUtils.java
index 413243d1d..3a3213a37 100644
--- a/core/src/test/java/de/danoeh/antennapod/core/storage/DbTestUtils.java
+++ b/core/src/test/java/de/danoeh/antennapod/core/storage/DbTestUtils.java
@@ -9,8 +9,8 @@ import de.danoeh.antennapod.model.feed.Chapter;
import de.danoeh.antennapod.model.feed.Feed;
import de.danoeh.antennapod.model.feed.FeedItem;
import de.danoeh.antennapod.model.feed.FeedMedia;
-import de.danoeh.antennapod.parser.feed.element.SimpleChapter;
import de.danoeh.antennapod.core.util.comparator.FeedItemPubdateComparator;
+import de.danoeh.antennapod.storage.database.PodDBAdapter;
import static org.junit.Assert.assertTrue;
@@ -56,7 +56,7 @@ abstract class DbTestUtils {
List<Chapter> chapters = new ArrayList<>();
item.setChapters(chapters);
for (int k = 0; k < numChapters; k++) {
- chapters.add(new SimpleChapter(k, "item " + j + " chapter " + k,
+ chapters.add(new Chapter(k, "item " + j + " chapter " + k,
"http://example.com", "http://example.com/image.png"));
}
}
diff --git a/core/src/test/java/de/danoeh/antennapod/core/storage/DbWriterTest.java b/core/src/test/java/de/danoeh/antennapod/core/storage/DbWriterTest.java
index 5e73773db..de1e78408 100644
--- a/core/src/test/java/de/danoeh/antennapod/core/storage/DbWriterTest.java
+++ b/core/src/test/java/de/danoeh/antennapod/core/storage/DbWriterTest.java
@@ -10,6 +10,7 @@ import androidx.core.util.Consumer;
import androidx.preference.PreferenceManager;
import androidx.test.platform.app.InstrumentationRegistry;
+import de.danoeh.antennapod.storage.database.PodDBAdapter;
import org.awaitility.Awaitility;
import org.junit.After;
import org.junit.Before;
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
index 8695f98b1..486aa82a8 100644
--- 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
@@ -6,6 +6,8 @@ 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;
@@ -13,7 +15,6 @@ import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import de.danoeh.antennapod.model.feed.Feed;
-import de.danoeh.antennapod.core.storage.PodDBAdapter;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
diff --git a/core/src/test/java/de/danoeh/antennapod/core/util/LongLongMapTest.java b/core/src/test/java/de/danoeh/antennapod/core/util/LongLongMapTest.java
deleted file mode 100644
index 21df71bec..000000000
--- a/core/src/test/java/de/danoeh/antennapod/core/util/LongLongMapTest.java
+++ /dev/null
@@ -1,67 +0,0 @@
-package de.danoeh.antennapod.core.util;
-
-import org.junit.Test;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-
-public class LongLongMapTest {
-
- @Test
- public void testEmptyMap() {
- LongIntMap map = new LongIntMap();
- assertEquals(0, map.size());
- assertEquals("LongLongMap{}", map.toString());
- assertEquals(0, map.get(42));
- assertEquals(-1, map.get(42, -1));
- assertFalse(map.delete(42));
- assertEquals(-1, map.indexOfKey(42));
- assertEquals(-1, map.indexOfValue(42));
- assertEquals(1, map.hashCode());
- }
-
- @Test
- public void testSingleElement() {
- LongIntMap map = new LongIntMap();
- map.put(17, 42);
- assertEquals(1, map.size());
- assertEquals("LongLongMap{17=42}", map.toString());
- assertEquals(42, map.get(17));
- assertEquals(42, map.get(17, -1));
- assertEquals(0, map.indexOfKey(17));
- assertEquals(0, map.indexOfValue(42));
- assertTrue(map.delete(17));
- }
-
- @Test
- public void testAddAndDelete() {
- LongIntMap map = new LongIntMap();
- for(int i=0; i < 100; i++) {
- map.put(i * 17, i * 42);
- }
- assertEquals(100, map.size());
- assertEquals(0, map.get(0));
- assertEquals(42, map.get(17));
- assertEquals(42, map.get(17, -1));
- assertEquals(1, map.indexOfKey(17));
- assertEquals(1, map.indexOfValue(42));
- for(int i=0; i < 100; i++) {
- assertTrue(map.delete(i * 17));
- }
- }
-
- @Test
- public void testOverwrite() {
- LongIntMap map = new LongIntMap();
- map.put(17, 42);
- assertEquals(1, map.size());
- assertEquals("LongLongMap{17=42}", map.toString());
- assertEquals(42, map.get(17));
- map.put(17, 23);
- assertEquals(1, map.size());
- assertEquals("LongLongMap{17=23}", map.toString());
- assertEquals(23, map.get(17));
- }
-
-}
diff --git a/event/build.gradle b/event/build.gradle
index c852c0351..033fc5a3c 100644
--- a/event/build.gradle
+++ b/event/build.gradle
@@ -1,4 +1,6 @@
-apply plugin: "com.android.library"
+plugins {
+ id("com.android.library")
+}
apply from: "../common.gradle"
dependencies {
diff --git a/event/src/main/java/de/danoeh/antennapod/event/FavoritesEvent.java b/event/src/main/java/de/danoeh/antennapod/event/FavoritesEvent.java
index 8b27f74ab..0a3c65adc 100644
--- a/event/src/main/java/de/danoeh/antennapod/event/FavoritesEvent.java
+++ b/event/src/main/java/de/danoeh/antennapod/event/FavoritesEvent.java
@@ -1,26 +1,7 @@
package de.danoeh.antennapod.event;
-import de.danoeh.antennapod.model.feed.FeedItem;
-
public class FavoritesEvent {
- public enum Action {
- ADDED, REMOVED
- }
-
- private final Action action;
- private final FeedItem item;
-
- private FavoritesEvent(Action action, FeedItem item) {
- this.action = action;
- this.item = item;
- }
-
- public static FavoritesEvent added(FeedItem item) {
- return new FavoritesEvent(Action.ADDED, item);
- }
-
- public static FavoritesEvent removed(FeedItem item) {
- return new FavoritesEvent(Action.REMOVED, item);
+ public FavoritesEvent() {
}
}
diff --git a/event/src/main/java/de/danoeh/antennapod/event/FeedItemEvent.java b/event/src/main/java/de/danoeh/antennapod/event/FeedItemEvent.java
index 6c7adc2d7..125d113d6 100644
--- a/event/src/main/java/de/danoeh/antennapod/event/FeedItemEvent.java
+++ b/event/src/main/java/de/danoeh/antennapod/event/FeedItemEvent.java
@@ -9,33 +9,17 @@ import java.util.List;
import de.danoeh.antennapod.model.feed.FeedItem;
public class FeedItemEvent {
-
- public enum Action {
- UPDATE, DELETE_MEDIA
- }
-
- @NonNull
- private final Action action;
@NonNull public final List<FeedItem> items;
- private FeedItemEvent(@NonNull Action action, @NonNull List<FeedItem> items) {
- this.action = action;
+ public FeedItemEvent(@NonNull List<FeedItem> items) {
this.items = items;
}
- public static FeedItemEvent deletedMedia(List<FeedItem> items) {
- return new FeedItemEvent(Action.DELETE_MEDIA, items);
- }
-
- public static FeedItemEvent deletedMedia(FeedItem... items) {
- return deletedMedia(Arrays.asList(items));
- }
-
public static FeedItemEvent updated(List<FeedItem> items) {
- return new FeedItemEvent(Action.UPDATE, items);
+ return new FeedItemEvent(items);
}
public static FeedItemEvent updated(FeedItem... items) {
- return updated(Arrays.asList(items));
+ return new FeedItemEvent(Arrays.asList(items));
}
}
diff --git a/event/src/main/java/de/danoeh/antennapod/event/StatisticsEvent.java b/event/src/main/java/de/danoeh/antennapod/event/StatisticsEvent.java
new file mode 100644
index 000000000..d01e6a059
--- /dev/null
+++ b/event/src/main/java/de/danoeh/antennapod/event/StatisticsEvent.java
@@ -0,0 +1,7 @@
+package de.danoeh.antennapod.event;
+
+public class StatisticsEvent {
+
+ public StatisticsEvent() {
+ }
+}
diff --git a/model/build.gradle b/model/build.gradle
index 6f956a2d5..751a52ef6 100644
--- a/model/build.gradle
+++ b/model/build.gradle
@@ -1,4 +1,6 @@
-apply plugin: "com.android.library"
+plugins {
+ id("com.android.library")
+}
apply from: "../common.gradle"
dependencies {
diff --git a/model/src/main/java/de/danoeh/antennapod/model/download/DownloadError.java b/model/src/main/java/de/danoeh/antennapod/model/download/DownloadError.java
new file mode 100644
index 000000000..a2f987a86
--- /dev/null
+++ b/model/src/main/java/de/danoeh/antennapod/model/download/DownloadError.java
@@ -0,0 +1,49 @@
+package de.danoeh.antennapod.model.download;
+
+/** Utility class for Download Errors. */
+public enum DownloadError {
+ SUCCESS(0),
+ ERROR_PARSER_EXCEPTION(1),
+ ERROR_UNSUPPORTED_TYPE(2),
+ ERROR_CONNECTION_ERROR(3),
+ ERROR_MALFORMED_URL(4),
+ ERROR_IO_ERROR(5),
+ ERROR_FILE_EXISTS(6),
+ ERROR_DOWNLOAD_CANCELLED(7),
+ ERROR_DEVICE_NOT_FOUND(8),
+ ERROR_HTTP_DATA_ERROR(9),
+ ERROR_NOT_ENOUGH_SPACE(10),
+ ERROR_UNKNOWN_HOST(11),
+ ERROR_REQUEST_ERROR(12),
+ ERROR_DB_ACCESS_ERROR(13),
+ ERROR_UNAUTHORIZED(14),
+ ERROR_FILE_TYPE(15),
+ ERROR_FORBIDDEN(16),
+ ERROR_IO_WRONG_SIZE(17),
+ ERROR_IO_BLOCKED(18),
+ ERROR_UNSUPPORTED_TYPE_HTML(19),
+ ERROR_NOT_FOUND(20),
+ ERROR_CERTIFICATE(21),
+ ERROR_PARSER_EXCEPTION_DUPLICATE(22);
+
+ private final int code;
+
+ DownloadError(int code) {
+ this.code = code;
+ }
+
+ /** Return DownloadError from its associated code. */
+ public static DownloadError fromCode(int code) {
+ for (DownloadError reason : values()) {
+ if (reason.getCode() == code) {
+ return reason;
+ }
+ }
+ throw new IllegalArgumentException("unknown code: " + code);
+ }
+
+ /** Get machine-readable code. */
+ public int getCode() {
+ return code;
+ }
+}
diff --git a/model/src/main/java/de/danoeh/antennapod/model/download/DownloadStatus.java b/model/src/main/java/de/danoeh/antennapod/model/download/DownloadStatus.java
new file mode 100644
index 000000000..0a18973df
--- /dev/null
+++ b/model/src/main/java/de/danoeh/antennapod/model/download/DownloadStatus.java
@@ -0,0 +1,153 @@
+package de.danoeh.antennapod.model.download;
+
+import androidx.annotation.NonNull;
+
+import java.util.Date;
+
+import de.danoeh.antennapod.model.feed.FeedFile;
+
+/**
+ * Contains status attributes for one download
+ */
+public class DownloadStatus {
+ /**
+ * Downloaders should use this constant for the size attribute if necessary
+ * so that the listadapters etc. can react properly.
+ */
+ public static final int SIZE_UNKNOWN = -1;
+
+ // ----------------------------------- ATTRIBUTES STORED IN DB
+ /**
+ * A human-readable string which is shown to the user so that he can
+ * identify the download. Should be the title of the item/feed/media or the
+ * URL if the download has no other title.
+ */
+ private final String title;
+ private final long feedfileId;
+ /**
+ * Is used to determine the type of the feedfile even if the feedfile does
+ * not exist anymore. The value should be FEEDFILETYPE_FEED,
+ * FEEDFILETYPE_FEEDIMAGE or FEEDFILETYPE_FEEDMEDIA
+ */
+ private final int feedfileType;
+ private final boolean initiatedByUser;
+ /**
+ * Unique id for storing the object in database.
+ */
+ private long id;
+ private DownloadError reason;
+ /**
+ * A message which can be presented to the user to give more information.
+ * Should be null if Download was successful.
+ */
+ private String reasonDetailed;
+ private boolean successful;
+ private final Date completionDate;
+ // ------------------------------------ NOT STORED IN DB
+ private boolean done;
+ private boolean cancelled;
+
+ /**
+ * Constructor for creating new completed downloads.
+ */
+ public DownloadStatus(@NonNull FeedFile feedfile, String title, DownloadError reason, boolean successful,
+ String reasonDetailed, boolean initiatedByUser) {
+ this(0, title, feedfile.getId(), feedfile.getTypeAsInt(), successful, false, true, reason, new Date(),
+ reasonDetailed, initiatedByUser);
+ }
+
+ public DownloadStatus(long id, String title, long feedfileId, int feedfileType, boolean successful,
+ boolean cancelled, boolean done, DownloadError reason, Date completionDate,
+ String reasonDetailed, boolean initiatedByUser) {
+ this.id = id;
+ this.title = title;
+ this.feedfileId = feedfileId;
+ this.reason = reason;
+ this.successful = successful;
+ this.cancelled = cancelled;
+ this.done = done;
+ this.completionDate = (Date) completionDate.clone();
+ this.reasonDetailed = reasonDetailed;
+ this.feedfileType = feedfileType;
+ this.initiatedByUser = initiatedByUser;
+ }
+
+ @Override
+ @NonNull
+ public String toString() {
+ return "DownloadStatus [id=" + id + ", title=" + title + ", reason="
+ + reason + ", reasonDetailed=" + reasonDetailed
+ + ", successful=" + successful + ", completionDate="
+ + completionDate + ", feedfileId=" + feedfileId
+ + ", feedfileType=" + feedfileType + ", done=" + done
+ + ", cancelled=" + cancelled + "]";
+ }
+
+ public long getId() {
+ return id;
+ }
+
+ public void setId(long id) {
+ this.id = id;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public DownloadError getReason() {
+ return reason;
+ }
+
+ public String getReasonDetailed() {
+ return reasonDetailed;
+ }
+
+ public boolean isSuccessful() {
+ return successful;
+ }
+
+ public Date getCompletionDate() {
+ return (Date) completionDate.clone();
+ }
+
+ public long getFeedfileId() {
+ return feedfileId;
+ }
+
+ public int getFeedfileType() {
+ return feedfileType;
+ }
+
+ public boolean isInitiatedByUser() {
+ return initiatedByUser;
+ }
+
+ public boolean isDone() {
+ return done;
+ }
+
+ public boolean isCancelled() {
+ return cancelled;
+ }
+
+ public void setSuccessful() {
+ this.successful = true;
+ this.reason = DownloadError.SUCCESS;
+ this.done = true;
+ }
+
+ public void setFailed(DownloadError reason, String reasonDetailed) {
+ this.successful = false;
+ this.reason = reason;
+ this.reasonDetailed = reasonDetailed;
+ this.done = true;
+ }
+
+ public void setCancelled() {
+ this.successful = false;
+ this.reason = DownloadError.ERROR_DOWNLOAD_CANCELLED;
+ this.done = true;
+ this.cancelled = true;
+ }
+} \ No newline at end of file
diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/ProxyConfig.java b/model/src/main/java/de/danoeh/antennapod/model/download/ProxyConfig.java
index 797001e3a..4e194b341 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/service/download/ProxyConfig.java
+++ b/model/src/main/java/de/danoeh/antennapod/model/download/ProxyConfig.java
@@ -1,4 +1,4 @@
-package de.danoeh.antennapod.core.service.download;
+package de.danoeh.antennapod.model.download;
import androidx.annotation.Nullable;
diff --git a/model/src/main/java/de/danoeh/antennapod/model/feed/Chapter.java b/model/src/main/java/de/danoeh/antennapod/model/feed/Chapter.java
index 0508df901..e55364dd9 100644
--- a/model/src/main/java/de/danoeh/antennapod/model/feed/Chapter.java
+++ b/model/src/main/java/de/danoeh/antennapod/model/feed/Chapter.java
@@ -1,31 +1,23 @@
package de.danoeh.antennapod.model.feed;
-public abstract class Chapter extends FeedComponent {
-
+public class Chapter extends FeedComponent {
/** Defines starting point in milliseconds. */
private long start;
private String title;
private String link;
private String imageUrl;
+ private String chapterId;
- protected Chapter() {
- }
-
- protected Chapter(long start) {
- super();
- this.start = start;
+ public Chapter() {
}
- protected Chapter(long start, String title, String link, String imageUrl) {
- super();
+ public Chapter(long start, String title, String link, String imageUrl) {
this.start = start;
this.title = title;
this.link = link;
this.imageUrl = imageUrl;
}
- public abstract int getChapterType();
-
public long getStart() {
return start;
}
@@ -58,8 +50,24 @@ public abstract class Chapter extends FeedComponent {
this.imageUrl = imageUrl;
}
+ /**
+ * ID from the chapter source, not the database ID.
+ */
+ public String getChapterId() {
+ return chapterId;
+ }
+
+ public void setChapterId(String chapterId) {
+ this.chapterId = chapterId;
+ }
+
@Override
public String getHumanReadableIdentifier() {
return title;
}
+
+ @Override
+ public String toString() {
+ return "ID3Chapter [title=" + getTitle() + ", start=" + getStart() + ", url=" + getLink() + "]";
+ }
}
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 e570f9bce..006505eb1 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
@@ -19,6 +19,7 @@ public class Feed extends FeedFile {
public static final String TYPE_RSS2 = "rss";
public static final String TYPE_ATOM1 = "atom";
public static final String PREFIX_LOCAL_FOLDER = "antennapod_local:";
+ public static final String PREFIX_GENERATIVE_COVER = "antennapod_generative_cover:";
/**
* title as defined by the feed.
diff --git a/model/src/main/java/de/danoeh/antennapod/model/feed/FeedCounter.java b/model/src/main/java/de/danoeh/antennapod/model/feed/FeedCounter.java
new file mode 100644
index 000000000..eef1cc1ef
--- /dev/null
+++ b/model/src/main/java/de/danoeh/antennapod/model/feed/FeedCounter.java
@@ -0,0 +1,24 @@
+package de.danoeh.antennapod.model.feed;
+
+public enum FeedCounter {
+ SHOW_NEW_UNPLAYED_SUM(0),
+ SHOW_NEW(1),
+ SHOW_UNPLAYED(2),
+ SHOW_NONE(3),
+ SHOW_DOWNLOADED(4);
+
+ public final int id;
+
+ FeedCounter(int id) {
+ this.id = id;
+ }
+
+ public static FeedCounter fromOrdinal(int id) {
+ for (FeedCounter counter : values()) {
+ if (counter.id == id) {
+ return counter;
+ }
+ }
+ return SHOW_NONE;
+ }
+}
diff --git a/model/src/main/java/de/danoeh/antennapod/model/feed/FeedItem.java b/model/src/main/java/de/danoeh/antennapod/model/feed/FeedItem.java
index 08f79252a..a8570ea4e 100644
--- a/model/src/main/java/de/danoeh/antennapod/model/feed/FeedItem.java
+++ b/model/src/main/java/de/danoeh/antennapod/model/feed/FeedItem.java
@@ -41,6 +41,7 @@ public class FeedItem extends FeedComponent implements Serializable {
private transient Feed feed;
private long feedId;
+ private String podcastIndexChapterUrl;
private int state;
public static final int NEW = -1;
@@ -81,7 +82,7 @@ public class FeedItem extends FeedComponent implements Serializable {
* */
public FeedItem(long id, String title, String link, Date pubDate, String paymentLink, long feedId,
boolean hasChapters, String imageUrl, int state,
- String itemIdentifier, long autoDownload) {
+ String itemIdentifier, long autoDownload, String podcastIndexChapterUrl) {
this.id = id;
this.title = title;
this.link = link;
@@ -93,6 +94,7 @@ public class FeedItem extends FeedComponent implements Serializable {
this.state = state;
this.itemIdentifier = itemIdentifier;
this.autoDownload = autoDownload;
+ this.podcastIndexChapterUrl = podcastIndexChapterUrl;
}
/**
@@ -157,6 +159,9 @@ public class FeedItem extends FeedComponent implements Serializable {
chapters = other.chapters;
}
}
+ if (other.podcastIndexChapterUrl != null) {
+ podcastIndexChapterUrl = other.podcastIndexChapterUrl;
+ }
}
/**
@@ -427,6 +432,14 @@ public class FeedItem extends FeedComponent implements Serializable {
tags.remove(tag);
}
+ public String getPodcastIndexChapterUrl() {
+ return podcastIndexChapterUrl;
+ }
+
+ public void setPodcastIndexChapterUrl(String url) {
+ podcastIndexChapterUrl = url;
+ }
+
@NonNull
@Override
public String toString() {
diff --git a/model/src/main/java/de/danoeh/antennapod/model/feed/FeedItemFilter.java b/model/src/main/java/de/danoeh/antennapod/model/feed/FeedItemFilter.java
index 4f0736c58..c9989e60a 100644
--- a/model/src/main/java/de/danoeh/antennapod/model/feed/FeedItemFilter.java
+++ b/model/src/main/java/de/danoeh/antennapod/model/feed/FeedItemFilter.java
@@ -1,9 +1,11 @@
package de.danoeh.antennapod.model.feed;
import android.text.TextUtils;
+
+import java.io.Serializable;
import java.util.Arrays;
-public class FeedItemFilter {
+public class FeedItemFilter implements Serializable {
private final String[] properties;
diff --git a/model/src/main/java/de/danoeh/antennapod/model/feed/FeedMedia.java b/model/src/main/java/de/danoeh/antennapod/model/feed/FeedMedia.java
index d2e4e4556..2fb1a5c0c 100644
--- a/model/src/main/java/de/danoeh/antennapod/model/feed/FeedMedia.java
+++ b/model/src/main/java/de/danoeh/antennapod/model/feed/FeedMedia.java
@@ -226,10 +226,6 @@ public class FeedMedia extends FeedFile implements Playable {
return mime_type;
}
- public void setMime_type(String mime_type) {
- this.mime_type = mime_type;
- }
-
@Nullable
public FeedItem getItem() {
return item;
diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/SubscriptionsFilter.java b/model/src/main/java/de/danoeh/antennapod/model/feed/SubscriptionsFilter.java
index a083a7220..434d474a7 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/feed/SubscriptionsFilter.java
+++ b/model/src/main/java/de/danoeh/antennapod/model/feed/SubscriptionsFilter.java
@@ -1,13 +1,10 @@
-package de.danoeh.antennapod.core.feed;
+package de.danoeh.antennapod.model.feed;
import android.text.TextUtils;
import java.util.ArrayList;
import java.util.List;
-
-import de.danoeh.antennapod.core.util.LongIntMap;
-import de.danoeh.antennapod.model.feed.Feed;
-import de.danoeh.antennapod.model.feed.FeedPreferences;
+import java.util.Map;
public class SubscriptionsFilter {
private static final String divider = ",";
@@ -69,7 +66,7 @@ public class SubscriptionsFilter {
/**
* Run a list of feed items through the filter.
*/
- public List<Feed> filter(List<Feed> items, LongIntMap feedCounters) {
+ public List<Feed> filter(List<Feed> items, Map<Long, Integer> feedCounters) {
if (properties.length == 0) {
return items;
}
@@ -104,7 +101,8 @@ public class SubscriptionsFilter {
if (showIfCounterGreaterZero) {
for (int i = result.size() - 1; i >= 0; i--) {
- if (feedCounters.get(result.get(i).getId()) <= 0) {
+ if (!feedCounters.containsKey(result.get(i).getId())
+ || feedCounters.get(result.get(i).getId()) <= 0) {
result.remove(i);
}
}
diff --git a/model/src/main/java/de/danoeh/antennapod/model/playback/MediaType.java b/model/src/main/java/de/danoeh/antennapod/model/playback/MediaType.java
index 6a7b36097..799977e9a 100644
--- a/model/src/main/java/de/danoeh/antennapod/model/playback/MediaType.java
+++ b/model/src/main/java/de/danoeh/antennapod/model/playback/MediaType.java
@@ -15,16 +15,6 @@ public enum MediaType {
"application/x-flac"
));
- // based on https://developer.android.com/guide/topics/media/media-formats
- static final Set<String> AUDIO_FILE_EXTENSIONS = new HashSet<>(Arrays.asList(
- "3gp", "aac", "amr", "flac", "imy", "m4a", "mid", "mkv", "mp3", "mp4", "mxmf", "oga",
- "ogg", "ogx", "opus", "ota", "rtttl", "rtx", "wav", "xmf"
- ));
-
- static final Set<String> VIDEO_FILE_EXTENSIONS = new HashSet<>(Arrays.asList(
- "3gp", "mkv", "mp4", "ogg", "ogv", "ogx", "webm"
- ));
-
public static MediaType fromMimeType(String mimeType) {
if (TextUtils.isEmpty(mimeType)) {
return MediaType.UNKNOWN;
@@ -37,20 +27,4 @@ public enum MediaType {
}
return MediaType.UNKNOWN;
}
-
- /**
- * @param extensionWithoutDot the file extension (suffix) without the dot
- * @return the {@link MediaType} that likely corresponds to the extension. However, since the
- * extension is not always enough to determine whether a file is an audio or video (3gp
- * can be both, for example), this may not be correct. As a result, where possible,
- * {@link #fromMimeType(String) fromMimeType} should always be tried first.
- */
- public static MediaType fromFileExtension(String extensionWithoutDot) {
- if (AUDIO_FILE_EXTENSIONS.contains(extensionWithoutDot)) {
- return MediaType.AUDIO;
- } else if (VIDEO_FILE_EXTENSIONS.contains(extensionWithoutDot)) {
- return MediaType.VIDEO;
- }
- return MediaType.UNKNOWN;
- }
}
diff --git a/net/discovery/README.md b/net/discovery/README.md
new file mode 100644
index 000000000..53bc7e87c
--- /dev/null
+++ b/net/discovery/README.md
@@ -0,0 +1,3 @@
+# :net:discovery
+
+This module contains the podcast search/discovery APIs.
diff --git a/net/discovery/build.gradle b/net/discovery/build.gradle
new file mode 100644
index 000000000..4bebbd04d
--- /dev/null
+++ b/net/discovery/build.gradle
@@ -0,0 +1,37 @@
+plugins {
+ id("com.android.library")
+}
+apply from: "../../common.gradle"
+apply from: "../../playFlavor.gradle"
+
+android {
+ defaultConfig {
+ if (project.hasProperty("podcastindexApiKey")) {
+ buildConfigField "String", "PODCASTINDEX_API_KEY", '"' + podcastindexApiKey + '"'
+ buildConfigField "String", "PODCASTINDEX_API_SECRET", '"' + podcastindexApiSecret + '"'
+ } else {
+ buildConfigField "String", "PODCASTINDEX_API_KEY", '"XTMMQGA2YZ4WJUBYY4HK"'
+ buildConfigField "String", "PODCASTINDEX_API_SECRET", '"XAaAhk4^2YBsTE33vdbwbZNj82ZRLABDDqFdKe7x"'
+ }
+ }
+
+ lintOptions {
+ disable 'InvalidPeriodicWorkRequestInterval', 'MissingPermission',
+ 'GradleCompatible', 'AppCompatResource', 'QueryPermissionsNeeded'
+ }
+}
+
+dependencies {
+ implementation project(':core')
+ implementation project(':model')
+ implementation project(':net:sync:gpoddernet')
+ implementation project(':net:sync:model')
+
+ annotationProcessor "androidx.annotation:annotation:$annotationVersion"
+
+ implementation 'com.github.mfietz:fyydlin:v0.5.0'
+
+ implementation "io.reactivex.rxjava2:rxandroid:$rxAndroidVersion"
+ implementation "io.reactivex.rxjava2:rxjava:$rxJavaVersion"
+ implementation "com.squareup.okhttp3:okhttp:$okhttpVersion"
+}
diff --git a/net/discovery/src/main/AndroidManifest.xml b/net/discovery/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..3e42a802a
--- /dev/null
+++ b/net/discovery/src/main/AndroidManifest.xml
@@ -0,0 +1 @@
+<manifest package="de.danoeh.antennapod.net.discovery" />
diff --git a/app/src/main/java/de/danoeh/antennapod/discovery/CombinedSearcher.java b/net/discovery/src/main/java/de/danoeh/antennapod/net/discovery/CombinedSearcher.java
index 6c2a87c12..6cbf8eb2e 100644
--- a/app/src/main/java/de/danoeh/antennapod/discovery/CombinedSearcher.java
+++ b/net/discovery/src/main/java/de/danoeh/antennapod/net/discovery/CombinedSearcher.java
@@ -1,4 +1,4 @@
-package de.danoeh.antennapod.discovery;
+package de.danoeh.antennapod.net.discovery;
import android.text.TextUtils;
import android.util.Log;
diff --git a/app/src/main/java/de/danoeh/antennapod/discovery/FyydPodcastSearcher.java b/net/discovery/src/main/java/de/danoeh/antennapod/net/discovery/FyydPodcastSearcher.java
index 5a93e6530..d4674c79d 100644
--- a/app/src/main/java/de/danoeh/antennapod/discovery/FyydPodcastSearcher.java
+++ b/net/discovery/src/main/java/de/danoeh/antennapod/net/discovery/FyydPodcastSearcher.java
@@ -1,4 +1,4 @@
-package de.danoeh.antennapod.discovery;
+package de.danoeh.antennapod.net.discovery;
import de.danoeh.antennapod.core.service.download.AntennapodHttpClient;
import de.mfietz.fyydlin.FyydClient;
diff --git a/app/src/main/java/de/danoeh/antennapod/discovery/GpodnetPodcastSearcher.java b/net/discovery/src/main/java/de/danoeh/antennapod/net/discovery/GpodnetPodcastSearcher.java
index 340783208..222c415ab 100644
--- a/app/src/main/java/de/danoeh/antennapod/discovery/GpodnetPodcastSearcher.java
+++ b/net/discovery/src/main/java/de/danoeh/antennapod/net/discovery/GpodnetPodcastSearcher.java
@@ -1,4 +1,4 @@
-package de.danoeh.antennapod.discovery;
+package de.danoeh.antennapod.net.discovery;
import de.danoeh.antennapod.core.sync.SynchronizationCredentials;
import de.danoeh.antennapod.core.service.download.AntennapodHttpClient;
diff --git a/app/src/main/java/de/danoeh/antennapod/discovery/ItunesPodcastSearcher.java b/net/discovery/src/main/java/de/danoeh/antennapod/net/discovery/ItunesPodcastSearcher.java
index 81ce77ef8..b2ac1766c 100644
--- a/app/src/main/java/de/danoeh/antennapod/discovery/ItunesPodcastSearcher.java
+++ b/net/discovery/src/main/java/de/danoeh/antennapod/net/discovery/ItunesPodcastSearcher.java
@@ -1,4 +1,4 @@
-package de.danoeh.antennapod.discovery;
+package de.danoeh.antennapod.net.discovery;
import de.danoeh.antennapod.core.feed.FeedUrlNotFoundException;
import de.danoeh.antennapod.core.service.download.AntennapodHttpClient;
diff --git a/app/src/main/java/de/danoeh/antennapod/discovery/ItunesTopListLoader.java b/net/discovery/src/main/java/de/danoeh/antennapod/net/discovery/ItunesTopListLoader.java
index e4135fcaa..827a3202f 100644
--- a/app/src/main/java/de/danoeh/antennapod/discovery/ItunesTopListLoader.java
+++ b/net/discovery/src/main/java/de/danoeh/antennapod/net/discovery/ItunesTopListLoader.java
@@ -1,9 +1,8 @@
-package de.danoeh.antennapod.discovery;
+package de.danoeh.antennapod.net.discovery;
import android.content.Context;
import android.util.Log;
-import de.danoeh.antennapod.R;
import de.danoeh.antennapod.core.service.download.AntennapodHttpClient;
import io.reactivex.Single;
import io.reactivex.SingleOnSubscribe;
diff --git a/app/src/main/java/de/danoeh/antennapod/discovery/PodcastIndexPodcastSearcher.java b/net/discovery/src/main/java/de/danoeh/antennapod/net/discovery/PodcastIndexPodcastSearcher.java
index c8e5dc4ef..4645aaf62 100644
--- a/app/src/main/java/de/danoeh/antennapod/discovery/PodcastIndexPodcastSearcher.java
+++ b/net/discovery/src/main/java/de/danoeh/antennapod/net/discovery/PodcastIndexPodcastSearcher.java
@@ -1,4 +1,4 @@
-package de.danoeh.antennapod.discovery;
+package de.danoeh.antennapod.net.discovery;
import org.json.JSONArray;
import org.json.JSONException;
@@ -15,7 +15,7 @@ import java.util.List;
import java.util.Locale;
import java.util.TimeZone;
-import de.danoeh.antennapod.BuildConfig;
+import de.danoeh.antennapod.net.discovery.BuildConfig;
import de.danoeh.antennapod.core.ClientConfig;
import de.danoeh.antennapod.core.service.download.AntennapodHttpClient;
import io.reactivex.Single;
diff --git a/app/src/main/java/de/danoeh/antennapod/discovery/PodcastSearchResult.java b/net/discovery/src/main/java/de/danoeh/antennapod/net/discovery/PodcastSearchResult.java
index 767845cb4..b3f352334 100644
--- a/app/src/main/java/de/danoeh/antennapod/discovery/PodcastSearchResult.java
+++ b/net/discovery/src/main/java/de/danoeh/antennapod/net/discovery/PodcastSearchResult.java
@@ -1,4 +1,4 @@
-package de.danoeh.antennapod.discovery;
+package de.danoeh.antennapod.net.discovery;
import androidx.annotation.Nullable;
import de.danoeh.antennapod.net.sync.gpoddernet.model.GpodnetPodcast;
@@ -39,10 +39,6 @@ public class PodcastSearchResult {
this.author = author;
}
- private PodcastSearchResult(String title, @Nullable String imageUrl, @Nullable String feedUrl) {
- this(title, imageUrl, feedUrl, "");
- }
-
public static PodcastSearchResult dummy() {
return new PodcastSearchResult("", "", "", "");
}
diff --git a/app/src/main/java/de/danoeh/antennapod/discovery/PodcastSearcher.java b/net/discovery/src/main/java/de/danoeh/antennapod/net/discovery/PodcastSearcher.java
index 8fbc8c76b..76edbf843 100644
--- a/app/src/main/java/de/danoeh/antennapod/discovery/PodcastSearcher.java
+++ b/net/discovery/src/main/java/de/danoeh/antennapod/net/discovery/PodcastSearcher.java
@@ -1,4 +1,4 @@
-package de.danoeh.antennapod.discovery;
+package de.danoeh.antennapod.net.discovery;
import io.reactivex.Single;
import java.util.List;
diff --git a/app/src/main/java/de/danoeh/antennapod/discovery/PodcastSearcherRegistry.java b/net/discovery/src/main/java/de/danoeh/antennapod/net/discovery/PodcastSearcherRegistry.java
index dfea627df..c7892bd09 100644
--- a/app/src/main/java/de/danoeh/antennapod/discovery/PodcastSearcherRegistry.java
+++ b/net/discovery/src/main/java/de/danoeh/antennapod/net/discovery/PodcastSearcherRegistry.java
@@ -1,5 +1,4 @@
-package de.danoeh.antennapod.discovery;
-
+package de.danoeh.antennapod.net.discovery;
import io.reactivex.Single;
diff --git a/net/ssl/build.gradle b/net/ssl/build.gradle
index 2a47968d3..f76823e2a 100644
--- a/net/ssl/build.gradle
+++ b/net/ssl/build.gradle
@@ -1,4 +1,6 @@
-apply plugin: "com.android.library"
+plugins {
+ id("com.android.library")
+}
apply from: "../../common.gradle"
apply from: "../../playFlavor.gradle"
diff --git a/net/sync/gpoddernet/build.gradle b/net/sync/gpoddernet/build.gradle
index 10c1d3e17..bea35d793 100644
--- a/net/sync/gpoddernet/build.gradle
+++ b/net/sync/gpoddernet/build.gradle
@@ -1,4 +1,6 @@
-apply plugin: "com.android.library"
+plugins {
+ id("com.android.library")
+}
apply from: "../../../common.gradle"
dependencies {
diff --git a/net/sync/gpoddernet/src/main/java/de/danoeh/antennapod/net/sync/gpoddernet/GpodnetService.java b/net/sync/gpoddernet/src/main/java/de/danoeh/antennapod/net/sync/gpoddernet/GpodnetService.java
index 21a362a40..7c0d7cb51 100644
--- a/net/sync/gpoddernet/src/main/java/de/danoeh/antennapod/net/sync/gpoddernet/GpodnetService.java
+++ b/net/sync/gpoddernet/src/main/java/de/danoeh/antennapod/net/sync/gpoddernet/GpodnetService.java
@@ -19,7 +19,6 @@ import java.net.URISyntaxException;
import java.net.URL;
import java.nio.charset.Charset;
import java.util.ArrayList;
-import java.util.Collections;
import java.util.List;
import java.util.Locale;
@@ -213,45 +212,6 @@ public class GpodnetService implements ISyncService {
}
/**
- * Returns synchronization status of devices.
- * <p/>
- * This method requires authentication.
- *
- * @throws GpodnetServiceAuthenticationException If there is an authentication error.
- */
- public List<List<String>> getSynchronizedDevices() throws GpodnetServiceException {
- requireLoggedIn();
- try {
- URL url = new URI(baseScheme, null, baseHost, basePort,
- String.format("/api/2/sync-devices/%s.json", username), null, null).toURL();
- Request.Builder request = new Request.Builder().url(url);
- String response = executeRequest(request);
- JSONObject syncStatus = new JSONObject(response);
- List<List<String>> result = new ArrayList<>();
-
- JSONArray synchronizedDevices = syncStatus.getJSONArray("synchronized");
- for (int i = 0; i < synchronizedDevices.length(); i++) {
- JSONArray groupDevices = synchronizedDevices.getJSONArray(i);
- List<String> group = new ArrayList<>();
- for (int j = 0; j < groupDevices.length(); j++) {
- group.add(groupDevices.getString(j));
- }
- result.add(group);
- }
-
- JSONArray notSynchronizedDevices = syncStatus.getJSONArray("not-synchronized");
- for (int i = 0; i < notSynchronizedDevices.length(); i++) {
- result.add(Collections.singletonList(notSynchronizedDevices.getString(i)));
- }
-
- return result;
- } catch (JSONException | MalformedURLException | URISyntaxException e) {
- e.printStackTrace();
- throw new GpodnetServiceException(e);
- }
- }
-
- /**
* Configures the device of a given user.
* <p/>
* This method requires authentication.
@@ -288,39 +248,6 @@ public class GpodnetService implements ISyncService {
}
/**
- * Links devices for synchronization.
- * <p/>
- * This method requires authentication.
- *
- * @throws GpodnetServiceAuthenticationException If there is an authentication error.
- */
- public void linkDevices(@NonNull List<String> deviceIds) throws GpodnetServiceException {
- requireLoggedIn();
- try {
- final URL url = new URI(baseScheme, null, baseHost, basePort,
- String.format("/api/2/sync-devices/%s.json", username), null, null).toURL();
- JSONObject jsonContent = new JSONObject();
- JSONArray group = new JSONArray();
- for (String deviceId : deviceIds) {
- group.put(deviceId);
- }
-
- JSONArray synchronizedGroups = new JSONArray();
- synchronizedGroups.put(group);
- jsonContent.put("synchronize", synchronizedGroups);
- jsonContent.put("stop-synchronize", new JSONArray());
-
- Log.d("aaaa", jsonContent.toString());
- RequestBody body = RequestBody.create(JSON, jsonContent.toString());
- Request.Builder request = new Request.Builder().post(body).url(url);
- executeRequest(request);
- } catch (JSONException | MalformedURLException | URISyntaxException e) {
- e.printStackTrace();
- throw new GpodnetServiceException(e);
- }
- }
-
- /**
* Uploads the subscriptions of a specific device.
* <p/>
* This method requires authentication.
diff --git a/net/sync/model/build.gradle b/net/sync/model/build.gradle
index 72d962536..205a00fb0 100644
--- a/net/sync/model/build.gradle
+++ b/net/sync/model/build.gradle
@@ -1,4 +1,6 @@
-apply plugin: "com.android.library"
+plugins {
+ id("com.android.library")
+}
apply from: "../../../common.gradle"
dependencies {
diff --git a/parser/feed/build.gradle b/parser/feed/build.gradle
index 774e08a66..56b4ff740 100644
--- a/parser/feed/build.gradle
+++ b/parser/feed/build.gradle
@@ -1,4 +1,6 @@
-apply plugin: "com.android.library"
+plugins {
+ id("com.android.library")
+}
apply from: "../../common.gradle"
android {
diff --git a/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/PodcastIndexChapterParser.java b/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/PodcastIndexChapterParser.java
new file mode 100644
index 000000000..5dcc18b14
--- /dev/null
+++ b/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/PodcastIndexChapterParser.java
@@ -0,0 +1,31 @@
+package de.danoeh.antennapod.parser.feed;
+
+import de.danoeh.antennapod.model.feed.Chapter;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class PodcastIndexChapterParser {
+ public static List<Chapter> parse(String jsonStr) {
+ try {
+ List<Chapter> chapters = new ArrayList<>();
+ JSONObject obj = new JSONObject(jsonStr);
+ JSONArray objChapters = obj.getJSONArray("chapters");
+ for (int i = 0; i < objChapters.length(); i++) {
+ JSONObject jsonObject = objChapters.getJSONObject(i);
+ int startTime = jsonObject.optInt("startTime", 0);
+ String title = jsonObject.optString("title");
+ String link = jsonObject.optString("url");
+ String img = jsonObject.optString("img");
+ chapters.add(new Chapter(startTime * 1000L, title, link, img));
+ }
+ return chapters;
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ return null;
+ }
+}
diff --git a/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/element/SimpleChapter.java b/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/element/SimpleChapter.java
deleted file mode 100644
index 069e49f09..000000000
--- a/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/element/SimpleChapter.java
+++ /dev/null
@@ -1,16 +0,0 @@
-package de.danoeh.antennapod.parser.feed.element;
-
-import de.danoeh.antennapod.model.feed.Chapter;
-
-public class SimpleChapter extends Chapter {
- public static final int CHAPTERTYPE_SIMPLECHAPTER = 0;
-
- public SimpleChapter(long start, String title, String link, String imageUrl) {
- super(start, title, link, imageUrl);
- }
-
- @Override
- public int getChapterType() {
- return CHAPTERTYPE_SIMPLECHAPTER;
- }
-}
diff --git a/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/namespace/Atom.java b/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/namespace/Atom.java
index ef802c355..a79556c2c 100644
--- a/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/namespace/Atom.java
+++ b/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/namespace/Atom.java
@@ -13,7 +13,7 @@ import org.xml.sax.Attributes;
import de.danoeh.antennapod.model.feed.FeedItem;
import de.danoeh.antennapod.model.feed.FeedMedia;
import de.danoeh.antennapod.parser.feed.element.SyndElement;
-import de.danoeh.antennapod.parser.feed.util.SyndTypeUtils;
+import de.danoeh.antennapod.parser.feed.util.MimeTypeUtils;
public class Atom extends Namespace {
private static final String TAG = "NSAtom";
@@ -91,15 +91,11 @@ public class Atom extends Namespace {
} catch (NumberFormatException e) {
Log.d(TAG, "Length attribute could not be parsed.");
}
- String type = attributes.getValue(LINK_TYPE);
-
- if (type == null) {
- type = SyndTypeUtils.getMimeTypeFromUrl(href);
- }
+ String mimeType = MimeTypeUtils.getMimeType(attributes.getValue(LINK_TYPE), href);
FeedItem currItem = state.getCurrentItem();
- if (SyndTypeUtils.enclosureTypeValid(type) && currItem != null && !currItem.hasMedia()) {
- currItem.setMedia(new FeedMedia(currItem, href, size, type));
+ if (MimeTypeUtils.isMediaFile(mimeType) && currItem != null && !currItem.hasMedia()) {
+ currItem.setMedia(new FeedMedia(currItem, href, size, mimeType));
}
} else if (LINK_REL_PAYMENT.equals(rel)) {
state.getCurrentItem().setPaymentLink(href);
diff --git a/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/namespace/DublinCore.java b/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/namespace/DublinCore.java
index 003f72e9b..7e2f68a17 100644
--- a/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/namespace/DublinCore.java
+++ b/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/namespace/DublinCore.java
@@ -8,7 +8,6 @@ import org.xml.sax.Attributes;
import de.danoeh.antennapod.model.feed.FeedItem;
public class DublinCore extends Namespace {
- private static final String TAG = "NSDublinCore";
public static final String NSTAG = "dc";
public static final String NSURI = "http://purl.org/dc/elements/1.1/";
diff --git a/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/namespace/Itunes.java b/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/namespace/Itunes.java
index 63d8dd476..9c57d4ef3 100644
--- a/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/namespace/Itunes.java
+++ b/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/namespace/Itunes.java
@@ -55,7 +55,7 @@ public class Itunes extends Namespace {
return;
}
- if (AUTHOR.equals(localName) && state.getFeed() != null) {
+ if (AUTHOR.equals(localName) && state.getFeed() != null && state.getTagstack().size() <= 3) {
state.getFeed().setAuthor(contentFromHtml);
} else if (DURATION.equals(localName)) {
try {
diff --git a/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/namespace/Media.java b/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/namespace/Media.java
index f480a0417..85cafea84 100644
--- a/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/namespace/Media.java
+++ b/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/namespace/Media.java
@@ -11,7 +11,7 @@ import java.util.concurrent.TimeUnit;
import de.danoeh.antennapod.model.feed.FeedMedia;
import de.danoeh.antennapod.parser.feed.element.AtomText;
-import de.danoeh.antennapod.parser.feed.util.SyndTypeUtils;
+import de.danoeh.antennapod.parser.feed.util.MimeTypeUtils;
/** Processes tags from the http://search.yahoo.com/mrss/ namespace. */
public class Media extends Namespace {
@@ -43,33 +43,28 @@ public class Media extends Namespace {
Attributes attributes) {
if (CONTENT.equals(localName)) {
String url = attributes.getValue(DOWNLOAD_URL);
- String type = attributes.getValue(MIME_TYPE);
String defaultStr = attributes.getValue(DEFAULT);
String medium = attributes.getValue(MEDIUM);
boolean validTypeMedia = false;
boolean validTypeImage = false;
boolean isDefault = "true".equals(defaultStr);
- String guessedType = SyndTypeUtils.getMimeTypeFromUrl(url);
+ String mimeType = MimeTypeUtils.getMimeType(attributes.getValue(MIME_TYPE), url);
if (MEDIUM_AUDIO.equals(medium)) {
validTypeMedia = true;
- type = "audio/*";
+ mimeType = "audio/*";
} else if (MEDIUM_VIDEO.equals(medium)) {
validTypeMedia = true;
- type = "video/*";
- } else if (MEDIUM_IMAGE.equals(medium) && (guessedType == null
- || (!guessedType.startsWith("audio/") && !guessedType.startsWith("video/")))) {
+ mimeType = "video/*";
+ } else if (MEDIUM_IMAGE.equals(medium) && (mimeType == null
+ || (!mimeType.startsWith("audio/") && !mimeType.startsWith("video/")))) {
// Apparently, some publishers explicitly specify the audio file as an image
validTypeImage = true;
- type = "image/*";
+ mimeType = "image/*";
} else {
- if (type == null) {
- type = guessedType;
- }
-
- if (SyndTypeUtils.enclosureTypeValid(type)) {
+ if (MimeTypeUtils.isMediaFile(mimeType)) {
validTypeMedia = true;
- } else if (SyndTypeUtils.imageTypeValid(type)) {
+ } else if (MimeTypeUtils.isImageFile(mimeType)) {
validTypeImage = true;
}
}
@@ -94,7 +89,7 @@ public class Media extends Namespace {
Log.e(TAG, "Duration \"" + durationStr + "\" could not be parsed");
}
}
- FeedMedia media = new FeedMedia(state.getCurrentItem(), url, size, type);
+ FeedMedia media = new FeedMedia(state.getCurrentItem(), url, size, mimeType);
if (durationMs > 0) {
media.setDuration(durationMs);
}
diff --git a/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/namespace/Namespace.java b/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/namespace/Namespace.java
index 5273c6731..f65d124eb 100644
--- a/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/namespace/Namespace.java
+++ b/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/namespace/Namespace.java
@@ -5,10 +5,7 @@ import de.danoeh.antennapod.parser.feed.element.SyndElement;
import org.xml.sax.Attributes;
public abstract class Namespace {
- public static final String NSTAG = null;
- public static final String NSURI = null;
-
- /** Called by a Feedhandler when in startElement and it detects a namespace element
+ /** Called by a Feedhandler when in startElement and it detects a namespace element
* @return The SyndElement to push onto the stack
* */
public abstract SyndElement handleElementStart(String localName, HandlerState state, Attributes attributes);
diff --git a/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/namespace/PodcastIndex.java b/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/namespace/PodcastIndex.java
index 1d4a91192..1f543a5ae 100644
--- a/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/namespace/PodcastIndex.java
+++ b/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/namespace/PodcastIndex.java
@@ -1,8 +1,8 @@
package de.danoeh.antennapod.parser.feed.namespace;
+import android.text.TextUtils;
import de.danoeh.antennapod.parser.feed.HandlerState;
import de.danoeh.antennapod.parser.feed.element.SyndElement;
-import org.jsoup.helper.StringUtil;
import org.xml.sax.Attributes;
import de.danoeh.antennapod.model.feed.FeedFunding;
@@ -13,6 +13,7 @@ public class PodcastIndex extends Namespace {
public static final String NSURI2 = "https://podcastindex.org/namespace/1.0";
private static final String URL = "url";
private static final String FUNDING = "funding";
+ private static final String CHAPTERS = "chapters";
@Override
public SyndElement handleElementStart(String localName, HandlerState state,
@@ -22,6 +23,11 @@ public class PodcastIndex extends Namespace {
FeedFunding funding = new FeedFunding(href, "");
state.setCurrentFunding(funding);
state.getFeed().addPayment(state.getCurrentFunding());
+ } else if (CHAPTERS.equals(localName)) {
+ String href = attributes.getValue(URL);
+ if (!TextUtils.isEmpty(href)) {
+ state.getCurrentItem().setPodcastIndexChapterUrl(href);
+ }
}
return new SyndElement(localName, this);
}
@@ -32,7 +38,7 @@ public class PodcastIndex extends Namespace {
return;
}
String content = state.getContentBuf().toString();
- if (FUNDING.equals(localName) && state.getCurrentFunding() != null && !StringUtil.isBlank(content)) {
+ if (FUNDING.equals(localName) && state.getCurrentFunding() != null && !TextUtils.isEmpty(content)) {
state.getCurrentFunding().setContent(content);
}
}
diff --git a/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/namespace/Rss20.java b/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/namespace/Rss20.java
index 420bc0000..b19500895 100644
--- a/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/namespace/Rss20.java
+++ b/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/namespace/Rss20.java
@@ -12,7 +12,7 @@ import org.xml.sax.Attributes;
import de.danoeh.antennapod.model.feed.FeedItem;
import de.danoeh.antennapod.model.feed.FeedMedia;
-import de.danoeh.antennapod.parser.feed.util.SyndTypeUtils;
+import de.danoeh.antennapod.parser.feed.util.MimeTypeUtils;
import java.util.Locale;
@@ -46,18 +46,12 @@ public class Rss20 extends Namespace {
state.getItems().add(state.getCurrentItem());
state.getCurrentItem().setFeed(state.getFeed());
} else if (ENCLOSURE.equals(localName) && ITEM.equals(state.getTagstack().peek().getName())) {
- String type = attributes.getValue(ENC_TYPE);
String url = attributes.getValue(ENC_URL);
-
- boolean validType = SyndTypeUtils.enclosureTypeValid(type);
- if (!validType) {
- type = SyndTypeUtils.getMimeTypeFromUrl(url);
- validType = SyndTypeUtils.enclosureTypeValid(type);
- }
+ String mimeType = MimeTypeUtils.getMimeType(attributes.getValue(ENC_TYPE), url);
boolean validUrl = !TextUtils.isEmpty(url);
if (state.getCurrentItem() != null && state.getCurrentItem().getMedia() == null
- && validType && validUrl) {
+ && MimeTypeUtils.isMediaFile(mimeType) && validUrl) {
long size = 0;
try {
size = Long.parseLong(attributes.getValue(ENC_LEN));
@@ -68,7 +62,7 @@ public class Rss20 extends Namespace {
} catch (NumberFormatException e) {
Log.d(TAG, "Length attribute could not be parsed.");
}
- FeedMedia media = new FeedMedia(state.getCurrentItem(), url, size, type);
+ FeedMedia media = new FeedMedia(state.getCurrentItem(), url, size, mimeType);
state.getCurrentItem().setMedia(media);
}
}
diff --git a/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/namespace/SimpleChapters.java b/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/namespace/SimpleChapters.java
index e1912ed45..dd116e189 100644
--- a/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/namespace/SimpleChapters.java
+++ b/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/namespace/SimpleChapters.java
@@ -2,8 +2,8 @@ package de.danoeh.antennapod.parser.feed.namespace;
import android.util.Log;
+import de.danoeh.antennapod.model.feed.Chapter;
import de.danoeh.antennapod.parser.feed.HandlerState;
-import de.danoeh.antennapod.parser.feed.element.SimpleChapter;
import de.danoeh.antennapod.parser.feed.element.SyndElement;
import de.danoeh.antennapod.parser.feed.util.DateUtils;
import org.xml.sax.Attributes;
@@ -37,7 +37,7 @@ public class SimpleChapters extends Namespace {
String title = attributes.getValue(TITLE);
String link = attributes.getValue(HREF);
String imageUrl = attributes.getValue(IMAGE);
- SimpleChapter chapter = new SimpleChapter(start, title, link, imageUrl);
+ Chapter chapter = new Chapter(start, title, link, imageUrl);
currentItem.getChapters().add(chapter);
} catch (NumberFormatException e) {
Log.e(TAG, "Unable to read chapter", e);
diff --git a/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/util/MimeTypeUtils.java b/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/util/MimeTypeUtils.java
new file mode 100644
index 000000000..99faaa133
--- /dev/null
+++ b/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/util/MimeTypeUtils.java
@@ -0,0 +1,83 @@
+package de.danoeh.antennapod.parser.feed.util;
+
+import android.webkit.MimeTypeMap;
+import androidx.annotation.Nullable;
+import org.apache.commons.io.FilenameUtils;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Utility class for handling MIME-Types of enclosures.
+ * */
+public class MimeTypeUtils {
+ public static final String OCTET_STREAM = "application/octet-stream";
+
+ // based on https://developer.android.com/guide/topics/media/media-formats
+ static final Set<String> AUDIO_FILE_EXTENSIONS = new HashSet<>(Arrays.asList(
+ "3gp", "aac", "amr", "flac", "imy", "m4a", "m4b", "mid", "mkv", "mp3", "mp4", "mxmf", "oga",
+ "ogg", "ogx", "opus", "ota", "rtttl", "rtx", "wav", "xmf"
+ ));
+
+ static final Set<String> VIDEO_FILE_EXTENSIONS = new HashSet<>(Arrays.asList(
+ "3gp", "mkv", "mp4", "ogg", "ogv", "ogx", "webm"
+ ));
+
+ private MimeTypeUtils() {
+
+ }
+
+ @Nullable
+ public static String getMimeType(@Nullable String type, @Nullable String filename) {
+ if (isMediaFile(type) && !OCTET_STREAM.equals(type)) {
+ return type;
+ }
+ String filenameType = MimeTypeUtils.getMimeTypeFromUrl(filename);
+ if (isMediaFile(filenameType)) {
+ return filenameType;
+ }
+ return type;
+ }
+
+ public static boolean isMediaFile(String type) {
+ if (type == null) {
+ return false;
+ } else {
+ return type.startsWith("audio/")
+ || type.startsWith("video/")
+ || type.equals("application/ogg")
+ || type.equals("application/octet-stream");
+ }
+ }
+
+ public static boolean isImageFile(String type) {
+ if (type == null) {
+ return false;
+ } else {
+ return type.startsWith("image/");
+ }
+ }
+
+ /**
+ * Should be used if mime-type of enclosure tag is not supported. This
+ * method will return the mime-type of the file extension.
+ */
+ private static String getMimeTypeFromUrl(String url) {
+ if (url == null) {
+ return null;
+ }
+ String extension = FilenameUtils.getExtension(url);
+ String mapResult = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
+ if (mapResult != null) {
+ return mapResult;
+ }
+
+ if (AUDIO_FILE_EXTENSIONS.contains(extension)) {
+ return "audio/*";
+ } else if (VIDEO_FILE_EXTENSIONS.contains(extension)) {
+ return "video/*";
+ }
+ return null;
+ }
+}
diff --git a/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/util/SyndTypeUtils.java b/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/util/SyndTypeUtils.java
deleted file mode 100644
index 2e6cf864f..000000000
--- a/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/util/SyndTypeUtils.java
+++ /dev/null
@@ -1,44 +0,0 @@
-package de.danoeh.antennapod.parser.feed.util;
-
-import android.webkit.MimeTypeMap;
-import org.apache.commons.io.FilenameUtils;
-
-/**
- * Utility class for handling MIME-Types of enclosures.
- * */
-public class SyndTypeUtils {
- private SyndTypeUtils() {
-
- }
-
- public static boolean enclosureTypeValid(String type) {
- if (type == null) {
- return false;
- } else {
- return type.startsWith("audio/")
- || type.startsWith("video/")
- || type.equals("application/ogg")
- || type.equals("application/octet-stream");
- }
- }
-
- public static boolean imageTypeValid(String type) {
- if (type == null) {
- return false;
- } else {
- return type.startsWith("image/");
- }
- }
-
- /**
- * Should be used if mime-type of enclosure tag is not supported. This
- * method will return the mime-type of the file extension.
- */
- public static String getMimeTypeFromUrl(String url) {
- if (url == null) {
- return null;
- }
- String extension = FilenameUtils.getExtension(url);
- return MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
- }
-}
diff --git a/parser/media/build.gradle b/parser/media/build.gradle
index e463040b9..7dce7b86a 100644
--- a/parser/media/build.gradle
+++ b/parser/media/build.gradle
@@ -1,4 +1,6 @@
-apply plugin: "com.android.library"
+plugins {
+ id("com.android.library")
+}
apply from: "../../common.gradle"
dependencies {
diff --git a/parser/media/src/main/java/de/danoeh/antennapod/parser/media/id3/ChapterReader.java b/parser/media/src/main/java/de/danoeh/antennapod/parser/media/id3/ChapterReader.java
index ac79432b5..62322e7b2 100644
--- a/parser/media/src/main/java/de/danoeh/antennapod/parser/media/id3/ChapterReader.java
+++ b/parser/media/src/main/java/de/danoeh/antennapod/parser/media/id3/ChapterReader.java
@@ -50,7 +50,10 @@ public class ChapterReader extends ID3Reader {
String elementId = readIsoStringNullTerminated(100);
long startTime = readInt();
skipBytes(12); // Ignore end time, start offset, end offset
- ID3Chapter chapter = new ID3Chapter(elementId, startTime);
+
+ Chapter chapter = new Chapter();
+ chapter.setStart(startTime);
+ chapter.setChapterId(elementId);
// Read sub-frames
while (getPosition() < chapterStartedPosition + frameHeader.getSize()) {
diff --git a/parser/media/src/main/java/de/danoeh/antennapod/parser/media/id3/ID3Chapter.java b/parser/media/src/main/java/de/danoeh/antennapod/parser/media/id3/ID3Chapter.java
deleted file mode 100644
index fc594ab5a..000000000
--- a/parser/media/src/main/java/de/danoeh/antennapod/parser/media/id3/ID3Chapter.java
+++ /dev/null
@@ -1,38 +0,0 @@
-package de.danoeh.antennapod.parser.media.id3;
-
-import de.danoeh.antennapod.model.feed.Chapter;
-
-public class ID3Chapter extends Chapter {
- public static final int CHAPTERTYPE_ID3CHAPTER = 2;
-
- /**
- * Identifies the chapter in its ID3 tag. This attribute does not have to be
- * store in the DB and is only used for parsing.
- */
- private String id3ID;
-
- public ID3Chapter(String id3ID, long start) {
- super(start);
- this.id3ID = id3ID;
- }
-
- public ID3Chapter(long start, String title, String link, String imageUrl) {
- super(start, title, link, imageUrl);
- }
-
- @Override
- public String toString() {
- return "ID3Chapter [id3ID=" + id3ID + ", title=" + getTitle() + ", start="
- + getStart() + ", url=" + getLink() + "]";
- }
-
- @Override
- public int getChapterType() {
- return CHAPTERTYPE_ID3CHAPTER;
- }
-
- public String getId3ID() {
- return id3ID;
- }
-
-}
diff --git a/parser/media/src/main/java/de/danoeh/antennapod/parser/media/id3/Id3MetadataReader.java b/parser/media/src/main/java/de/danoeh/antennapod/parser/media/id3/Id3MetadataReader.java
new file mode 100644
index 000000000..e6cc67ee8
--- /dev/null
+++ b/parser/media/src/main/java/de/danoeh/antennapod/parser/media/id3/Id3MetadataReader.java
@@ -0,0 +1,39 @@
+package de.danoeh.antennapod.parser.media.id3;
+
+import androidx.annotation.NonNull;
+import de.danoeh.antennapod.parser.media.id3.model.FrameHeader;
+import org.apache.commons.io.input.CountingInputStream;
+
+import java.io.IOException;
+
+/**
+ * Reads general ID3 metadata like comment, which Android's MediaMetadataReceiver does not support.
+ */
+public class Id3MetadataReader extends ID3Reader {
+ public static final String FRAME_ID_COMMENT = "COMM";
+
+ private String comment = null;
+
+ public Id3MetadataReader(CountingInputStream input) {
+ super(input);
+ }
+
+ @Override
+ protected void readFrame(@NonNull FrameHeader frameHeader) throws IOException, ID3ReaderException {
+ if (FRAME_ID_COMMENT.equals(frameHeader.getId())) {
+ long frameStart = getPosition();
+ int encoding = readByte();
+ skipBytes(3); // Language
+ String shortDescription = readEncodedString(encoding, frameHeader.getSize() - 4);
+ String longDescription = readEncodedString(encoding,
+ (int) (frameHeader.getSize() - (getPosition() - frameStart)));
+ comment = shortDescription.length() > longDescription.length() ? shortDescription : longDescription;
+ } else {
+ super.readFrame(frameHeader);
+ }
+ }
+
+ public String getComment() {
+ return comment;
+ }
+}
diff --git a/parser/media/src/main/java/de/danoeh/antennapod/parser/media/vorbis/VorbisCommentChapter.java b/parser/media/src/main/java/de/danoeh/antennapod/parser/media/vorbis/VorbisCommentChapter.java
deleted file mode 100644
index 88ee7fef9..000000000
--- a/parser/media/src/main/java/de/danoeh/antennapod/parser/media/vorbis/VorbisCommentChapter.java
+++ /dev/null
@@ -1,88 +0,0 @@
-package de.danoeh.antennapod.parser.media.vorbis;
-
-import java.util.concurrent.TimeUnit;
-
-import de.danoeh.antennapod.model.feed.Chapter;
-
-public class VorbisCommentChapter extends Chapter {
- public static final int CHAPTERTYPE_VORBISCOMMENT_CHAPTER = 3;
-
- private static final int CHAPTERXXX_LENGTH = "chapterxxx".length();
-
- private int vorbisCommentId;
-
- public VorbisCommentChapter(int vorbisCommentId) {
- this.vorbisCommentId = vorbisCommentId;
- }
-
- public VorbisCommentChapter(long start, String title, String link, String imageUrl) {
- super(start, title, link, imageUrl);
- }
-
- @Override
- public String toString() {
- return "VorbisCommentChapter [id=" + getId() + ", title=" + getTitle()
- + ", link=" + getLink() + ", start=" + getStart() + "]";
- }
-
- public static long getStartTimeFromValue(String value)
- throws VorbisCommentReaderException {
- String[] parts = value.split(":");
- if (parts.length >= 3) {
- try {
- long hours = TimeUnit.MILLISECONDS.convert(
- Long.parseLong(parts[0]), TimeUnit.HOURS);
- long minutes = TimeUnit.MILLISECONDS.convert(
- Long.parseLong(parts[1]), TimeUnit.MINUTES);
- if (parts[2].contains("-->")) {
- parts[2] = parts[2].substring(0, parts[2].indexOf("-->"));
- }
- long seconds = TimeUnit.MILLISECONDS.convert(
- ((long) Float.parseFloat(parts[2])), TimeUnit.SECONDS);
- return hours + minutes + seconds;
- } catch (NumberFormatException e) {
- throw new VorbisCommentReaderException(e);
- }
- } else {
- throw new VorbisCommentReaderException("Invalid time string");
- }
- }
-
- /**
- * Return the id of a vorbiscomment chapter from a string like CHAPTERxxx*
- *
- * @return the id of the chapter key or -1 if the id couldn't be read.
- * @throws VorbisCommentReaderException
- * */
- public static int getIDFromKey(String key) throws VorbisCommentReaderException {
- if (key.length() >= CHAPTERXXX_LENGTH) { // >= CHAPTERxxx
- try {
- String strId = key.substring(8, 10);
- return Integer.parseInt(strId);
- } catch (NumberFormatException e) {
- throw new VorbisCommentReaderException(e);
- }
- }
- throw new VorbisCommentReaderException("key is too short (" + key + ")");
- }
-
- /**
- * Get the string that comes after 'CHAPTERxxx', for example 'name' or
- * 'url'.
- */
- public static String getAttributeTypeFromKey(String key) {
- if (key.length() > CHAPTERXXX_LENGTH) {
- return key.substring(CHAPTERXXX_LENGTH);
- }
- return null;
- }
-
- @Override
- public int getChapterType() {
- return CHAPTERTYPE_VORBISCOMMENT_CHAPTER;
- }
-
- public int getVorbisCommentId() {
- return vorbisCommentId;
- }
-}
diff --git a/parser/media/src/main/java/de/danoeh/antennapod/parser/media/vorbis/VorbisCommentChapterReader.java b/parser/media/src/main/java/de/danoeh/antennapod/parser/media/vorbis/VorbisCommentChapterReader.java
index 82455d180..8290a547a 100644
--- a/parser/media/src/main/java/de/danoeh/antennapod/parser/media/vorbis/VorbisCommentChapterReader.java
+++ b/parser/media/src/main/java/de/danoeh/antennapod/parser/media/vorbis/VorbisCommentChapterReader.java
@@ -2,8 +2,10 @@ package de.danoeh.antennapod.parser.media.vorbis;
import android.util.Log;
+import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
+import java.util.concurrent.TimeUnit;
import de.danoeh.antennapod.model.feed.Chapter;
import de.danoeh.antennapod.parser.media.BuildConfig;
@@ -14,26 +16,17 @@ public class VorbisCommentChapterReader extends VorbisCommentReader {
private static final String CHAPTER_KEY = "chapter\\d\\d\\d.*";
private static final String CHAPTER_ATTRIBUTE_TITLE = "name";
private static final String CHAPTER_ATTRIBUTE_LINK = "url";
+ private static final int CHAPTERXXX_LENGTH = "chapterxxx".length();
- private List<Chapter> chapters;
+ private final List<Chapter> chapters = new ArrayList<>();
- public VorbisCommentChapterReader() {
+ public VorbisCommentChapterReader(InputStream input) {
+ super(input);
}
@Override
- public void onVorbisCommentFound() {
- System.out.println("Vorbis comment found");
- }
-
- @Override
- public void onVorbisCommentHeaderFound(VorbisCommentHeader header) {
- chapters = new ArrayList<>();
- System.out.println(header.toString());
- }
-
- @Override
- public boolean onContentVectorKey(String content) {
- return content.matches(CHAPTER_KEY);
+ public boolean handles(String key) {
+ return key.matches(CHAPTER_KEY);
}
@Override
@@ -41,14 +34,15 @@ public class VorbisCommentChapterReader extends VorbisCommentReader {
if (BuildConfig.DEBUG) {
Log.d(TAG, "Key: " + key + ", value: " + value);
}
- String attribute = VorbisCommentChapter.getAttributeTypeFromKey(key);
- int id = VorbisCommentChapter.getIDFromKey(key);
+ String attribute = getAttributeTypeFromKey(key);
+ int id = getIdFromKey(key);
Chapter chapter = getChapterById(id);
if (attribute == null) {
if (getChapterById(id) == null) {
// new chapter
- long start = VorbisCommentChapter.getStartTimeFromValue(value);
- chapter = new VorbisCommentChapter(id);
+ long start = getStartTimeFromValue(value);
+ chapter = new Chapter();
+ chapter.setChapterId("" + id);
chapter.setStart(start);
chapters.add(chapter);
} else {
@@ -65,25 +59,64 @@ public class VorbisCommentChapterReader extends VorbisCommentReader {
}
}
- @Override
- public void onEndOfComment() {
- System.out.println("End of comment");
+ private Chapter getChapterById(long id) {
for (Chapter c : chapters) {
- System.out.println(c.toString());
+ if (("" + id).equals(c.getChapterId())) {
+ return c;
+ }
}
+ return null;
}
- @Override
- public void onError(VorbisCommentReaderException exception) {
- exception.printStackTrace();
+ public static long getStartTimeFromValue(String value)
+ throws VorbisCommentReaderException {
+ String[] parts = value.split(":");
+ if (parts.length >= 3) {
+ try {
+ long hours = TimeUnit.MILLISECONDS.convert(
+ Long.parseLong(parts[0]), TimeUnit.HOURS);
+ long minutes = TimeUnit.MILLISECONDS.convert(
+ Long.parseLong(parts[1]), TimeUnit.MINUTES);
+ if (parts[2].contains("-->")) {
+ parts[2] = parts[2].substring(0, parts[2].indexOf("-->"));
+ }
+ long seconds = TimeUnit.MILLISECONDS.convert(
+ ((long) Float.parseFloat(parts[2])), TimeUnit.SECONDS);
+ return hours + minutes + seconds;
+ } catch (NumberFormatException e) {
+ throw new VorbisCommentReaderException(e);
+ }
+ } else {
+ throw new VorbisCommentReaderException("Invalid time string");
+ }
}
- private Chapter getChapterById(long id) {
- for (Chapter c : chapters) {
- if (((VorbisCommentChapter) c).getVorbisCommentId() == id) {
- return c;
+ /**
+ * Return the id of a vorbiscomment chapter from a string like CHAPTERxxx*
+ *
+ * @return the id of the chapter key or -1 if the id couldn't be read.
+ * @throws VorbisCommentReaderException
+ * */
+ private static int getIdFromKey(String key) throws VorbisCommentReaderException {
+ if (key.length() >= CHAPTERXXX_LENGTH) { // >= CHAPTERxxx
+ try {
+ String strId = key.substring(8, 10);
+ return Integer.parseInt(strId);
+ } catch (NumberFormatException e) {
+ throw new VorbisCommentReaderException(e);
}
}
+ throw new VorbisCommentReaderException("key is too short (" + key + ")");
+ }
+
+ /**
+ * Get the string that comes after 'CHAPTERxxx', for example 'name' or
+ * 'url'.
+ */
+ private static String getAttributeTypeFromKey(String key) {
+ if (key.length() > CHAPTERXXX_LENGTH) {
+ return key.substring(CHAPTERXXX_LENGTH);
+ }
return null;
}
diff --git a/parser/media/src/main/java/de/danoeh/antennapod/parser/media/vorbis/VorbisCommentMetadataReader.java b/parser/media/src/main/java/de/danoeh/antennapod/parser/media/vorbis/VorbisCommentMetadataReader.java
new file mode 100644
index 000000000..158a0d8f7
--- /dev/null
+++ b/parser/media/src/main/java/de/danoeh/antennapod/parser/media/vorbis/VorbisCommentMetadataReader.java
@@ -0,0 +1,32 @@
+package de.danoeh.antennapod.parser.media.vorbis;
+
+import java.io.InputStream;
+
+public class VorbisCommentMetadataReader extends VorbisCommentReader {
+ private static final String KEY_DESCRIPTION = "description";
+ private static final String KEY_COMMENT = "comment";
+
+ private String description = null;
+
+ public VorbisCommentMetadataReader(InputStream input) {
+ super(input);
+ }
+
+ @Override
+ public boolean handles(String key) {
+ return KEY_DESCRIPTION.equals(key) || KEY_COMMENT.equals(key);
+ }
+
+ @Override
+ public void onContentVectorValue(String key, String value) {
+ if (KEY_DESCRIPTION.equals(key) || KEY_COMMENT.equals(key)) {
+ if (description == null || value.length() > description.length()) {
+ description = value;
+ }
+ }
+ }
+
+ public String getDescription() {
+ return description;
+ }
+}
diff --git a/parser/media/src/main/java/de/danoeh/antennapod/parser/media/vorbis/VorbisCommentReader.java b/parser/media/src/main/java/de/danoeh/antennapod/parser/media/vorbis/VorbisCommentReader.java
index 3d5f29f17..13126a73d 100644
--- a/parser/media/src/main/java/de/danoeh/antennapod/parser/media/vorbis/VorbisCommentReader.java
+++ b/parser/media/src/main/java/de/danoeh/antennapod/parser/media/vorbis/VorbisCommentReader.java
@@ -3,6 +3,7 @@ package de.danoeh.antennapod.parser.media.vorbis;
import androidx.annotation.NonNull;
import org.apache.commons.io.EndianUtils;
import org.apache.commons.io.IOUtils;
+import android.util.Log;
import java.io.IOException;
import java.io.InputStream;
@@ -12,51 +13,35 @@ import java.nio.charset.Charset;
import java.util.Locale;
public abstract class VorbisCommentReader {
- /** Length of first page in an ogg file in bytes. */
+ private static final String TAG = "VorbisCommentReader";
private static final int FIRST_OGG_PAGE_LENGTH = 58;
private static final int FIRST_OPUS_PAGE_LENGTH = 47;
private static final int SECOND_PAGE_MAX_LENGTH = 64 * 1024 * 1024;
private static final int PACKET_TYPE_IDENTIFICATION = 1;
private static final int PACKET_TYPE_COMMENT = 3;
- /** Called when Reader finds identification header. */
- protected abstract void onVorbisCommentFound();
+ private final InputStream input;
- protected abstract void onVorbisCommentHeaderFound(VorbisCommentHeader header);
-
- /**
- * Is called every time the Reader finds a content vector. The handler
- * should return true if it wants to handle the content vector.
- */
- protected abstract boolean onContentVectorKey(String content);
-
- /**
- * Is called if onContentVectorKey returned true for the key.
- */
- protected abstract void onContentVectorValue(String key, String value) throws VorbisCommentReaderException;
-
- protected abstract void onEndOfComment();
-
- protected abstract void onError(VorbisCommentReaderException exception);
+ VorbisCommentReader(InputStream input) {
+ this.input = input;
+ }
- public void readInputStream(InputStream input) throws VorbisCommentReaderException {
+ public void readInputStream() throws VorbisCommentReaderException {
try {
- findIdentificationHeader(input);
- onVorbisCommentFound();
- findOggPage(input);
- findCommentHeader(input);
- VorbisCommentHeader commentHeader = readCommentHeader(input);
- onVorbisCommentHeaderFound(commentHeader);
+ findIdentificationHeader();
+ findOggPage();
+ findCommentHeader();
+ VorbisCommentHeader commentHeader = readCommentHeader();
+ Log.d(TAG, commentHeader.toString());
for (int i = 0; i < commentHeader.getUserCommentLength(); i++) {
- readUserComment(input);
+ readUserComment();
}
- onEndOfComment();
} catch (IOException e) {
- onError(new VorbisCommentReaderException(e));
+ e.printStackTrace();
}
}
- private void findOggPage(InputStream input) throws IOException {
+ private void findOggPage() throws IOException {
// find OggS
byte[] buffer = new byte[4];
final byte[] oggPageHeader = {'O', 'g', 'g', 'S'};
@@ -76,17 +61,19 @@ public abstract class VorbisCommentReader {
IOUtils.skipFully(input, numSegments);
}
- private void readUserComment(InputStream input) throws VorbisCommentReaderException {
+ private void readUserComment() throws VorbisCommentReaderException {
try {
long vectorLength = EndianUtils.readSwappedUnsignedInteger(input);
if (vectorLength > 20 * 1024 * 1024) {
- // Avoid reading entire file if it is encoded incorrectly
- throw new VorbisCommentReaderException("User comment unrealistically long: " + vectorLength);
+ String keyPart = readUtf8String(10);
+ throw new VorbisCommentReaderException("User comment unrealistically long. "
+ + "key=" + keyPart + ", length=" + vectorLength);
}
- String key = readContentVectorKey(input, vectorLength).toLowerCase(Locale.US);
- boolean readValue = onContentVectorKey(key);
- if (readValue) {
- String value = readUtf8String(input, vectorLength - key.length() - 1);
+ String key = readContentVectorKey(vectorLength).toLowerCase(Locale.US);
+ boolean shouldReadValue = handles(key);
+ Log.d(TAG, "key=" + key + ", length=" + vectorLength + ", handles=" + shouldReadValue);
+ if (shouldReadValue) {
+ String value = readUtf8String(vectorLength - key.length() - 1);
onContentVectorValue(key, value);
} else {
IOUtils.skipFully(input, vectorLength - key.length() - 1);
@@ -96,7 +83,7 @@ public abstract class VorbisCommentReader {
}
}
- private String readUtf8String(InputStream input, long length) throws IOException {
+ private String readUtf8String(long length) throws IOException {
byte[] buffer = new byte[(int) length];
IOUtils.readFully(input, buffer);
Charset charset = Charset.forName("UTF-8");
@@ -107,7 +94,7 @@ public abstract class VorbisCommentReader {
* Looks for an identification header in the first page of the file. If an
* identification header is found, it will be skipped completely
*/
- private void findIdentificationHeader(InputStream input) throws IOException {
+ private void findIdentificationHeader() throws IOException {
byte[] buffer = new byte[FIRST_OPUS_PAGE_LENGTH];
IOUtils.readFully(input, buffer);
final byte[] oggIdentificationHeader = new byte[]{ PACKET_TYPE_IDENTIFICATION, 'v', 'o', 'r', 'b', 'i', 's' };
@@ -122,7 +109,7 @@ public abstract class VorbisCommentReader {
throw new IOException("No vorbis identification header found");
}
- private void findCommentHeader(InputStream input) throws IOException {
+ private void findCommentHeader() throws IOException {
byte[] buffer = new byte[64]; // Enough space for some bytes. Used circularly.
final byte[] oggCommentHeader = new byte[]{ PACKET_TYPE_COMMENT, 'v', 'o', 'r', 'b', 'i', 's' };
for (int bytesRead = 0; bytesRead < SECOND_PAGE_MAX_LENGTH; bytesRead++) {
@@ -155,10 +142,10 @@ public abstract class VorbisCommentReader {
}
@NonNull
- private VorbisCommentHeader readCommentHeader(InputStream input) throws IOException, VorbisCommentReaderException {
+ private VorbisCommentHeader readCommentHeader() throws IOException, VorbisCommentReaderException {
try {
long vendorLength = EndianUtils.readSwappedUnsignedInteger(input);
- String vendorName = readUtf8String(input, vendorLength);
+ String vendorName = readUtf8String(vendorLength);
long userCommentLength = EndianUtils.readSwappedUnsignedInteger(input);
return new VorbisCommentHeader(vendorName, userCommentLength);
} catch (UnsupportedEncodingException e) {
@@ -166,7 +153,7 @@ public abstract class VorbisCommentReader {
}
}
- private String readContentVectorKey(InputStream input, long vectorLength) throws IOException {
+ private String readContentVectorKey(long vectorLength) throws IOException {
StringBuilder builder = new StringBuilder();
for (int i = 0; i < vectorLength; i++) {
char c = (char) input.read();
@@ -178,4 +165,15 @@ public abstract class VorbisCommentReader {
}
return null; // no key found
}
+
+ /**
+ * Is called every time the Reader finds a content vector. The handler
+ * should return true if it wants to handle the content vector.
+ */
+ protected abstract boolean handles(String key);
+
+ /**
+ * Is called if onContentVectorKey returned true for the key.
+ */
+ protected abstract void onContentVectorValue(String key, String value) throws VorbisCommentReaderException;
}
diff --git a/parser/media/src/main/java/de/danoeh/antennapod/parser/media/vorbis/VorbisCommentReaderException.java b/parser/media/src/main/java/de/danoeh/antennapod/parser/media/vorbis/VorbisCommentReaderException.java
index 8de1b29c0..f1a46bda0 100644
--- a/parser/media/src/main/java/de/danoeh/antennapod/parser/media/vorbis/VorbisCommentReaderException.java
+++ b/parser/media/src/main/java/de/danoeh/antennapod/parser/media/vorbis/VorbisCommentReaderException.java
@@ -3,14 +3,6 @@ package de.danoeh.antennapod.parser.media.vorbis;
public class VorbisCommentReaderException extends Exception {
private static final long serialVersionUID = 1L;
- public VorbisCommentReaderException() {
- super();
- }
-
- public VorbisCommentReaderException(String message, Throwable cause) {
- super(message, cause);
- }
-
public VorbisCommentReaderException(String message) {
super(message);
}
diff --git a/parser/media/src/test/java/de/danoeh/antennapod/parser/media/id3/ChapterReaderTest.java b/parser/media/src/test/java/de/danoeh/antennapod/parser/media/id3/ChapterReaderTest.java
index f87764c7f..d7321f766 100644
--- a/parser/media/src/test/java/de/danoeh/antennapod/parser/media/id3/ChapterReaderTest.java
+++ b/parser/media/src/test/java/de/danoeh/antennapod/parser/media/id3/ChapterReaderTest.java
@@ -96,7 +96,7 @@ public class ChapterReaderTest {
FrameHeader header = new FrameHeader(ChapterReader.FRAME_ID_TITLE, titleSubframeContent.length, (short) 0);
CountingInputStream inputStream = new CountingInputStream(new ByteArrayInputStream(titleSubframeContent));
ChapterReader reader = new ChapterReader(inputStream);
- Chapter chapter = new ID3Chapter("", 0);
+ Chapter chapter = new Chapter();
reader.readChapterSubFrame(header, chapter);
assertEquals("A", chapter.getTitle());
diff --git a/parser/media/src/test/java/de/danoeh/antennapod/parser/media/id3/MetadataReaderTest.java b/parser/media/src/test/java/de/danoeh/antennapod/parser/media/id3/MetadataReaderTest.java
new file mode 100644
index 000000000..5b6b06ea7
--- /dev/null
+++ b/parser/media/src/test/java/de/danoeh/antennapod/parser/media/id3/MetadataReaderTest.java
@@ -0,0 +1,50 @@
+package de.danoeh.antennapod.parser.media.id3;
+
+import org.apache.commons.io.input.CountingInputStream;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+import java.io.IOException;
+
+import static org.junit.Assert.assertEquals;
+
+@RunWith(RobolectricTestRunner.class)
+public class MetadataReaderTest {
+ @Test
+ public void testRealFileUltraschall() throws IOException, ID3ReaderException {
+ CountingInputStream inputStream = new CountingInputStream(getClass().getClassLoader()
+ .getResource("ultraschall5.mp3").openStream());
+ Id3MetadataReader reader = new Id3MetadataReader(inputStream);
+ reader.readInputStream();
+ assertEquals("Description", reader.getComment());
+ }
+
+ @Test
+ public void testRealFileAuphonic() throws IOException, ID3ReaderException {
+ CountingInputStream inputStream = new CountingInputStream(getClass().getClassLoader()
+ .getResource("auphonic.mp3").openStream());
+ Id3MetadataReader reader = new Id3MetadataReader(inputStream);
+ reader.readInputStream();
+ assertEquals("Summary", reader.getComment());
+ }
+
+ @Test
+ public void testRealFileHindenburgJournalistPro() throws IOException, ID3ReaderException {
+ CountingInputStream inputStream = new CountingInputStream(getClass().getClassLoader()
+ .getResource("hindenburg-journalist-pro.mp3").openStream());
+ Id3MetadataReader reader = new Id3MetadataReader(inputStream);
+ reader.readInputStream();
+ assertEquals("This is the summary of this podcast episode. This file was made with"
+ + " Hindenburg Journalist Pro version 1.85, build number 2360.", reader.getComment());
+ }
+
+ @Test
+ public void testRealFileMp3chapsPy() throws IOException, ID3ReaderException {
+ CountingInputStream inputStream = new CountingInputStream(getClass().getClassLoader()
+ .getResource("mp3chaps-py.mp3").openStream());
+ Id3MetadataReader reader = new Id3MetadataReader(inputStream);
+ reader.readInputStream();
+ assertEquals("2021.08.13", reader.getComment());
+ }
+}
diff --git a/parser/media/src/test/java/de/danoeh/antennapod/parser/media/vorbis/VorbisCommentChapterReaderTest.java b/parser/media/src/test/java/de/danoeh/antennapod/parser/media/vorbis/VorbisCommentChapterReaderTest.java
index 6ebe875d9..b19be324d 100644
--- a/parser/media/src/test/java/de/danoeh/antennapod/parser/media/vorbis/VorbisCommentChapterReaderTest.java
+++ b/parser/media/src/test/java/de/danoeh/antennapod/parser/media/vorbis/VorbisCommentChapterReaderTest.java
@@ -23,8 +23,8 @@ public class VorbisCommentChapterReaderTest {
public void testRealFileAuphonic(String filename) throws IOException, VorbisCommentReaderException {
InputStream inputStream = getClass().getClassLoader()
.getResource(filename).openStream();
- VorbisCommentChapterReader reader = new VorbisCommentChapterReader();
- reader.readInputStream(inputStream);
+ VorbisCommentChapterReader reader = new VorbisCommentChapterReader(inputStream);
+ reader.readInputStream();
List<Chapter> chapters = reader.getChapters();
assertEquals(4, chapters.size());
diff --git a/parser/media/src/test/java/de/danoeh/antennapod/parser/media/vorbis/VorbisCommentMetadataReaderTest.java b/parser/media/src/test/java/de/danoeh/antennapod/parser/media/vorbis/VorbisCommentMetadataReaderTest.java
new file mode 100644
index 000000000..7f71c8fa9
--- /dev/null
+++ b/parser/media/src/test/java/de/danoeh/antennapod/parser/media/vorbis/VorbisCommentMetadataReaderTest.java
@@ -0,0 +1,28 @@
+package de.danoeh.antennapod.parser.media.vorbis;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import static org.junit.Assert.assertEquals;
+
+@RunWith(RobolectricTestRunner.class)
+public class VorbisCommentMetadataReaderTest {
+
+ @Test
+ public void testRealFilesAuphonic() throws IOException, VorbisCommentReaderException {
+ testRealFileAuphonic("auphonic.ogg");
+ testRealFileAuphonic("auphonic.opus");
+ }
+
+ public void testRealFileAuphonic(String filename) throws IOException, VorbisCommentReaderException {
+ InputStream inputStream = getClass().getClassLoader()
+ .getResource(filename).openStream();
+ VorbisCommentMetadataReader reader = new VorbisCommentMetadataReader(inputStream);
+ reader.readInputStream();
+ assertEquals("Summary", reader.getDescription());
+ }
+}
diff --git a/playback/base/build.gradle b/playback/base/build.gradle
index a1d344492..df49b2fb6 100644
--- a/playback/base/build.gradle
+++ b/playback/base/build.gradle
@@ -1,4 +1,6 @@
-apply plugin: "com.android.library"
+plugins {
+ id("com.android.library")
+}
apply from: "../../common.gradle"
dependencies {
diff --git a/playback/base/src/main/java/de/danoeh/antennapod/playback/base/PlaybackServiceMediaPlayer.java b/playback/base/src/main/java/de/danoeh/antennapod/playback/base/PlaybackServiceMediaPlayer.java
index d03695896..a2ccb3f88 100644
--- a/playback/base/src/main/java/de/danoeh/antennapod/playback/base/PlaybackServiceMediaPlayer.java
+++ b/playback/base/src/main/java/de/danoeh/antennapod/playback/base/PlaybackServiceMediaPlayer.java
@@ -179,12 +179,6 @@ public abstract class PlaybackServiceMediaPlayer {
*/
public abstract void shutdown();
- /**
- * Releases internally used resources. This method should only be called when the object is not used anymore.
- * This method is executed on an internal executor service.
- */
- public abstract void shutdownQuietly();
-
public abstract void setVideoSurface(SurfaceHolder surface);
public abstract void resetVideoSurface();
diff --git a/playback/base/src/main/java/de/danoeh/antennapod/playback/base/PlayerStatus.java b/playback/base/src/main/java/de/danoeh/antennapod/playback/base/PlayerStatus.java
index d995ae21f..154fc0c83 100644
--- a/playback/base/src/main/java/de/danoeh/antennapod/playback/base/PlayerStatus.java
+++ b/playback/base/src/main/java/de/danoeh/antennapod/playback/base/PlayerStatus.java
@@ -23,10 +23,6 @@ public enum PlayerStatus {
statusValue = val;
}
- public static PlayerStatus fromOrdinal(int o) {
- return fromOrdinalLookup[o];
- }
-
public boolean isAtLeast(PlayerStatus other) {
return other == null || this.statusValue >= other.statusValue;
}
diff --git a/playback/cast/build.gradle b/playback/cast/build.gradle
index c51354838..64c16eb26 100644
--- a/playback/cast/build.gradle
+++ b/playback/cast/build.gradle
@@ -1,4 +1,6 @@
-apply plugin: "com.android.library"
+plugins {
+ id("com.android.library")
+}
apply from: "../../common.gradle"
apply from: "../../playFlavor.gradle"
diff --git a/playback/cast/src/play/java/de/danoeh/antennapod/playback/cast/CastEnabledActivity.java b/playback/cast/src/play/java/de/danoeh/antennapod/playback/cast/CastEnabledActivity.java
index 2cebde6a3..83abd98b7 100644
--- a/playback/cast/src/play/java/de/danoeh/antennapod/playback/cast/CastEnabledActivity.java
+++ b/playback/cast/src/play/java/de/danoeh/antennapod/playback/cast/CastEnabledActivity.java
@@ -13,7 +13,6 @@ import com.google.android.gms.common.GoogleApiAvailability;
* network.
*/
public abstract class CastEnabledActivity extends AppCompatActivity {
- private static final String TAG = "CastEnabledActivity";
private boolean canCast = false;
@Override
diff --git a/playback/cast/src/play/java/de/danoeh/antennapod/playback/cast/CastPsmp.java b/playback/cast/src/play/java/de/danoeh/antennapod/playback/cast/CastPsmp.java
index 8e74154e8..d3c4f3468 100644
--- a/playback/cast/src/play/java/de/danoeh/antennapod/playback/cast/CastPsmp.java
+++ b/playback/cast/src/play/java/de/danoeh/antennapod/playback/cast/CastPsmp.java
@@ -456,11 +456,6 @@ public class CastPsmp extends PlaybackServiceMediaPlayer {
}
@Override
- public void shutdownQuietly() {
- shutdown();
- }
-
- @Override
public void setVideoSurface(SurfaceHolder surface) {
throw new UnsupportedOperationException("Setting Video Surface unsupported in Remote Media Player");
}
diff --git a/settings.gradle b/settings.gradle
index c7f5e6449..0cf0366fe 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -3,6 +3,7 @@ include ':core'
include ':event'
include ':model'
+include ':net:discovery'
include ':net:ssl'
include ':net:sync:gpoddernet'
include ':net:sync:model'
@@ -13,6 +14,10 @@ include ':parser:media'
include ':playback:base'
include ':playback:cast'
+include ':storage:database'
+
include ':ui:app-start-intent'
include ':ui:common'
+include ':ui:i18n'
include ':ui:png-icons'
+include ':ui:statistics'
diff --git a/storage/README.md b/storage/README.md
new file mode 100644
index 000000000..adabdc7a6
--- /dev/null
+++ b/storage/README.md
@@ -0,0 +1,3 @@
+# :storage
+
+Data storage for podcast data (subscriptions, playback state) and settings (but not settings UI).
diff --git a/storage/database/README.md b/storage/database/README.md
new file mode 100644
index 000000000..45af475f2
--- /dev/null
+++ b/storage/database/README.md
@@ -0,0 +1,3 @@
+# :storage:database
+
+AntennaPod's main database, containing subscriptions and playback state (but not user settings).
diff --git a/storage/database/build.gradle b/storage/database/build.gradle
new file mode 100644
index 000000000..141cdb086
--- /dev/null
+++ b/storage/database/build.gradle
@@ -0,0 +1,17 @@
+plugins {
+ id("com.android.library")
+}
+apply from: "../../common.gradle"
+
+android {
+ lintOptions {
+ disable "StaticFieldLeak"
+ }
+}
+
+dependencies {
+ implementation project(':model')
+
+ annotationProcessor "androidx.annotation:annotation:$annotationVersion"
+ implementation "commons-io:commons-io:$commonsioVersion"
+}
diff --git a/storage/database/src/main/AndroidManifest.xml b/storage/database/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..df0171d12
--- /dev/null
+++ b/storage/database/src/main/AndroidManifest.xml
@@ -0,0 +1 @@
+<manifest package="de.danoeh.antennapod.storage.database" />
diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/DBUpgrader.java b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/DBUpgrader.java
index 4e0a6aeda..78eaf6964 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/storage/DBUpgrader.java
+++ b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/DBUpgrader.java
@@ -1,4 +1,4 @@
-package de.danoeh.antennapod.core.storage;
+package de.danoeh.antennapod.storage.database;
import android.content.ContentValues;
import android.database.Cursor;
@@ -39,7 +39,7 @@ class DBUpgrader {
}
if (oldVersion <= 6) {
db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_SIMPLECHAPTERS
- + " ADD COLUMN " + PodDBAdapter.KEY_CHAPTER_TYPE + " INTEGER");
+ + " ADD COLUMN type INTEGER");
}
if (oldVersion <= 7) {
db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEED_MEDIA
@@ -117,7 +117,7 @@ class DBUpgrader {
PodDBAdapter.KEY_START,
PodDBAdapter.KEY_FEEDITEM,
PodDBAdapter.KEY_LINK,
- PodDBAdapter.KEY_CHAPTER_TYPE));
+ "type"));
}
if (oldVersion <= 14) {
db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEED_ITEMS
@@ -326,6 +326,10 @@ class DBUpgrader {
db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS
+ " ADD COLUMN " + PodDBAdapter.KEY_MINIMAL_DURATION_FILTER + " INTEGER DEFAULT -1");
}
+ if (oldVersion < 2060000) {
+ db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEED_ITEMS
+ + " ADD COLUMN " + PodDBAdapter.KEY_PODCASTINDEX_CHAPTER_URL + " TEXT");
+ }
}
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/PodDBAdapter.java b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/PodDBAdapter.java
index 43d9c7f11..173354197 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/storage/PodDBAdapter.java
+++ b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/PodDBAdapter.java
@@ -1,4 +1,4 @@
-package de.danoeh.antennapod.core.storage;
+package de.danoeh.antennapod.storage.database;
import android.content.ContentValues;
import android.content.Context;
@@ -17,9 +17,8 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
+import de.danoeh.antennapod.model.feed.FeedCounter;
import de.danoeh.antennapod.model.feed.FeedFunding;
-import de.danoeh.antennapod.core.storage.mapper.FeedItemFilterQuery;
-import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.IOException;
@@ -36,10 +35,10 @@ import de.danoeh.antennapod.model.feed.FeedItem;
import de.danoeh.antennapod.model.feed.FeedItemFilter;
import de.danoeh.antennapod.model.feed.FeedMedia;
import de.danoeh.antennapod.model.feed.FeedPreferences;
-import de.danoeh.antennapod.core.preferences.UserPreferences;
-import de.danoeh.antennapod.core.service.download.DownloadStatus;
-import de.danoeh.antennapod.core.util.LongIntMap;
+import de.danoeh.antennapod.model.download.DownloadStatus;
import de.danoeh.antennapod.model.feed.SortOrder;
+import de.danoeh.antennapod.storage.database.mapper.FeedItemFilterQuery;
+import org.apache.commons.io.FileUtils;
import static de.danoeh.antennapod.model.feed.FeedPreferences.SPEED_USE_GLOBAL;
import static de.danoeh.antennapod.model.feed.SortOrder.toCodeString;
@@ -51,7 +50,7 @@ public class PodDBAdapter {
private static final String TAG = "PodDBAdapter";
public static final String DATABASE_NAME = "Antennapod.db";
- public static final int VERSION = 2050000;
+ public static final int VERSION = 2060000;
/**
* Maximum number of arguments for IN-operator.
@@ -93,7 +92,6 @@ public class PodDBAdapter {
public static final String KEY_FEED_IDENTIFIER = "feed_identifier";
public static final String KEY_REASON_DETAILED = "reason_detailed";
public static final String KEY_DOWNLOADSTATUS_TITLE = "title";
- public static final String KEY_CHAPTER_TYPE = "type";
public static final String KEY_PLAYBACK_COMPLETION_DATE = "playback_completion_date";
public static final String KEY_AUTO_DOWNLOAD_ATTEMPTS = "auto_download";
public static final String KEY_AUTO_DOWNLOAD_ENABLED = "auto_download"; // Both tables use the same key
@@ -118,6 +116,7 @@ public class PodDBAdapter {
public static final String KEY_FEED_SKIP_ENDING = "feed_skip_ending";
public static final String KEY_FEED_TAGS = "tags";
public static final String KEY_EPISODE_NOTIFICATION = "episode_notification";
+ public static final String KEY_PODCASTINDEX_CHAPTER_URL = "podcastindex_chapter_url";
// Table names
public static final String TABLE_NAME_FEEDS = "Feeds";
@@ -168,7 +167,8 @@ public class PodDBAdapter {
+ KEY_MEDIA + " INTEGER," + KEY_FEED + " INTEGER,"
+ KEY_HAS_CHAPTERS + " INTEGER," + KEY_ITEM_IDENTIFIER + " TEXT,"
+ KEY_IMAGE_URL + " TEXT,"
- + KEY_AUTO_DOWNLOAD_ATTEMPTS + " INTEGER)";
+ + KEY_AUTO_DOWNLOAD_ATTEMPTS + " INTEGER,"
+ + KEY_PODCASTINDEX_CHAPTER_URL + " TEXT)";
private static final String CREATE_TABLE_FEED_MEDIA = "CREATE TABLE "
+ TABLE_NAME_FEED_MEDIA + " (" + TABLE_PRIMARY_KEY + KEY_DURATION
@@ -195,7 +195,7 @@ public class PodDBAdapter {
private static final String CREATE_TABLE_SIMPLECHAPTERS = "CREATE TABLE "
+ TABLE_NAME_SIMPLECHAPTERS + " (" + TABLE_PRIMARY_KEY + KEY_TITLE
+ " TEXT," + KEY_START + " INTEGER," + KEY_FEEDITEM + " INTEGER,"
- + KEY_LINK + " TEXT," + KEY_IMAGE_URL + " TEXT," + KEY_CHAPTER_TYPE + " INTEGER)";
+ + KEY_LINK + " TEXT," + KEY_IMAGE_URL + " TEXT)";
// SQL Statements for creating indexes
static final String CREATE_INDEX_FEEDITEMS_FEED = "CREATE INDEX "
@@ -294,7 +294,8 @@ public class PodDBAdapter {
+ TABLE_NAME_FEED_ITEMS + "." + KEY_HAS_CHAPTERS + ", "
+ TABLE_NAME_FEED_ITEMS + "." + KEY_ITEM_IDENTIFIER + ", "
+ TABLE_NAME_FEED_ITEMS + "." + KEY_IMAGE_URL + ", "
- + TABLE_NAME_FEED_ITEMS + "." + KEY_AUTO_DOWNLOAD_ATTEMPTS;
+ + TABLE_NAME_FEED_ITEMS + "." + KEY_AUTO_DOWNLOAD_ATTEMPTS + ", "
+ + TABLE_NAME_FEED_ITEMS + "." + KEY_PODCASTINDEX_CHAPTER_URL;
private static final String KEYS_FEED_MEDIA =
TABLE_NAME_FEED_MEDIA + "." + KEY_ID + " AS " + SELECT_KEY_MEDIA_ID + ", "
@@ -650,6 +651,7 @@ public class PodDBAdapter {
values.put(KEY_ITEM_IDENTIFIER, item.getItemIdentifier());
values.put(KEY_AUTO_DOWNLOAD_ATTEMPTS, item.getAutoDownloadAttemptsAndTime());
values.put(KEY_IMAGE_URL, item.getImageUrl());
+ values.put(KEY_PODCASTINDEX_CHAPTER_URL, item.getPodcastIndexChapterUrl());
if (item.getId() == 0) {
item.setId(db.insert(TABLE_NAME_FEED_ITEMS, null, values));
@@ -720,7 +722,6 @@ public class PodDBAdapter {
values.put(KEY_FEEDITEM, item.getId());
values.put(KEY_LINK, chapter.getLink());
values.put(KEY_IMAGE_URL, chapter.getImageUrl());
- values.put(KEY_CHAPTER_TYPE, chapter.getChapterType());
if (chapter.getId() == 0) {
chapter.setId(db.insert(TABLE_NAME_SIMPLECHAPTERS, null, values));
} else {
@@ -737,7 +738,7 @@ public class PodDBAdapter {
db.execSQL(sql);
}
- void setFeedCustomTitle(long feedId, String customTitle) {
+ public void setFeedCustomTitle(long feedId, String customTitle) {
ContentValues values = new ContentValues();
values.put(KEY_CUSTOM_TITLE, customTitle);
db.update(TABLE_NAME_FEEDS, values, KEY_ID + "=?", new String[]{String.valueOf(feedId)});
@@ -1133,6 +1134,17 @@ public class PodDBAdapter {
return db.rawQuery(query, null);
}
+ public final Cursor getMonthlyStatisticsCursor() {
+ final String query = "SELECT SUM(" + KEY_PLAYED_DURATION + ") AS total_duration"
+ + ", strftime('%m', datetime(" + KEY_LAST_PLAYED_TIME + "/1000, 'unixepoch')) AS month"
+ + ", strftime('%Y', datetime(" + KEY_LAST_PLAYED_TIME + "/1000, 'unixepoch')) AS year"
+ + " FROM " + TABLE_NAME_FEED_MEDIA
+ + " WHERE " + KEY_LAST_PLAYED_TIME + " > 0 AND " + KEY_PLAYED_DURATION + " > 0"
+ + " GROUP BY year, month"
+ + " ORDER BY year, month";
+ return db.rawQuery(query, null);
+ }
+
public int getQueueSize() {
final String query = String.format("SELECT COUNT(%s) FROM %s", KEY_ID, TABLE_NAME_QUEUE);
Cursor c = db.rawQuery(query, null);
@@ -1163,37 +1175,31 @@ public class PodDBAdapter {
return result;
}
- public final LongIntMap getFeedCounters(long... feedIds) {
- int setting = UserPreferences.getFeedCounterSetting();
-
- return getFeedCounters(setting, feedIds);
- }
-
- public final LongIntMap getFeedCounters(int setting, long... feedIds) {
+ public final Map<Long, Integer> getFeedCounters(FeedCounter setting, long... feedIds) {
String whereRead;
switch (setting) {
- case UserPreferences.FEED_COUNTER_SHOW_NEW_UNPLAYED_SUM:
+ case SHOW_NEW_UNPLAYED_SUM:
whereRead = "(" + KEY_READ + "=" + FeedItem.NEW +
" OR " + KEY_READ + "=" + FeedItem.UNPLAYED + ")";
break;
- case UserPreferences.FEED_COUNTER_SHOW_NEW:
+ case SHOW_NEW:
whereRead = KEY_READ + "=" + FeedItem.NEW;
break;
- case UserPreferences.FEED_COUNTER_SHOW_UNPLAYED:
+ case SHOW_UNPLAYED:
whereRead = KEY_READ + "=" + FeedItem.UNPLAYED;
break;
- case UserPreferences.FEED_COUNTER_SHOW_DOWNLOADED:
+ case SHOW_DOWNLOADED:
whereRead = KEY_DOWNLOADED + "=1";
break;
- case UserPreferences.FEED_COUNTER_SHOW_NONE:
+ case SHOW_NONE:
// deliberate fall-through
default: // NONE
- return new LongIntMap(0);
+ return new HashMap<>();
}
return conditionalFeedCounterRead(whereRead, feedIds);
}
- private LongIntMap conditionalFeedCounterRead(String whereRead, long... feedIds) {
+ private Map<Long, Integer> conditionalFeedCounterRead(String whereRead, long... feedIds) {
String limitFeeds = "";
if (feedIds.length > 0) {
// work around TextUtils.join wanting only boxed items
@@ -1216,7 +1222,7 @@ public class PodDBAdapter {
+ whereRead + " GROUP BY " + KEY_FEED;
Cursor c = db.rawQuery(query, null);
- LongIntMap result = new LongIntMap(c.getCount());
+ Map<Long, Integer> result = new HashMap<>();
if (c.moveToFirst()) {
do {
long feedId = c.getLong(0);
@@ -1228,7 +1234,7 @@ public class PodDBAdapter {
return result;
}
- public final LongIntMap getPlayedEpisodesCounters(long... feedIds) {
+ public final Map<Long, Integer> getPlayedEpisodesCounters(long... feedIds) {
String whereRead = KEY_READ + "=" + FeedItem.PLAYED;
return conditionalFeedCounterRead(whereRead, feedIds);
}
diff --git a/storage/database/src/main/java/de/danoeh/antennapod/storage/database/mapper/ChapterCursorMapper.java b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/mapper/ChapterCursorMapper.java
new file mode 100644
index 000000000..71e67812d
--- /dev/null
+++ b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/mapper/ChapterCursorMapper.java
@@ -0,0 +1,32 @@
+package de.danoeh.antennapod.storage.database.mapper;
+
+import android.database.Cursor;
+import androidx.annotation.NonNull;
+import de.danoeh.antennapod.model.feed.Chapter;
+import de.danoeh.antennapod.storage.database.PodDBAdapter;
+
+/**
+ * Converts a {@link Cursor} to a {@link Chapter} object.
+ */
+public abstract class ChapterCursorMapper {
+ /**
+ * Create a {@link Chapter} instance from a database row (cursor).
+ */
+ @NonNull
+ public static Chapter convert(@NonNull Cursor cursor) {
+ int indexId = cursor.getColumnIndex(PodDBAdapter.KEY_ID);
+ int indexTitle = cursor.getColumnIndex(PodDBAdapter.KEY_TITLE);
+ int indexStart = cursor.getColumnIndex(PodDBAdapter.KEY_START);
+ int indexLink = cursor.getColumnIndex(PodDBAdapter.KEY_LINK);
+ int indexImage = cursor.getColumnIndex(PodDBAdapter.KEY_IMAGE_URL);
+
+ long id = cursor.getLong(indexId);
+ String title = cursor.getString(indexTitle);
+ long start = cursor.getLong(indexStart);
+ String link = cursor.getString(indexLink);
+ String imageUrl = cursor.getString(indexImage);
+ Chapter chapter = new Chapter(start, title, link, imageUrl);
+ chapter.setId(id);
+ return chapter;
+ }
+}
diff --git a/storage/database/src/main/java/de/danoeh/antennapod/storage/database/mapper/DownloadStatusCursorMapper.java b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/mapper/DownloadStatusCursorMapper.java
new file mode 100644
index 000000000..4a5a792af
--- /dev/null
+++ b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/mapper/DownloadStatusCursorMapper.java
@@ -0,0 +1,35 @@
+package de.danoeh.antennapod.storage.database.mapper;
+
+import android.database.Cursor;
+import androidx.annotation.NonNull;
+import de.danoeh.antennapod.model.download.DownloadStatus;
+import de.danoeh.antennapod.model.download.DownloadError;
+import de.danoeh.antennapod.storage.database.PodDBAdapter;
+
+import java.util.Date;
+
+/**
+ * Converts a {@link Cursor} to a {@link DownloadStatus} object.
+ */
+public abstract class DownloadStatusCursorMapper {
+ /**
+ * Create a {@link DownloadStatus} instance from a database row (cursor).
+ */
+ @NonNull
+ public static DownloadStatus convert(@NonNull Cursor cursor) {
+ int indexId = cursor.getColumnIndex(PodDBAdapter.KEY_ID);
+ int indexTitle = cursor.getColumnIndex(PodDBAdapter.KEY_DOWNLOADSTATUS_TITLE);
+ int indexFeedFile = cursor.getColumnIndex(PodDBAdapter.KEY_FEEDFILE);
+ int indexFileFileType = cursor.getColumnIndex(PodDBAdapter.KEY_FEEDFILETYPE);
+ int indexSuccessful = cursor.getColumnIndex(PodDBAdapter.KEY_SUCCESSFUL);
+ int indexReason = cursor.getColumnIndex(PodDBAdapter.KEY_REASON);
+ int indexCompletionDate = cursor.getColumnIndex(PodDBAdapter.KEY_COMPLETION_DATE);
+ int indexReasonDetailed = cursor.getColumnIndex(PodDBAdapter.KEY_REASON_DETAILED);
+
+ return new DownloadStatus(cursor.getLong(indexId), cursor.getString(indexTitle), cursor.getLong(indexFeedFile),
+ cursor.getInt(indexFileFileType), cursor.getInt(indexSuccessful) > 0, false, true,
+ DownloadError.fromCode(cursor.getInt(indexReason)),
+ new Date(cursor.getLong(indexCompletionDate)),
+ cursor.getString(indexReasonDetailed), false);
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/mapper/FeedCursorMapper.java b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/mapper/FeedCursorMapper.java
index a2a180735..25df7313f 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/storage/mapper/FeedCursorMapper.java
+++ b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/mapper/FeedCursorMapper.java
@@ -1,4 +1,4 @@
-package de.danoeh.antennapod.core.storage.mapper;
+package de.danoeh.antennapod.storage.database.mapper;
import android.database.Cursor;
@@ -6,8 +6,8 @@ import androidx.annotation.NonNull;
import de.danoeh.antennapod.model.feed.Feed;
import de.danoeh.antennapod.model.feed.FeedPreferences;
-import de.danoeh.antennapod.core.storage.PodDBAdapter;
import de.danoeh.antennapod.model.feed.SortOrder;
+import de.danoeh.antennapod.storage.database.PodDBAdapter;
/**
* Converts a {@link Cursor} to a {@link Feed} object.
diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/mapper/FeedItemCursorMapper.java b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/mapper/FeedItemCursorMapper.java
index ca0834339..fcf51e31e 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/storage/mapper/FeedItemCursorMapper.java
+++ b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/mapper/FeedItemCursorMapper.java
@@ -1,9 +1,9 @@
-package de.danoeh.antennapod.core.storage.mapper;
+package de.danoeh.antennapod.storage.database.mapper;
import android.database.Cursor;
import androidx.annotation.NonNull;
import de.danoeh.antennapod.model.feed.FeedItem;
-import de.danoeh.antennapod.core.storage.PodDBAdapter;
+import de.danoeh.antennapod.storage.database.PodDBAdapter;
import java.util.Date;
@@ -27,6 +27,7 @@ public abstract class FeedItemCursorMapper {
int indexItemIdentifier = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_ITEM_IDENTIFIER);
int indexAutoDownload = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_AUTO_DOWNLOAD_ATTEMPTS);
int indexImageUrl = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_IMAGE_URL);
+ int indexPodcastIndexChapterUrl = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_PODCASTINDEX_CHAPTER_URL);
long id = cursor.getInt(indexId);
String title = cursor.getString(indexTitle);
@@ -39,8 +40,9 @@ public abstract class FeedItemCursorMapper {
String itemIdentifier = cursor.getString(indexItemIdentifier);
long autoDownload = cursor.getLong(indexAutoDownload);
String imageUrl = cursor.getString(indexImageUrl);
+ String podcastIndexChapterUrl = cursor.getString(indexPodcastIndexChapterUrl);
return new FeedItem(id, title, link, pubDate, paymentLink, feedId,
- hasChapters, imageUrl, state, itemIdentifier, autoDownload);
+ hasChapters, imageUrl, state, itemIdentifier, autoDownload, podcastIndexChapterUrl);
}
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/mapper/FeedItemFilterQuery.java b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/mapper/FeedItemFilterQuery.java
index 1d9c8a9e6..1728a905f 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/storage/mapper/FeedItemFilterQuery.java
+++ b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/mapper/FeedItemFilterQuery.java
@@ -1,7 +1,7 @@
-package de.danoeh.antennapod.core.storage.mapper;
+package de.danoeh.antennapod.storage.database.mapper;
import de.danoeh.antennapod.model.feed.FeedItemFilter;
-import de.danoeh.antennapod.core.storage.PodDBAdapter;
+import de.danoeh.antennapod.storage.database.PodDBAdapter;
import java.util.ArrayList;
import java.util.List;
diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/mapper/FeedMediaCursorMapper.java b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/mapper/FeedMediaCursorMapper.java
index 0dc3dc231..f57e91b83 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/storage/mapper/FeedMediaCursorMapper.java
+++ b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/mapper/FeedMediaCursorMapper.java
@@ -1,9 +1,9 @@
-package de.danoeh.antennapod.core.storage.mapper;
+package de.danoeh.antennapod.storage.database.mapper;
import android.database.Cursor;
import androidx.annotation.NonNull;
import de.danoeh.antennapod.model.feed.FeedMedia;
-import de.danoeh.antennapod.core.storage.PodDBAdapter;
+import de.danoeh.antennapod.storage.database.PodDBAdapter;
import java.util.Date;
diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/mapper/FeedPreferencesCursorMapper.java b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/mapper/FeedPreferencesCursorMapper.java
index f062609b6..9fc70a2d7 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/storage/mapper/FeedPreferencesCursorMapper.java
+++ b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/mapper/FeedPreferencesCursorMapper.java
@@ -1,4 +1,4 @@
-package de.danoeh.antennapod.core.storage.mapper;
+package de.danoeh.antennapod.storage.database.mapper;
import android.database.Cursor;
import android.text.TextUtils;
@@ -6,7 +6,7 @@ import androidx.annotation.NonNull;
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.core.storage.PodDBAdapter;
+import de.danoeh.antennapod.storage.database.PodDBAdapter;
import java.util.Arrays;
import java.util.HashSet;
diff --git a/ui/app-start-intent/build.gradle b/ui/app-start-intent/build.gradle
index 2f6d821b1..1f5161392 100644
--- a/ui/app-start-intent/build.gradle
+++ b/ui/app-start-intent/build.gradle
@@ -1,4 +1,6 @@
-apply plugin: "com.android.library"
+plugins {
+ id("com.android.library")
+}
apply from: "../../common.gradle"
dependencies {
diff --git a/ui/app-start-intent/src/main/java/de/danoeh/antennapod/ui/appstartintent/PlaybackSpeedActivityStarter.java b/ui/app-start-intent/src/main/java/de/danoeh/antennapod/ui/appstartintent/PlaybackSpeedActivityStarter.java
new file mode 100644
index 000000000..ac7e72a4e
--- /dev/null
+++ b/ui/app-start-intent/src/main/java/de/danoeh/antennapod/ui/appstartintent/PlaybackSpeedActivityStarter.java
@@ -0,0 +1,40 @@
+package de.danoeh.antennapod.ui.appstartintent;
+
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Build;
+
+/**
+ * Launches the playback speed dialog activity of the app with specific arguments.
+ * Does not require a dependency on the actual implementation of the activity.
+ */
+public class PlaybackSpeedActivityStarter {
+ public static final String INTENT = "de.danoeh.antennapod.intents.PLAYBACK_SPEED";
+ private final Intent intent;
+ private final Context context;
+
+ public PlaybackSpeedActivityStarter(Context context) {
+ this.context = context;
+ intent = new Intent(INTENT);
+ intent.setPackage(context.getPackageName());
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
+ } else {
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ }
+ }
+
+ public Intent getIntent() {
+ return intent;
+ }
+
+ public PendingIntent getPendingIntent() {
+ return PendingIntent.getActivity(context, R.id.pending_intent_playback_speed, getIntent(),
+ PendingIntent.FLAG_UPDATE_CURRENT | (Build.VERSION.SDK_INT >= 23 ? PendingIntent.FLAG_IMMUTABLE : 0));
+ }
+
+ public void start() {
+ context.startActivity(getIntent());
+ }
+}
diff --git a/ui/app-start-intent/src/main/res/values/pending_intent.xml b/ui/app-start-intent/src/main/res/values/pending_intent.xml
index 1e426e954..ed7e9b2cd 100644
--- a/ui/app-start-intent/src/main/res/values/pending_intent.xml
+++ b/ui/app-start-intent/src/main/res/values/pending_intent.xml
@@ -6,6 +6,7 @@
<item name="pending_intent_download_service_autodownload_report" type="id"/>
<item name="pending_intent_allow_stream_always" type="id"/>
<item name="pending_intent_allow_stream_this_time" type="id"/>
+ <item name="pending_intent_playback_speed" type="id"/>
<item name="pending_intent_player_activity" type="id"/>
<item name="pending_intent_video_player" type="id"/>
<item name="pending_intent_sync_error" type="id"/>
diff --git a/ui/common/build.gradle b/ui/common/build.gradle
index 5390b85d8..ff4bf83cf 100644
--- a/ui/common/build.gradle
+++ b/ui/common/build.gradle
@@ -1,7 +1,11 @@
-apply plugin: "com.android.library"
+plugins {
+ id("com.android.library")
+}
apply from: "../../common.gradle"
dependencies {
annotationProcessor "androidx.annotation:annotation:$annotationVersion"
implementation "androidx.appcompat:appcompat:$appcompatVersion"
+ implementation "androidx.viewpager2:viewpager2:$viewPager2Version"
+ implementation "com.google.android.material:material:$googleMaterialVersion"
}
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/PagedToolbarFragment.java b/ui/common/src/main/java/de/danoeh/antennapod/ui/common/PagedToolbarFragment.java
index f79bffabc..cbdd789db 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/PagedToolbarFragment.java
+++ b/ui/common/src/main/java/de/danoeh/antennapod/ui/common/PagedToolbarFragment.java
@@ -1,4 +1,4 @@
-package de.danoeh.antennapod.fragment;
+package de.danoeh.antennapod.ui.common;
import androidx.annotation.NonNull;
import androidx.appcompat.widget.Toolbar;
@@ -17,7 +17,7 @@ public abstract class PagedToolbarFragment extends Fragment {
* Invalidate the toolbar menu if the current child fragment is visible.
* @param child The fragment to invalidate
*/
- void invalidateOptionsMenuIfActive(@NonNull Fragment child) {
+ public void invalidateOptionsMenuIfActive(@NonNull Fragment child) {
Fragment visibleChild = getChildFragmentManager().findFragmentByTag("f" + viewPager.getCurrentItem());
if (visibleChild == child) {
visibleChild.onPrepareOptionsMenu(toolbar.getMenu());
@@ -29,6 +29,9 @@ public abstract class PagedToolbarFragment extends Fragment {
this.viewPager = viewPager;
toolbar.setOnMenuItemClickListener(item -> {
+ if (this.onOptionsItemSelected(item)) {
+ return true;
+ }
Fragment child = getChildFragmentManager().findFragmentByTag("f" + viewPager.getCurrentItem());
if (child != null) {
return child.onOptionsItemSelected(item);
diff --git a/ui/common/src/main/java/de/danoeh/antennapod/ui/common/RecursiveRadioGroup.java b/ui/common/src/main/java/de/danoeh/antennapod/ui/common/RecursiveRadioGroup.java
index 94ef73ffc..578208be4 100644
--- a/ui/common/src/main/java/de/danoeh/antennapod/ui/common/RecursiveRadioGroup.java
+++ b/ui/common/src/main/java/de/danoeh/antennapod/ui/common/RecursiveRadioGroup.java
@@ -6,6 +6,9 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.RadioButton;
+import android.widget.RadioGroup;
+import androidx.annotation.Nullable;
+
import java.util.ArrayList;
/**
@@ -15,6 +18,8 @@ import java.util.ArrayList;
public class RecursiveRadioGroup extends LinearLayout {
private final ArrayList<RadioButton> radioButtons = new ArrayList<>();
private RadioButton checkedButton = null;
+ @Nullable
+ private RadioGroup.OnCheckedChangeListener checkedChangeListener;
public RecursiveRadioGroup(Context context) {
super(context);
@@ -34,6 +39,10 @@ public class RecursiveRadioGroup extends LinearLayout {
parseChild(child);
}
+ public void setOnCheckedChangeListener(@Nullable RadioGroup.OnCheckedChangeListener listener) {
+ checkedChangeListener = listener;
+ }
+
public void parseChild(final View child) {
if (child instanceof RadioButton) {
RadioButton button = (RadioButton) child;
@@ -43,6 +52,9 @@ public class RecursiveRadioGroup extends LinearLayout {
return;
}
checkedButton = (RadioButton) buttonView;
+ if (checkedChangeListener != null) {
+ checkedChangeListener.onCheckedChanged(null, checkedButton.getId());
+ }
for (RadioButton view : radioButtons) {
if (view != buttonView) {
diff --git a/app/src/main/res/layout/pager_fragment.xml b/ui/common/src/main/res/layout/pager_fragment.xml
index ea007892a..ea007892a 100644
--- a/app/src/main/res/layout/pager_fragment.xml
+++ b/ui/common/src/main/res/layout/pager_fragment.xml
diff --git a/ui/i18n/README.md b/ui/i18n/README.md
new file mode 100644
index 000000000..a15c38807
--- /dev/null
+++ b/ui/i18n/README.md
@@ -0,0 +1,3 @@
+# :ui:i18n
+
+The abbreviation i18n stands for internationalization. This module contains the app's main texts that are translated to different languages. Most modules that show UI depend on this module.
diff --git a/ui/i18n/build.gradle b/ui/i18n/build.gradle
new file mode 100644
index 000000000..a1ace417b
--- /dev/null
+++ b/ui/i18n/build.gradle
@@ -0,0 +1,15 @@
+plugins {
+ id("com.android.library")
+}
+apply from: "../../common.gradle"
+
+android {
+ lintOptions {
+ disable "Typos", "ExtraTranslation", "ImpliedQuantity",
+ "PluralsCandidate", "UnusedQuantity", "TypographyEllipsis"
+ }
+}
+
+dependencies {
+ annotationProcessor "androidx.annotation:annotation:$annotationVersion"
+}
diff --git a/ui/i18n/lint.xml b/ui/i18n/lint.xml
new file mode 100644
index 000000000..e35ec9859
--- /dev/null
+++ b/ui/i18n/lint.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<lint>
+ <issue id="MissingDefaultResource">
+ <ignore path="**/values-**/strings.xml" />
+ </issue>
+
+ <issue id="UnusedResources" severity="error">
+ <ignore path="**/values-**/strings.xml" />
+ </issue>
+</lint>
diff --git a/ui/i18n/src/main/AndroidManifest.xml b/ui/i18n/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..8729444b0
--- /dev/null
+++ b/ui/i18n/src/main/AndroidManifest.xml
@@ -0,0 +1 @@
+<manifest package="de.danoeh.antennapod.ui.i18n" />
diff --git a/core/src/main/res/values-ar/strings.xml b/ui/i18n/src/main/res/values-ar/strings.xml
index 01d2b04bb..01d2b04bb 100644
--- a/core/src/main/res/values-ar/strings.xml
+++ b/ui/i18n/src/main/res/values-ar/strings.xml
diff --git a/core/src/main/res/values-ast/strings.xml b/ui/i18n/src/main/res/values-ast/strings.xml
index e9d1ea593..e9d1ea593 100644
--- a/core/src/main/res/values-ast/strings.xml
+++ b/ui/i18n/src/main/res/values-ast/strings.xml
diff --git a/core/src/main/res/values-br/strings.xml b/ui/i18n/src/main/res/values-br/strings.xml
index 698d9a7c0..698d9a7c0 100644
--- a/core/src/main/res/values-br/strings.xml
+++ b/ui/i18n/src/main/res/values-br/strings.xml
diff --git a/core/src/main/res/values-ca/strings.xml b/ui/i18n/src/main/res/values-ca/strings.xml
index 929446879..929446879 100644
--- a/core/src/main/res/values-ca/strings.xml
+++ b/ui/i18n/src/main/res/values-ca/strings.xml
diff --git a/core/src/main/res/values-cs/strings.xml b/ui/i18n/src/main/res/values-cs/strings.xml
index 69736be49..69736be49 100644
--- a/core/src/main/res/values-cs/strings.xml
+++ b/ui/i18n/src/main/res/values-cs/strings.xml
diff --git a/core/src/main/res/values-da/strings.xml b/ui/i18n/src/main/res/values-da/strings.xml
index e5d424499..e5d424499 100644
--- a/core/src/main/res/values-da/strings.xml
+++ b/ui/i18n/src/main/res/values-da/strings.xml
diff --git a/core/src/main/res/values-de/strings.xml b/ui/i18n/src/main/res/values-de/strings.xml
index 6dad2edec..6dad2edec 100644
--- a/core/src/main/res/values-de/strings.xml
+++ b/ui/i18n/src/main/res/values-de/strings.xml
diff --git a/core/src/main/res/values-es/strings.xml b/ui/i18n/src/main/res/values-es/strings.xml
index 7e4ab717a..7e4ab717a 100644
--- a/core/src/main/res/values-es/strings.xml
+++ b/ui/i18n/src/main/res/values-es/strings.xml
diff --git a/core/src/main/res/values-et/strings.xml b/ui/i18n/src/main/res/values-et/strings.xml
index 0427fe34a..0427fe34a 100644
--- a/core/src/main/res/values-et/strings.xml
+++ b/ui/i18n/src/main/res/values-et/strings.xml
diff --git a/core/src/main/res/values-eu/strings.xml b/ui/i18n/src/main/res/values-eu/strings.xml
index 8a17763a7..8a17763a7 100644
--- a/core/src/main/res/values-eu/strings.xml
+++ b/ui/i18n/src/main/res/values-eu/strings.xml
diff --git a/core/src/main/res/values-fa/strings.xml b/ui/i18n/src/main/res/values-fa/strings.xml
index e6145d62b..e6145d62b 100644
--- a/core/src/main/res/values-fa/strings.xml
+++ b/ui/i18n/src/main/res/values-fa/strings.xml
diff --git a/core/src/main/res/values-fi/strings.xml b/ui/i18n/src/main/res/values-fi/strings.xml
index bc5d94ad5..bc5d94ad5 100644
--- a/core/src/main/res/values-fi/strings.xml
+++ b/ui/i18n/src/main/res/values-fi/strings.xml
diff --git a/core/src/main/res/values-fr/strings.xml b/ui/i18n/src/main/res/values-fr/strings.xml
index 87e5288d2..87e5288d2 100644
--- a/core/src/main/res/values-fr/strings.xml
+++ b/ui/i18n/src/main/res/values-fr/strings.xml
diff --git a/core/src/main/res/values-gl/strings.xml b/ui/i18n/src/main/res/values-gl/strings.xml
index a524d722d..a524d722d 100644
--- a/core/src/main/res/values-gl/strings.xml
+++ b/ui/i18n/src/main/res/values-gl/strings.xml
diff --git a/core/src/main/res/values-hu/strings.xml b/ui/i18n/src/main/res/values-hu/strings.xml
index a75ef63e4..a75ef63e4 100644
--- a/core/src/main/res/values-hu/strings.xml
+++ b/ui/i18n/src/main/res/values-hu/strings.xml
diff --git a/core/src/main/res/values-it/strings.xml b/ui/i18n/src/main/res/values-it/strings.xml
index 9e22da1f0..9e22da1f0 100644
--- a/core/src/main/res/values-it/strings.xml
+++ b/ui/i18n/src/main/res/values-it/strings.xml
diff --git a/core/src/main/res/values-iw/strings.xml b/ui/i18n/src/main/res/values-iw/strings.xml
index 1dc4cbb32..1dc4cbb32 100644
--- a/core/src/main/res/values-iw/strings.xml
+++ b/ui/i18n/src/main/res/values-iw/strings.xml
diff --git a/core/src/main/res/values-ja/strings.xml b/ui/i18n/src/main/res/values-ja/strings.xml
index c441e6778..c441e6778 100644
--- a/core/src/main/res/values-ja/strings.xml
+++ b/ui/i18n/src/main/res/values-ja/strings.xml
diff --git a/core/src/main/res/values-ko/strings.xml b/ui/i18n/src/main/res/values-ko/strings.xml
index bffc8059c..bffc8059c 100644
--- a/core/src/main/res/values-ko/strings.xml
+++ b/ui/i18n/src/main/res/values-ko/strings.xml
diff --git a/core/src/main/res/values-lt/strings.xml b/ui/i18n/src/main/res/values-lt/strings.xml
index 42645ffbb..42645ffbb 100644
--- a/core/src/main/res/values-lt/strings.xml
+++ b/ui/i18n/src/main/res/values-lt/strings.xml
diff --git a/core/src/main/res/values-nb/strings.xml b/ui/i18n/src/main/res/values-nb/strings.xml
index e785bd723..e785bd723 100644
--- a/core/src/main/res/values-nb/strings.xml
+++ b/ui/i18n/src/main/res/values-nb/strings.xml
diff --git a/core/src/main/res/values-nl/strings.xml b/ui/i18n/src/main/res/values-nl/strings.xml
index fd6540626..fd6540626 100644
--- a/core/src/main/res/values-nl/strings.xml
+++ b/ui/i18n/src/main/res/values-nl/strings.xml
diff --git a/core/src/main/res/values-pl/strings.xml b/ui/i18n/src/main/res/values-pl/strings.xml
index 146df2858..146df2858 100644
--- a/core/src/main/res/values-pl/strings.xml
+++ b/ui/i18n/src/main/res/values-pl/strings.xml
diff --git a/core/src/main/res/values-pt-rBR/strings.xml b/ui/i18n/src/main/res/values-pt-rBR/strings.xml
index 2a0dc208b..2a0dc208b 100644
--- a/core/src/main/res/values-pt-rBR/strings.xml
+++ b/ui/i18n/src/main/res/values-pt-rBR/strings.xml
diff --git a/core/src/main/res/values-pt/strings.xml b/ui/i18n/src/main/res/values-pt/strings.xml
index 646f9cdc2..646f9cdc2 100644
--- a/core/src/main/res/values-pt/strings.xml
+++ b/ui/i18n/src/main/res/values-pt/strings.xml
diff --git a/core/src/main/res/values-ro/strings.xml b/ui/i18n/src/main/res/values-ro/strings.xml
index b9cf03cb8..b9cf03cb8 100644
--- a/core/src/main/res/values-ro/strings.xml
+++ b/ui/i18n/src/main/res/values-ro/strings.xml
diff --git a/core/src/main/res/values-ru/strings.xml b/ui/i18n/src/main/res/values-ru/strings.xml
index 365912c52..365912c52 100644
--- a/core/src/main/res/values-ru/strings.xml
+++ b/ui/i18n/src/main/res/values-ru/strings.xml
diff --git a/core/src/main/res/values-sk/strings.xml b/ui/i18n/src/main/res/values-sk/strings.xml
index 8d44090e1..8d44090e1 100644
--- a/core/src/main/res/values-sk/strings.xml
+++ b/ui/i18n/src/main/res/values-sk/strings.xml
diff --git a/core/src/main/res/values-sl/strings.xml b/ui/i18n/src/main/res/values-sl/strings.xml
index 7a7e19042..7a7e19042 100644
--- a/core/src/main/res/values-sl/strings.xml
+++ b/ui/i18n/src/main/res/values-sl/strings.xml
diff --git a/core/src/main/res/values-sv/strings.xml b/ui/i18n/src/main/res/values-sv/strings.xml
index 528a0a88a..528a0a88a 100644
--- a/core/src/main/res/values-sv/strings.xml
+++ b/ui/i18n/src/main/res/values-sv/strings.xml
diff --git a/core/src/main/res/values-tr/strings.xml b/ui/i18n/src/main/res/values-tr/strings.xml
index aa0305e66..aa0305e66 100644
--- a/core/src/main/res/values-tr/strings.xml
+++ b/ui/i18n/src/main/res/values-tr/strings.xml
diff --git a/core/src/main/res/values-uk/strings.xml b/ui/i18n/src/main/res/values-uk/strings.xml
index 686654d2e..686654d2e 100644
--- a/core/src/main/res/values-uk/strings.xml
+++ b/ui/i18n/src/main/res/values-uk/strings.xml
diff --git a/core/src/main/res/values-zh-rCN/strings.xml b/ui/i18n/src/main/res/values-zh-rCN/strings.xml
index 5ad647f98..5ad647f98 100644
--- a/core/src/main/res/values-zh-rCN/strings.xml
+++ b/ui/i18n/src/main/res/values-zh-rCN/strings.xml
diff --git a/core/src/main/res/values-zh-rTW/strings.xml b/ui/i18n/src/main/res/values-zh-rTW/strings.xml
index 616c1d0d9..616c1d0d9 100644
--- a/core/src/main/res/values-zh-rTW/strings.xml
+++ b/ui/i18n/src/main/res/values-zh-rTW/strings.xml
diff --git a/core/src/main/res/values/strings.xml b/ui/i18n/src/main/res/values/strings.xml
index d9ed1262b..0073eebf5 100644
--- a/core/src/main/res/values/strings.xml
+++ b/ui/i18n/src/main/res/values/strings.xml
@@ -28,8 +28,7 @@
<string name="gpodnet_main_label">gpodder.net</string>
<string name="episode_cache_full_title">Episode cache full</string>
<string name="episode_cache_full_message">The episode cache limit has been reached. You can increase the cache size in the Settings.</string>
- <string name="playback_statistics_label">Playback</string>
- <string name="download_statistics_label">Downloads</string>
+ <string name="years_statistics_label">Years</string>
<string name="notification_pref_fragment">Notifications</string>
<!-- Google Assistant -->
@@ -44,17 +43,20 @@
<string name="change_setting">Change</string>
<!-- Statistics fragment -->
- <string name="total_time_listened_to_podcasts">Total time of episodes played:</string>
- <string name="statistics_mode">Statistics mode</string>
- <string name="statistics_mode_normal">Calculate duration that was actually played. Playing twice is counted twice, while marking as played is not counted</string>
- <string name="statistics_mode_count_all">Sum up all episodes marked as played</string>
+ <string name="statistics_include_marked">Include duration of episodes that are just marked as played</string>
<string name="statistics_speed_not_counted">Notice: Playback speed is never taken into account.</string>
+ <string name="statistics_from">From</string>
+ <string name="statistics_to">To</string>
+ <string name="statistics_today">Today</string>
+ <string name="statistics_filter_all_time">All time</string>
+ <string name="statistics_filter_past_year">Past year</string>
<string name="statistics_reset_data">Reset statistics data</string>
<string name="statistics_reset_data_msg">This will erase the history of duration played for all episodes. Are you sure you want to proceed?</string>
- <string name="statistics_counting_since">Since %s,\nyou played</string>
+ <string name="statistics_counting_range">Played between %1$s and %2$s</string>
+ <string name="statistics_counting_total">Played in total</string>
<!-- Download Statistics fragment -->
- <string name="total_size_downloaded_podcasts">Total size of episodes on the device:</string>
+ <string name="total_size_downloaded_podcasts">Total size of episodes on the device</string>
<!-- Main activity -->
<string name="drawer_open">Open menu</string>
@@ -165,10 +167,8 @@
<string name="rename_feed_label">Rename podcast</string>
<string name="remove_feed_label">Remove podcast</string>
<string name="share_label">Share</string>
- <string name="share_label_with_ellipses">Share…</string>
<string name="share_file_label">Share File</string>
- <string name="share_website_url_label">Website address</string>
- <string name="share_feed_url_label">Podcast feed URL</string>
+ <string name="share_rss_address_label">RSS address:</string>
<string name="feed_delete_confirmation_msg">Please confirm that you want to delete the podcast \"%1$s\" and ALL its episodes (including downloaded episodes).</string>
<string name="feed_delete_confirmation_msg_batch">Please confirm that you want to remove the selected podcasts and ALL their episodes (including downloaded episodes).</string>
<string name="feed_delete_confirmation_local_msg">Please confirm that you want to remove the podcast \"%1$s\". The files in the local source folder will not be deleted.</string>
@@ -277,6 +277,8 @@
</plurals>
<string name="completing">Completing…</string>
<string name="download_notification_title">Downloading podcast data</string>
+ <string name="download_notification_title_feeds">Refreshing podcasts</string>
+ <string name="download_notification_title_episodes">Downloading episodes</string>
<string name="download_log_title_unknown">Unknown Title</string>
<string name="download_type_feed">Feed</string>
<string name="download_type_media">Media file</string>
@@ -302,7 +304,6 @@
<string name="playback_error_unknown">Unknown Error</string>
<string name="no_media_playing_label">No media playing</string>
<string name="position_default_label" translatable="false">00:00:00</string>
- <string name="player_go_to_picture_in_picture">Picture-in-picture mode</string>
<string name="unknown_media_key">AntennaPod - Unknown media key: %1$d</string>
<string name="error_file_not_found">File not found</string>
<string name="no_media_label">Item does not contain a media file</string>
@@ -511,10 +512,6 @@
<string name="media_player_switch_to_exoplayer">Switch to ExoPlayer</string>
<string name="media_player_switched_to_exoplayer">Switched to ExoPlayer.</string>
<string name="pref_skip_silence_title">Skip Silence in Audio</string>
- <string name="pref_videoBehavior_title">Upon exiting video</string>
- <string name="pref_videoBehavior_sum">Behavior when leaving video playback</string>
- <string name="stop_playback">Stop playback</string>
- <string name="continue_playback">Continue audio playback</string>
<string name="behavior">Behavior</string>
<string name="pref_back_button_behavior_title">Back Button Behavior</string>
<string name="pref_back_button_behavior_sum">Change behavior of the back button.</string>
@@ -581,7 +578,7 @@
<string name="opml_import_summary">Import your subscriptions from another podcast app</string>
<string name="database_export_summary">Transfer subscriptions, listened episodes and queue to AntennaPod on another device</string>
<string name="database_import_summary">Import AntennaPod database from another device</string>
- <string name="opml_import_label">OPML Import</string>
+ <string name="opml_import_label">OPML import</string>
<string name="opml_add_podcast_label">Import podcast list (OPML)</string>
<string name="opml_reader_error">An error has occurred while reading the OPML document:</string>
<string name="opml_import_error_no_file">No file selected!</string>
@@ -707,7 +704,6 @@
<string name="auto_download_disabled_globally">Auto download is disabled in the main AntennaPod settings</string>
<string name="statistics_time_played">Time played:</string>
<string name="statistics_total_duration">Total duration (estimate):</string>
- <string name="statistics_duration_played_episodes">Duration of played episodes:</string>
<string name="statistics_episodes_on_device">Episodes on the device:</string>
<string name="statistics_space_used">Space used:</string>
<string name="statistics_episodes_started_total">Episodes started/total:</string>
@@ -779,10 +775,10 @@
<string name="rating_now_label" tools:ignore="UnusedResources">Sure, let\'s do this!</string>
<!-- Share episode dialog -->
- <string name="share_dialog_include_label">Include:</string>
- <string name="share_playback_position_dialog_label">Playback position</string>
+ <string name="share_playback_position_dialog_label">Include playback position</string>
<string name="share_dialog_episode_website_label">Episode webpage</string>
- <string name="share_dialog_link_to_episode">Link to episode</string>
+ <string name="share_dialog_for_social">Social message</string>
+ <string name="share_dialog_media_address">Media address</string>
<string name="share_dialog_media_file_label">Media file</string>
<string name="share_starting_position_label">Starting from</string>
@@ -793,6 +789,7 @@
<string name="stereo_to_mono">Downmix: Stereo to mono</string>
<string name="sonic_only">Sonic only</string>
<string name="exoplayer_only">ExoPlayer only</string>
+ <string name="player_switch_to_audio_only">Switch to audio only</string>
<!-- proxy settings -->
<string name="proxy_type_label">Type</string>
diff --git a/ui/png-icons/build.gradle b/ui/png-icons/build.gradle
index f0d9f7a57..ad0b9d0b0 100644
--- a/ui/png-icons/build.gradle
+++ b/ui/png-icons/build.gradle
@@ -1,4 +1,6 @@
-apply plugin: "com.android.library"
+plugins {
+ id("com.android.library")
+}
apply from: "../../common.gradle"
android {
diff --git a/ui/png-icons/src/main/res/drawable/ic_widget_playback_speed.xml b/ui/png-icons/src/main/res/drawable/ic_widget_playback_speed.xml
new file mode 100644
index 000000000..482b6617e
--- /dev/null
+++ b/ui/png-icons/src/main/res/drawable/ic_widget_playback_speed.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:height="48dp"
+ android:width="48dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+
+ <group
+ android:pivotX="12"
+ android:pivotY="12"
+ android:scaleX="0.8"
+ android:scaleY="0.8">
+
+ <path android:fillColor="#ffffff" android:pathData="M 12 15.98 A 2.98 2.98 0 0 1 9.02 12.99 c 0 -1.11 0.61 -2.09 1.49 -2.6 L 20.17 4.81 L 14.67 14.34 C 14.17 15.31 13.16 15.98 12 15.98 M 12 3.05 c 1.8 0 3.48 0.5 4.94 1.31 l -2.09 1.2 C 13.99 5.22 12.99 5.04 12 5.04 a 7.96 7.96 0 0 0 -7.96 7.96 c 0 2.2 0.89 4.19 2.33 5.62 h 0.01 c 0.39 0.39 0.39 1.01 0 1.4 c -0.39 0.39 -1.02 0.39 -1.41 0.01 v 0 C 3.17 18.22 2.05 15.74 2.05 12.99 A 9.95 9.95 0 0 1 12 3.05 m 9.95 9.95 c 0 2.75 -1.11 5.23 -2.91 7.03 v 0 c -0.39 0.38 -1.01 0.38 -1.4 -0.01 c -0.39 -0.39 -0.39 -1.01 0 -1.4 v 0 c 1.44 -1.44 2.33 -3.42 2.33 -5.62 c 0 -0.99 -0.19 -1.99 -0.54 -2.88 L 20.62 8.02 c 0.83 1.49 1.32 3.16 1.32 4.97 z" />
+ </group>
+</vector> \ No newline at end of file
diff --git a/ui/statistics/README.md b/ui/statistics/README.md
new file mode 100644
index 000000000..91fb4e2af
--- /dev/null
+++ b/ui/statistics/README.md
@@ -0,0 +1,3 @@
+# :ui:statistics
+
+This module provides the statistics screens.
diff --git a/ui/statistics/build.gradle b/ui/statistics/build.gradle
new file mode 100644
index 000000000..cca840989
--- /dev/null
+++ b/ui/statistics/build.gradle
@@ -0,0 +1,33 @@
+plugins {
+ id("com.android.library")
+}
+apply from: "../../common.gradle"
+apply from: "../../playFlavor.gradle"
+
+android {
+ lintOptions {
+ disable "InvalidPeriodicWorkRequestInterval", "MissingPermission", "GradleCompatible",
+ "QueryPermissionsNeeded", "Overdraw", "SetTextI18n", "RtlHardcoded"
+ }
+}
+
+dependencies {
+ implementation project(":core")
+ implementation project(':event')
+ implementation project(":model")
+ implementation project(":ui:common")
+
+ annotationProcessor "androidx.annotation:annotation:$annotationVersion"
+ implementation "androidx.appcompat:appcompat:$appcompatVersion"
+ implementation "androidx.core:core:$coreVersion"
+ implementation "androidx.fragment:fragment:$fragmentVersion"
+ implementation "androidx.recyclerview:recyclerview:$recyclerViewVersion"
+ implementation "androidx.viewpager2:viewpager2:$viewPager2Version"
+ implementation "com.google.android.material:material:$googleMaterialVersion"
+
+ implementation "org.greenrobot:eventbus:$eventbusVersion"
+ implementation "com.github.bumptech.glide:glide:$glideVersion"
+ annotationProcessor "com.github.bumptech.glide:compiler:$glideVersion"
+ implementation "io.reactivex.rxjava2:rxandroid:$rxAndroidVersion"
+ implementation "io.reactivex.rxjava2:rxjava:$rxJavaVersion"
+}
diff --git a/ui/statistics/src/main/AndroidManifest.xml b/ui/statistics/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..59c5bd42c
--- /dev/null
+++ b/ui/statistics/src/main/AndroidManifest.xml
@@ -0,0 +1 @@
+<manifest package="de.danoeh.antennapod.ui.statistics" />
diff --git a/app/src/main/java/de/danoeh/antennapod/view/PieChartView.java b/ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/PieChartView.java
index ab4920119..f3e6f0259 100644
--- a/app/src/main/java/de/danoeh/antennapod/view/PieChartView.java
+++ b/ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/PieChartView.java
@@ -1,4 +1,4 @@
-package de.danoeh.antennapod.view;
+package de.danoeh.antennapod.ui.statistics;
import android.annotation.SuppressLint;
import android.content.Context;
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
new file mode 100644
index 000000000..53a45f248
--- /dev/null
+++ b/ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/StatisticsFragment.java
@@ -0,0 +1,150 @@
+package de.danoeh.antennapod.ui.statistics;
+
+import android.content.Context;
+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 android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.appcompat.widget.Toolbar;
+import androidx.fragment.app.Fragment;
+import androidx.viewpager2.adapter.FragmentStateAdapter;
+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.storage.DBWriter;
+import de.danoeh.antennapod.event.StatisticsEvent;
+import de.danoeh.antennapod.ui.common.PagedToolbarFragment;
+import de.danoeh.antennapod.ui.statistics.downloads.DownloadStatisticsFragment;
+import de.danoeh.antennapod.ui.statistics.subscriptions.SubscriptionStatisticsFragment;
+import de.danoeh.antennapod.ui.statistics.years.YearsStatisticsFragment;
+import io.reactivex.Completable;
+import io.reactivex.android.schedulers.AndroidSchedulers;
+import io.reactivex.disposables.Disposable;
+import io.reactivex.schedulers.Schedulers;
+import org.greenrobot.eventbus.EventBus;
+
+/**
+ * Displays the 'statistics' screen
+ */
+public class StatisticsFragment extends PagedToolbarFragment {
+ public static final String TAG = "StatisticsFragment";
+ public static final String PREF_NAME = "StatisticsActivityPrefs";
+ public static final String PREF_INCLUDE_MARKED_PLAYED = "countAll";
+ public static final String PREF_FILTER_FROM = "filterFrom";
+ public static final String PREF_FILTER_TO = "filterTo";
+
+
+ private static final int POS_SUBSCRIPTIONS = 0;
+ private static final int POS_YEARS = 1;
+ private static final int POS_SPACE_TAKEN = 2;
+ private static final int TOTAL_COUNT = 3;
+
+ private TabLayout tabLayout;
+ private ViewPager2 viewPager;
+ private Toolbar toolbar;
+
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ super.onCreateView(inflater, container, savedInstanceState);
+ setHasOptionsMenu(true);
+
+ View rootView = inflater.inflate(R.layout.pager_fragment, container, false);
+ viewPager = rootView.findViewById(R.id.viewpager);
+ toolbar = rootView.findViewById(R.id.toolbar);
+ toolbar.setTitle(getString(R.string.statistics_label));
+ toolbar.inflateMenu(R.menu.statistics);
+ toolbar.setNavigationOnClickListener(v -> getParentFragmentManager().popBackStack());
+ viewPager.setAdapter(new StatisticsPagerAdapter(this));
+ // Give the TabLayout the ViewPager
+ tabLayout = rootView.findViewById(R.id.sliding_tabs);
+ super.setupPagedToolbar(toolbar, viewPager);
+ new TabLayoutMediator(tabLayout, viewPager, (tab, position) -> {
+ switch (position) {
+ case POS_SUBSCRIPTIONS:
+ tab.setText(R.string.subscriptions_label);
+ break;
+ case POS_YEARS:
+ tab.setText(R.string.years_statistics_label);
+ break;
+ case POS_SPACE_TAKEN:
+ tab.setText(R.string.downloads_label);
+ break;
+ default:
+ break;
+ }
+ }).attach();
+ return rootView;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(@NonNull MenuItem item) {
+ if (item.getItemId() == R.id.statistics_reset) {
+ confirmResetStatistics();
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ private void confirmResetStatistics() {
+ ConfirmationDialog conDialog = new ConfirmationDialog(
+ getActivity(),
+ R.string.statistics_reset_data,
+ R.string.statistics_reset_data_msg) {
+
+ @Override
+ public void onConfirmButtonPressed(DialogInterface dialog) {
+ dialog.dismiss();
+ doResetStatistics();
+ }
+ };
+ conDialog.createNewDialog().show();
+ }
+
+ private void doResetStatistics() {
+ getContext().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE).edit()
+ .putBoolean(PREF_INCLUDE_MARKED_PLAYED, false)
+ .putLong(PREF_FILTER_FROM, 0)
+ .putLong(PREF_FILTER_TO, Long.MAX_VALUE)
+ .apply();
+
+ Disposable disposable = Completable.fromFuture(DBWriter.resetStatistics())
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(() -> EventBus.getDefault().post(new StatisticsEvent()),
+ error -> Log.e(TAG, Log.getStackTraceString(error)));
+ }
+
+ public static class StatisticsPagerAdapter extends FragmentStateAdapter {
+
+ StatisticsPagerAdapter(@NonNull Fragment fragment) {
+ super(fragment);
+ }
+
+ @NonNull
+ @Override
+ public Fragment createFragment(int position) {
+ switch (position) {
+ case POS_SUBSCRIPTIONS:
+ return new SubscriptionStatisticsFragment();
+ case POS_YEARS:
+ return new YearsStatisticsFragment();
+ default:
+ case POS_SPACE_TAKEN:
+ return new DownloadStatisticsFragment();
+ }
+ }
+
+ @Override
+ public int getItemCount() {
+ return TOTAL_COUNT;
+ }
+ }
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/StatisticsListAdapter.java b/ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/StatisticsListAdapter.java
index 23b5cfdce..d961659d7 100644
--- a/app/src/main/java/de/danoeh/antennapod/adapter/StatisticsListAdapter.java
+++ b/ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/StatisticsListAdapter.java
@@ -1,4 +1,4 @@
-package de.danoeh.antennapod.adapter;
+package de.danoeh.antennapod.ui.statistics;
import android.content.Context;
import androidx.annotation.NonNull;
@@ -12,10 +12,8 @@ import android.widget.TextView;
import com.bumptech.glide.Glide;
import com.bumptech.glide.request.RequestOptions;
-import de.danoeh.antennapod.R;
import de.danoeh.antennapod.core.glide.ApGlideSettings;
import de.danoeh.antennapod.core.storage.StatisticsItem;
-import de.danoeh.antennapod.view.PieChartView;
import java.util.List;
@@ -25,11 +23,11 @@ import java.util.List;
public abstract class StatisticsListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private static final int TYPE_HEADER = 0;
private static final int TYPE_FEED = 1;
- final Context context;
+ protected final Context context;
private List<StatisticsItem> statisticsData;
- PieChartView.PieChartData pieChartData;
+ protected PieChartView.PieChartData pieChartData;
- StatisticsListAdapter(Context context) {
+ protected StatisticsListAdapter(Context context) {
this.context = context;
}
@@ -48,10 +46,7 @@ public abstract class StatisticsListAdapter extends RecyclerView.Adapter<Recycle
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
LayoutInflater inflater = LayoutInflater.from(context);
if (viewType == TYPE_HEADER) {
- View view = inflater.inflate(R.layout.statistics_listitem_total, parent, false);
- TextView totalText = view.findViewById(R.id.total_description);
- totalText.setText(getHeaderCaption());
- return new HeaderHolder(view);
+ return new HeaderHolder(inflater.inflate(R.layout.statistics_listitem_total, parent, false));
}
return new StatisticsHolder(inflater.inflate(R.layout.statistics_listitem, parent, false));
}
@@ -62,6 +57,7 @@ public abstract class StatisticsListAdapter extends RecyclerView.Adapter<Recycle
HeaderHolder holder = (HeaderHolder) h;
holder.pieChart.setData(pieChartData);
holder.totalTime.setText(getHeaderValue());
+ holder.totalText.setText(getHeaderCaption());
} else {
StatisticsHolder holder = (StatisticsHolder) h;
StatisticsItem statsItem = statisticsData.get(position - 1);
@@ -90,19 +86,21 @@ public abstract class StatisticsListAdapter extends RecyclerView.Adapter<Recycle
static class HeaderHolder extends RecyclerView.ViewHolder {
TextView totalTime;
PieChartView pieChart;
+ TextView totalText;
HeaderHolder(View itemView) {
super(itemView);
totalTime = itemView.findViewById(R.id.total_time);
pieChart = itemView.findViewById(R.id.pie_chart);
+ totalText = itemView.findViewById(R.id.total_description);
}
}
- static class StatisticsHolder extends RecyclerView.ViewHolder {
- ImageView image;
- TextView title;
- TextView value;
- TextView chip;
+ public static class StatisticsHolder extends RecyclerView.ViewHolder {
+ public ImageView image;
+ public TextView title;
+ public TextView value;
+ public TextView chip;
StatisticsHolder(View itemView) {
super(itemView);
@@ -113,11 +111,11 @@ public abstract class StatisticsListAdapter extends RecyclerView.Adapter<Recycle
}
}
- abstract String getHeaderCaption();
+ protected abstract String getHeaderCaption();
- abstract String getHeaderValue();
+ protected abstract String getHeaderValue();
- abstract PieChartView.PieChartData generateChartData(List<StatisticsItem> statisticsData);
+ protected abstract PieChartView.PieChartData generateChartData(List<StatisticsItem> statisticsData);
- abstract void onBindFeedViewHolder(StatisticsHolder holder, StatisticsItem item);
+ protected abstract void onBindFeedViewHolder(StatisticsHolder holder, StatisticsItem item);
}
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/DownloadStatisticsFragment.java b/ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/downloads/DownloadStatisticsFragment.java
index ff94cc20c..295094beb 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/DownloadStatisticsFragment.java
+++ b/ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/downloads/DownloadStatisticsFragment.java
@@ -1,8 +1,9 @@
-package de.danoeh.antennapod.fragment.preferences;
+package de.danoeh.antennapod.ui.statistics.downloads;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
+import android.view.Menu;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ProgressBar;
@@ -13,17 +14,14 @@ import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
-import de.danoeh.antennapod.R;
-import de.danoeh.antennapod.adapter.DownloadStatisticsListAdapter;
import de.danoeh.antennapod.core.storage.DBReader;
-import de.danoeh.antennapod.core.storage.StatisticsItem;
+import de.danoeh.antennapod.ui.statistics.R;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
import java.util.Collections;
-import java.util.List;
/**
* Displays the 'download statistics' screen
@@ -36,15 +34,11 @@ public class DownloadStatisticsFragment extends Fragment {
private ProgressBar progressBar;
private DownloadStatisticsListAdapter listAdapter;
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- }
-
@Nullable
@Override
- public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
- View root = inflater.inflate(R.layout.statistics_activity, container, false);
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
+ @Nullable Bundle savedInstanceState) {
+ View root = inflater.inflate(R.layout.statistics_fragment, container, false);
downloadStatisticsList = root.findViewById(R.id.statistics_list);
progressBar = root.findViewById(R.id.progressBar);
listAdapter = new DownloadStatisticsListAdapter(getContext());
@@ -59,6 +53,13 @@ public class DownloadStatisticsFragment extends Fragment {
refreshDownloadStatistics();
}
+ @Override
+ public void onPrepareOptionsMenu(@NonNull Menu menu) {
+ super.onPrepareOptionsMenu(menu);
+ menu.findItem(R.id.statistics_reset).setVisible(false);
+ menu.findItem(R.id.statistics_filter).setVisible(false);
+ }
+
private void refreshDownloadStatistics() {
progressBar.setVisibility(View.VISIBLE);
downloadStatisticsList.setVisibility(View.GONE);
@@ -72,15 +73,16 @@ public class DownloadStatisticsFragment extends Fragment {
disposable =
Observable.fromCallable(() -> {
- List<StatisticsItem> statisticsData = DBReader.getStatistics();
- Collections.sort(statisticsData, (item1, item2) ->
+ // Filters do not matter here
+ DBReader.StatisticsResult statisticsData = DBReader.getStatistics(false, 0, Long.MAX_VALUE);
+ Collections.sort(statisticsData.feedTime, (item1, item2) ->
Long.compare(item2.totalDownloadSize, item1.totalDownloadSize));
return statisticsData;
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(result -> {
- listAdapter.update(result);
+ listAdapter.update(result.feedTime);
progressBar.setVisibility(View.GONE);
downloadStatisticsList.setVisibility(View.VISIBLE);
}, error -> Log.e(TAG, Log.getStackTraceString(error)));
diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/DownloadStatisticsListAdapter.java b/ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/downloads/DownloadStatisticsListAdapter.java
index 684ba281d..edd3e322f 100644
--- a/app/src/main/java/de/danoeh/antennapod/adapter/DownloadStatisticsListAdapter.java
+++ b/ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/downloads/DownloadStatisticsListAdapter.java
@@ -1,15 +1,15 @@
-package de.danoeh.antennapod.adapter;
+package de.danoeh.antennapod.ui.statistics.downloads;
import android.content.Context;
import android.text.format.Formatter;
+import de.danoeh.antennapod.core.storage.StatisticsItem;
+import de.danoeh.antennapod.ui.statistics.PieChartView;
+import de.danoeh.antennapod.ui.statistics.R;
+import de.danoeh.antennapod.ui.statistics.StatisticsListAdapter;
import java.util.List;
import java.util.Locale;
-import de.danoeh.antennapod.R;
-import de.danoeh.antennapod.core.storage.StatisticsItem;
-import de.danoeh.antennapod.view.PieChartView;
-
/**
* Adapter for the download statistics list.
*/
@@ -20,17 +20,17 @@ public class DownloadStatisticsListAdapter extends StatisticsListAdapter {
}
@Override
- String getHeaderCaption() {
+ protected String getHeaderCaption() {
return context.getString(R.string.total_size_downloaded_podcasts);
}
@Override
- String getHeaderValue() {
+ protected String getHeaderValue() {
return Formatter.formatShortFileSize(context, (long) pieChartData.getSum());
}
@Override
- PieChartView.PieChartData generateChartData(List<StatisticsItem> statisticsData) {
+ protected PieChartView.PieChartData generateChartData(List<StatisticsItem> statisticsData) {
float[] dataValues = new float[statisticsData.size()];
for (int i = 0; i < statisticsData.size(); i++) {
StatisticsItem item = statisticsData.get(i);
@@ -40,7 +40,7 @@ public class DownloadStatisticsListAdapter extends StatisticsListAdapter {
}
@Override
- void onBindFeedViewHolder(StatisticsHolder holder, StatisticsItem item) {
+ protected void onBindFeedViewHolder(StatisticsHolder holder, StatisticsItem item) {
holder.value.setText(Formatter.formatShortFileSize(context, item.totalDownloadSize)
+ " • "
+ String.format(Locale.getDefault(), "%d%s",
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/FeedStatisticsDialogFragment.java b/ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/feed/FeedStatisticsDialogFragment.java
index 33710b2c4..7109bd6a1 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/FeedStatisticsDialogFragment.java
+++ b/ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/feed/FeedStatisticsDialogFragment.java
@@ -1,4 +1,4 @@
-package de.danoeh.antennapod.fragment;
+package de.danoeh.antennapod.ui.statistics.feed;
import android.app.Dialog;
import android.os.Bundle;
@@ -6,7 +6,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.DialogFragment;
-import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.ui.statistics.R;
public class FeedStatisticsDialogFragment extends DialogFragment {
private static final String EXTRA_FEED_ID = "de.danoeh.antennapod.extra.feedId";
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/FeedStatisticsFragment.java b/ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/feed/FeedStatisticsFragment.java
index e85c2a386..1aeeb8fa9 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/FeedStatisticsFragment.java
+++ b/ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/feed/FeedStatisticsFragment.java
@@ -1,4 +1,4 @@
-package de.danoeh.antennapod.fragment;
+package de.danoeh.antennapod.ui.statistics.feed;
import android.os.Bundle;
import android.text.format.Formatter;
@@ -11,13 +11,13 @@ import androidx.fragment.app.Fragment;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.storage.StatisticsItem;
import de.danoeh.antennapod.core.util.Converter;
-import de.danoeh.antennapod.databinding.FeedStatisticsBinding;
+import de.danoeh.antennapod.ui.statistics.databinding.FeedStatisticsBinding;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
-import java.util.List;
+import java.util.Collections;
import java.util.Locale;
public class FeedStatisticsFragment extends Fragment {
@@ -60,8 +60,11 @@ public class FeedStatisticsFragment extends Fragment {
private void loadStatistics() {
disposable =
Observable.fromCallable(() -> {
- List<StatisticsItem> statisticsData = DBReader.getStatistics();
- for (StatisticsItem statisticsItem : statisticsData) {
+ DBReader.StatisticsResult statisticsData = DBReader.getStatistics(true, 0, Long.MAX_VALUE);
+ Collections.sort(statisticsData.feedTime, (item1, item2) ->
+ Long.compare(item2.timePlayed, item1.timePlayed));
+
+ for (StatisticsItem statisticsItem : statisticsData.feedTime) {
if (statisticsItem.feed.getId() == feedId) {
return statisticsItem;
}
@@ -77,7 +80,6 @@ public class FeedStatisticsFragment extends Fragment {
viewBinding.startedTotalLabel.setText(String.format(Locale.getDefault(), "%d / %d",
s.episodesStarted, s.episodes));
viewBinding.timePlayedLabel.setText(Converter.shortLocalizedDuration(getContext(), s.timePlayed));
- viewBinding.durationPlayedLabel.setText(Converter.shortLocalizedDuration(getContext(), s.timePlayedCountAll));
viewBinding.totalDurationLabel.setText(Converter.shortLocalizedDuration(getContext(), s.time));
viewBinding.onDeviceLabel.setText(String.format(Locale.getDefault(), "%d", s.episodesDownloadCount));
viewBinding.spaceUsedLabel.setText(Formatter.formatShortFileSize(getContext(), s.totalDownloadSize));
diff --git a/ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/subscriptions/PlaybackStatisticsListAdapter.java b/ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/subscriptions/PlaybackStatisticsListAdapter.java
new file mode 100644
index 000000000..3936118ca
--- /dev/null
+++ b/ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/subscriptions/PlaybackStatisticsListAdapter.java
@@ -0,0 +1,75 @@
+package de.danoeh.antennapod.ui.statistics.subscriptions;
+
+import androidx.fragment.app.Fragment;
+import de.danoeh.antennapod.core.storage.StatisticsItem;
+import de.danoeh.antennapod.core.util.Converter;
+import de.danoeh.antennapod.ui.statistics.PieChartView;
+import de.danoeh.antennapod.ui.statistics.R;
+import de.danoeh.antennapod.ui.statistics.StatisticsListAdapter;
+import de.danoeh.antennapod.ui.statistics.feed.FeedStatisticsDialogFragment;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * Adapter for the playback statistics list.
+ */
+public class PlaybackStatisticsListAdapter extends StatisticsListAdapter {
+
+ private final Fragment fragment;
+ private long timeFilterFrom = 0;
+ private long timeFilterTo = Long.MAX_VALUE;
+ private boolean includeMarkedAsPlayed = false;
+
+ public PlaybackStatisticsListAdapter(Fragment fragment) {
+ super(fragment.getContext());
+ this.fragment = fragment;
+ }
+
+ public void setTimeFilter(boolean includeMarkedAsPlayed, long timeFilterFrom, long timeFilterTo) {
+ this.includeMarkedAsPlayed = includeMarkedAsPlayed;
+ this.timeFilterFrom = timeFilterFrom;
+ this.timeFilterTo = timeFilterTo;
+ }
+
+ @Override
+ protected String getHeaderCaption() {
+ if (includeMarkedAsPlayed) {
+ return context.getString(R.string.statistics_counting_total);
+ }
+ SimpleDateFormat dateFormat = new SimpleDateFormat("MMM yyyy", Locale.getDefault());
+ String dateFrom = dateFormat.format(new Date(timeFilterFrom));
+ // FilterTo is first day of next month => Subtract one day
+ String dateTo = dateFormat.format(new Date(timeFilterTo - 24L * 3600000L));
+ return context.getString(R.string.statistics_counting_range, dateFrom, dateTo);
+ }
+
+ @Override
+ protected String getHeaderValue() {
+ return Converter.shortLocalizedDuration(context, (long) pieChartData.getSum());
+ }
+
+ @Override
+ protected PieChartView.PieChartData generateChartData(List<StatisticsItem> statisticsData) {
+ float[] dataValues = new float[statisticsData.size()];
+ for (int i = 0; i < statisticsData.size(); i++) {
+ StatisticsItem item = statisticsData.get(i);
+ dataValues[i] = item.timePlayed;
+ }
+ return new PieChartView.PieChartData(dataValues);
+ }
+
+ @Override
+ protected void onBindFeedViewHolder(StatisticsHolder holder, StatisticsItem statsItem) {
+ long time = statsItem.timePlayed;
+ holder.value.setText(Converter.shortLocalizedDuration(context, time));
+
+ holder.itemView.setOnClickListener(v -> {
+ FeedStatisticsDialogFragment yourDialogFragment = FeedStatisticsDialogFragment.newInstance(
+ statsItem.feed.getId(), statsItem.feed.getTitle());
+ yourDialogFragment.show(fragment.getChildFragmentManager().beginTransaction(), "DialogFragment");
+ });
+ }
+}
diff --git a/ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/subscriptions/StatisticsFilterDialog.java b/ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/subscriptions/StatisticsFilterDialog.java
new file mode 100644
index 000000000..93f8e7715
--- /dev/null
+++ b/ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/subscriptions/StatisticsFilterDialog.java
@@ -0,0 +1,135 @@
+package de.danoeh.antennapod.ui.statistics.subscriptions;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.view.LayoutInflater;
+import android.widget.ArrayAdapter;
+import androidx.appcompat.app.AlertDialog;
+import androidx.core.util.Pair;
+import de.danoeh.antennapod.event.StatisticsEvent;
+import de.danoeh.antennapod.ui.statistics.R;
+import de.danoeh.antennapod.ui.statistics.StatisticsFragment;
+import de.danoeh.antennapod.ui.statistics.databinding.StatisticsFilterDialogBinding;
+import org.greenrobot.eventbus.EventBus;
+
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.Locale;
+
+public class StatisticsFilterDialog {
+ private final Context context;
+ private final SharedPreferences prefs;
+ private boolean includeMarkedAsPlayed;
+ private long timeFilterFrom;
+ private long timeFilterTo;
+ private final Pair<String[], Long[]> filterDatesFrom;
+ private final Pair<String[], Long[]> filterDatesTo;
+
+ public StatisticsFilterDialog(Context context, long oldestDate) {
+ this.context = context;
+ prefs = context.getSharedPreferences(StatisticsFragment.PREF_NAME, Context.MODE_PRIVATE);
+ includeMarkedAsPlayed = prefs.getBoolean(StatisticsFragment.PREF_INCLUDE_MARKED_PLAYED, false);
+ timeFilterFrom = prefs.getLong(StatisticsFragment.PREF_FILTER_FROM, 0);
+ timeFilterTo = prefs.getLong(StatisticsFragment.PREF_FILTER_TO, Long.MAX_VALUE);
+ filterDatesFrom = makeMonthlyList(oldestDate, false);
+ filterDatesTo = makeMonthlyList(oldestDate, true);
+ }
+
+ public void show() {
+ StatisticsFilterDialogBinding dialogBinding = StatisticsFilterDialogBinding.inflate(
+ LayoutInflater.from(context));
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ builder.setView(dialogBinding.getRoot());
+ builder.setTitle(R.string.filter);
+ dialogBinding.includeMarkedCheckbox.setOnCheckedChangeListener((compoundButton, checked) -> {
+ dialogBinding.timeToSpinner.setEnabled(!checked);
+ dialogBinding.timeFromSpinner.setEnabled(!checked);
+ dialogBinding.pastYearButton.setEnabled(!checked);
+ dialogBinding.allTimeButton.setEnabled(!checked);
+ dialogBinding.dateSelectionContainer.setAlpha(checked ? 0.5f : 1f);
+ });
+ dialogBinding.includeMarkedCheckbox.setChecked(includeMarkedAsPlayed);
+
+
+ ArrayAdapter<String> adapterFrom = new ArrayAdapter<>(context,
+ android.R.layout.simple_spinner_item, filterDatesFrom.first);
+ adapterFrom.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ dialogBinding.timeFromSpinner.setAdapter(adapterFrom);
+ for (int i = 0; i < filterDatesFrom.second.length; i++) {
+ if (filterDatesFrom.second[i] >= timeFilterFrom) {
+ dialogBinding.timeFromSpinner.setSelection(i);
+ break;
+ }
+ }
+
+ ArrayAdapter<String> adapterTo = new ArrayAdapter<>(context,
+ android.R.layout.simple_spinner_item, filterDatesTo.first);
+ adapterTo.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ dialogBinding.timeToSpinner.setAdapter(adapterTo);
+ for (int i = 0; i < filterDatesTo.second.length; i++) {
+ if (filterDatesTo.second[i] >= timeFilterTo) {
+ dialogBinding.timeToSpinner.setSelection(i);
+ break;
+ }
+ }
+
+ dialogBinding.allTimeButton.setOnClickListener(v -> {
+ dialogBinding.timeFromSpinner.setSelection(0);
+ dialogBinding.timeToSpinner.setSelection(filterDatesTo.first.length - 1);
+ });
+ dialogBinding.pastYearButton.setOnClickListener(v -> {
+ dialogBinding.timeFromSpinner.setSelection(Math.max(0, filterDatesFrom.first.length - 13));
+ dialogBinding.timeToSpinner.setSelection(filterDatesTo.first.length - 2);
+ });
+
+ builder.setPositiveButton(android.R.string.ok, (dialog, which) -> {
+ includeMarkedAsPlayed = dialogBinding.includeMarkedCheckbox.isChecked();
+ if (includeMarkedAsPlayed) {
+ // We do not know the date at which something was marked as played, so filtering does not make sense
+ timeFilterFrom = 0;
+ timeFilterTo = Long.MAX_VALUE;
+ } else {
+ timeFilterFrom = filterDatesFrom.second[dialogBinding.timeFromSpinner.getSelectedItemPosition()];
+ timeFilterTo = filterDatesTo.second[dialogBinding.timeToSpinner.getSelectedItemPosition()];
+ }
+ prefs.edit()
+ .putBoolean(StatisticsFragment.PREF_INCLUDE_MARKED_PLAYED, includeMarkedAsPlayed)
+ .putLong(StatisticsFragment.PREF_FILTER_FROM, timeFilterFrom)
+ .putLong(StatisticsFragment.PREF_FILTER_TO, timeFilterTo)
+ .apply();
+ EventBus.getDefault().post(new StatisticsEvent());
+ });
+ builder.show();
+ }
+
+ private Pair<String[], Long[]> makeMonthlyList(long oldestDate, boolean inclusive) {
+ Calendar date = Calendar.getInstance();
+ date.setTimeInMillis(oldestDate);
+ date.set(Calendar.DAY_OF_MONTH, 1);
+ ArrayList<String> names = new ArrayList<>();
+ ArrayList<Long> timestamps = new ArrayList<>();
+ SimpleDateFormat dateFormat = new SimpleDateFormat("MMM yyyy", Locale.getDefault());
+ while (date.getTimeInMillis() < System.currentTimeMillis()) {
+ names.add(dateFormat.format(new Date(date.getTimeInMillis())));
+ if (!inclusive) {
+ timestamps.add(date.getTimeInMillis());
+ }
+ if (date.get(Calendar.MONTH) == Calendar.DECEMBER) {
+ date.set(Calendar.MONTH, Calendar.JANUARY);
+ date.set(Calendar.YEAR, date.get(Calendar.YEAR) + 1);
+ } else {
+ date.set(Calendar.MONTH, date.get(Calendar.MONTH) + 1);
+ }
+ if (inclusive) {
+ timestamps.add(date.getTimeInMillis());
+ }
+ }
+ if (inclusive) {
+ names.add(context.getString(R.string.statistics_today));
+ timestamps.add(Long.MAX_VALUE);
+ }
+ return new Pair<>(names.toArray(new String[0]), timestamps.toArray(new Long[0]));
+ }
+}
diff --git a/ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/subscriptions/SubscriptionStatisticsFragment.java b/ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/subscriptions/SubscriptionStatisticsFragment.java
new file mode 100644
index 000000000..c7eddd222
--- /dev/null
+++ b/ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/subscriptions/SubscriptionStatisticsFragment.java
@@ -0,0 +1,137 @@
+package de.danoeh.antennapod.ui.statistics.subscriptions;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.Menu;
+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 androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+import de.danoeh.antennapod.core.storage.DBReader;
+import de.danoeh.antennapod.event.StatisticsEvent;
+import de.danoeh.antennapod.ui.statistics.R;
+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.EventBus;
+import org.greenrobot.eventbus.Subscribe;
+import org.greenrobot.eventbus.ThreadMode;
+
+import java.util.Collections;
+
+/**
+ * Displays the 'playback statistics' screen
+ */
+public class SubscriptionStatisticsFragment extends Fragment {
+ private static final String TAG = SubscriptionStatisticsFragment.class.getSimpleName();
+
+ private Disposable disposable;
+ private RecyclerView feedStatisticsList;
+ private ProgressBar progressBar;
+ private PlaybackStatisticsListAdapter listAdapter;
+ private DBReader.StatisticsResult statisticsResult;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setHasOptionsMenu(true);
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
+ @Nullable Bundle savedInstanceState) {
+ View root = inflater.inflate(R.layout.statistics_fragment, container, false);
+ feedStatisticsList = root.findViewById(R.id.statistics_list);
+ progressBar = root.findViewById(R.id.progressBar);
+ listAdapter = new PlaybackStatisticsListAdapter(this);
+ feedStatisticsList.setLayoutManager(new LinearLayoutManager(getContext()));
+ feedStatisticsList.setAdapter(listAdapter);
+ EventBus.getDefault().register(this);
+ return root;
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ refreshStatistics();
+ }
+
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+ EventBus.getDefault().unregister(this);
+ if (disposable != null) {
+ disposable.dispose();
+ }
+ }
+
+ @Subscribe(threadMode = ThreadMode.MAIN)
+ public void statisticsEvent(StatisticsEvent event) {
+ refreshStatistics();
+ }
+
+ @Override
+ public void onPrepareOptionsMenu(@NonNull Menu menu) {
+ super.onPrepareOptionsMenu(menu);
+ menu.findItem(R.id.statistics_reset).setVisible(true);
+ menu.findItem(R.id.statistics_filter).setVisible(true);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (item.getItemId() == R.id.statistics_filter) {
+ if (statisticsResult != null) {
+ new StatisticsFilterDialog(getContext(), statisticsResult.oldestDate).show();
+ }
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ private void refreshStatistics() {
+ progressBar.setVisibility(View.VISIBLE);
+ feedStatisticsList.setVisibility(View.GONE);
+ loadStatistics();
+ }
+
+ private void loadStatistics() {
+ 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);
+ long timeFilterFrom = prefs.getLong(StatisticsFragment.PREF_FILTER_FROM, 0);
+ long timeFilterTo = prefs.getLong(StatisticsFragment.PREF_FILTER_TO, Long.MAX_VALUE);
+ disposable = Observable.fromCallable(
+ () -> {
+ DBReader.StatisticsResult statisticsData = DBReader.getStatistics(
+ includeMarkedAsPlayed, timeFilterFrom, timeFilterTo);
+ Collections.sort(statisticsData.feedTime, (item1, item2) ->
+ Long.compare(item2.timePlayed, item1.timePlayed));
+ return statisticsData;
+ })
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(result -> {
+ statisticsResult = result;
+ // When "from" is "today", set it to today
+ listAdapter.setTimeFilter(includeMarkedAsPlayed, Math.max(
+ Math.min(timeFilterFrom, System.currentTimeMillis()), result.oldestDate),
+ Math.min(timeFilterTo, System.currentTimeMillis()));
+ listAdapter.update(result.feedTime);
+ progressBar.setVisibility(View.GONE);
+ feedStatisticsList.setVisibility(View.VISIBLE);
+ }, error -> Log.e(TAG, Log.getStackTraceString(error)));
+ }
+}
diff --git a/ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/years/BarChartView.java b/ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/years/BarChartView.java
new file mode 100644
index 000000000..eadbb29ee
--- /dev/null
+++ b/ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/years/BarChartView.java
@@ -0,0 +1,135 @@
+package de.danoeh.antennapod.ui.statistics.years;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.DashPathEffect;
+import android.graphics.Paint;
+import android.graphics.PixelFormat;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.widget.AppCompatImageView;
+import de.danoeh.antennapod.core.storage.DBReader;
+import de.danoeh.antennapod.ui.common.ThemeUtils;
+import de.danoeh.antennapod.ui.statistics.R;
+
+import java.util.List;
+
+public class BarChartView extends AppCompatImageView {
+ private BarChartDrawable drawable;
+
+ public BarChartView(Context context) {
+ super(context);
+ setup();
+ }
+
+ public BarChartView(Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ setup();
+ }
+
+ public BarChartView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ setup();
+ }
+
+ @SuppressLint("ClickableViewAccessibility")
+ private void setup() {
+ drawable = new BarChartDrawable();
+ setImageDrawable(drawable);
+ }
+
+ /**
+ * Set of data values to display.
+ */
+ public void setData(List<DBReader.MonthlyStatisticsItem> data) {
+ drawable.data = data;
+ drawable.maxValue = 1;
+ for (DBReader.MonthlyStatisticsItem item : data) {
+ drawable.maxValue = Math.max(drawable.maxValue, item.timePlayed);
+ }
+ }
+
+ private class BarChartDrawable extends Drawable {
+ private static final long ONE_HOUR = 3600000L;
+ private List<DBReader.MonthlyStatisticsItem> data;
+ private long maxValue = 1;
+ private final Paint paintBars;
+ private final Paint paintGridLines;
+ private final Paint paintGridText;
+ private final int[] colors = {0, 0xff9c27b0};
+
+ private BarChartDrawable() {
+ colors[0] = ThemeUtils.getColorFromAttr(getContext(), R.attr.colorAccent);
+ paintBars = new Paint();
+ paintBars.setStyle(Paint.Style.FILL);
+ paintBars.setAntiAlias(true);
+ paintGridLines = new Paint();
+ paintGridLines.setStyle(Paint.Style.STROKE);
+ paintGridLines.setPathEffect(new DashPathEffect(new float[] {10f, 10f}, 0f));
+ paintGridLines.setColor(ThemeUtils.getColorFromAttr(getContext(), android.R.attr.textColorSecondary));
+ paintGridText = new Paint();
+ paintGridText.setAntiAlias(true);
+ paintGridText.setColor(ThemeUtils.getColorFromAttr(getContext(), android.R.attr.textColorSecondary));
+ }
+
+ @Override
+ public void draw(@NonNull Canvas canvas) {
+ final float width = getBounds().width();
+ final float height = getBounds().height();
+ final float barHeight = height * 0.9f;
+ final float textPadding = width * 0.05f;
+ final float stepSize = (width - textPadding) / (data.size() + 2);
+ final float textSize = height * 0.06f;
+ paintGridText.setTextSize(textSize);
+
+ paintBars.setStrokeWidth(height * 0.015f);
+ paintBars.setColor(colors[0]);
+ int colorIndex = 0;
+ int lastYear = data.size() > 0 ? data.get(0).year : 0;
+ for (int i = 0; i < data.size(); i++) {
+ float x = textPadding + (i + 1) * stepSize;
+ if (lastYear != data.get(i).year) {
+ lastYear = data.get(i).year;
+ colorIndex++;
+ paintBars.setColor(colors[colorIndex % 2]);
+ if (i < data.size() - 2) {
+ canvas.drawText(String.valueOf(data.get(i).year), x + stepSize,
+ barHeight + (height - barHeight + textSize) / 2, paintGridText);
+ }
+ canvas.drawLine(x, height, x, barHeight, paintGridText);
+ }
+
+ float valuePercentage = (float) Math.max(0.005, (float) data.get(i).timePlayed / maxValue);
+ float y = (1 - valuePercentage) * barHeight;
+ canvas.drawRect(x, y, x + stepSize * 0.95f, barHeight, paintBars);
+ }
+
+ float maxLine = (float) (Math.floor(maxValue / (10.0 * ONE_HOUR)) * 10 * ONE_HOUR);
+ float y = (1 - (maxLine / maxValue)) * barHeight;
+ canvas.drawLine(0, y, width, y, paintGridLines);
+ canvas.drawText(String.valueOf((long) maxLine / ONE_HOUR), 0, y + 1.2f * textSize, paintGridText);
+
+ float midLine = maxLine / 2;
+ y = (1 - (midLine / maxValue)) * barHeight;
+ canvas.drawLine(0, y, width, y, paintGridLines);
+ canvas.drawText(String.valueOf((long) midLine / ONE_HOUR), 0, y + 1.2f * textSize, paintGridText);
+ }
+
+ @Override
+ public int getOpacity() {
+ return PixelFormat.TRANSLUCENT;
+ }
+
+ @Override
+ public void setAlpha(int alpha) {
+ }
+
+ @Override
+ public void setColorFilter(ColorFilter cf) {
+ }
+ }
+}
diff --git a/ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/years/YearStatisticsListAdapter.java b/ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/years/YearStatisticsListAdapter.java
new file mode 100644
index 000000000..e3251a96b
--- /dev/null
+++ b/ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/years/YearStatisticsListAdapter.java
@@ -0,0 +1,121 @@
+package de.danoeh.antennapod.ui.statistics.years;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+import de.danoeh.antennapod.core.storage.DBReader;
+import de.danoeh.antennapod.ui.statistics.R;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * Adapter for the yearly playback statistics list.
+ */
+public class YearStatisticsListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
+ private static final int TYPE_HEADER = 0;
+ private static final int TYPE_FEED = 1;
+ final Context context;
+ private final List<DBReader.MonthlyStatisticsItem> statisticsData = new ArrayList<>();
+ private final List<DBReader.MonthlyStatisticsItem> yearlyAggregate = new ArrayList<>();
+
+ public YearStatisticsListAdapter(Context context) {
+ this.context = context;
+ }
+
+ @Override
+ public int getItemCount() {
+ return yearlyAggregate.size() + 1;
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ return position == 0 ? TYPE_HEADER : TYPE_FEED;
+ }
+
+ @NonNull
+ @Override
+ public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ LayoutInflater inflater = LayoutInflater.from(context);
+ if (viewType == TYPE_HEADER) {
+ return new HeaderHolder(inflater.inflate(R.layout.statistics_listitem_barchart, parent, false));
+ }
+ return new StatisticsHolder(inflater.inflate(R.layout.statistics_year_listitem, parent, false));
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull RecyclerView.ViewHolder h, int position) {
+ if (getItemViewType(position) == TYPE_HEADER) {
+ HeaderHolder holder = (HeaderHolder) h;
+ holder.barChart.setData(statisticsData);
+ } else {
+ StatisticsHolder holder = (StatisticsHolder) h;
+ DBReader.MonthlyStatisticsItem statsItem = yearlyAggregate.get(position - 1);
+ holder.year.setText(String.format(Locale.getDefault(), "%d ", statsItem.year));
+ holder.hours.setText(String.format(Locale.getDefault(), "%.1f ", statsItem.timePlayed / 3600000.0f)
+ + context.getString(R.string.time_hours));
+ }
+ }
+
+ public void update(List<DBReader.MonthlyStatisticsItem> statistics) {
+ int lastYear = statistics.size() > 0 ? statistics.get(0).year : 0;
+ int lastDataPoint = statistics.size() > 0 ? (statistics.get(0).month - 1) + lastYear * 12 : 0;
+ long yearSum = 0;
+ yearlyAggregate.clear();
+ statisticsData.clear();
+ for (DBReader.MonthlyStatisticsItem statistic : statistics) {
+ if (statistic.year != lastYear) {
+ DBReader.MonthlyStatisticsItem yearAggregate = new DBReader.MonthlyStatisticsItem();
+ yearAggregate.year = lastYear;
+ yearAggregate.timePlayed = yearSum;
+ yearlyAggregate.add(yearAggregate);
+ yearSum = 0;
+ lastYear = statistic.year;
+ }
+ yearSum += statistic.timePlayed;
+ while (lastDataPoint + 1 < (statistic.month - 1) + statistic.year * 12) {
+ lastDataPoint++;
+ DBReader.MonthlyStatisticsItem item = new DBReader.MonthlyStatisticsItem();
+ item.year = lastDataPoint / 12;
+ item.month = lastDataPoint % 12 + 1;
+ statisticsData.add(item); // Compensate for months without playback
+ System.out.println("aaaaa extra:" + item.month + "/" + item.year);
+ }
+ System.out.println("aaaaa add:" + statistic.month + "/" + statistic.year);
+ statisticsData.add(statistic);
+ lastDataPoint = (statistic.month - 1) + statistic.year * 12;
+ }
+ DBReader.MonthlyStatisticsItem yearAggregate = new DBReader.MonthlyStatisticsItem();
+ yearAggregate.year = lastYear;
+ yearAggregate.timePlayed = yearSum;
+ yearlyAggregate.add(yearAggregate);
+ Collections.reverse(yearlyAggregate);
+ notifyDataSetChanged();
+ }
+
+ static class HeaderHolder extends RecyclerView.ViewHolder {
+ BarChartView barChart;
+
+ HeaderHolder(View itemView) {
+ super(itemView);
+ barChart = itemView.findViewById(R.id.barChart);
+ }
+ }
+
+ static class StatisticsHolder extends RecyclerView.ViewHolder {
+ TextView year;
+ TextView hours;
+
+ StatisticsHolder(View itemView) {
+ super(itemView);
+ year = itemView.findViewById(R.id.yearLabel);
+ hours = itemView.findViewById(R.id.hoursLabel);
+ }
+ }
+}
diff --git a/ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/years/YearsStatisticsFragment.java b/ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/years/YearsStatisticsFragment.java
new file mode 100644
index 000000000..5368e52a9
--- /dev/null
+++ b/ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/years/YearsStatisticsFragment.java
@@ -0,0 +1,97 @@
+package de.danoeh.antennapod.ui.statistics.years;
+
+import android.os.Bundle;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.Menu;
+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 androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+import de.danoeh.antennapod.core.storage.DBReader;
+import de.danoeh.antennapod.event.StatisticsEvent;
+import de.danoeh.antennapod.ui.statistics.R;
+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;
+
+/**
+ * Displays the yearly statistics screen
+ */
+public class YearsStatisticsFragment extends Fragment {
+ private static final String TAG = YearsStatisticsFragment.class.getSimpleName();
+
+ private Disposable disposable;
+ private RecyclerView yearStatisticsList;
+ private ProgressBar progressBar;
+ private YearStatisticsListAdapter listAdapter;
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
+ @Nullable Bundle savedInstanceState) {
+ View root = inflater.inflate(R.layout.statistics_fragment, container, false);
+ yearStatisticsList = root.findViewById(R.id.statistics_list);
+ progressBar = root.findViewById(R.id.progressBar);
+ listAdapter = new YearStatisticsListAdapter(getContext());
+ yearStatisticsList.setLayoutManager(new LinearLayoutManager(getContext()));
+ yearStatisticsList.setAdapter(listAdapter);
+ EventBus.getDefault().register(this);
+ return root;
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ refreshStatistics();
+ }
+
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+ EventBus.getDefault().unregister(this);
+ if (disposable != null) {
+ disposable.dispose();
+ }
+ }
+
+ @Subscribe(threadMode = ThreadMode.MAIN)
+ public void statisticsEvent(StatisticsEvent event) {
+ refreshStatistics();
+ }
+
+ @Override
+ public void onPrepareOptionsMenu(@NonNull Menu menu) {
+ super.onPrepareOptionsMenu(menu);
+ menu.findItem(R.id.statistics_reset).setVisible(true);
+ menu.findItem(R.id.statistics_filter).setVisible(false);
+ }
+
+ private void refreshStatistics() {
+ progressBar.setVisibility(View.VISIBLE);
+ yearStatisticsList.setVisibility(View.GONE);
+ loadStatistics();
+ }
+
+ private void loadStatistics() {
+ if (disposable != null) {
+ disposable.dispose();
+ }
+ disposable = Observable.fromCallable(DBReader::getMonthlyTimeStatistics)
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(result -> {
+ listAdapter.update(result);
+ progressBar.setVisibility(View.GONE);
+ yearStatisticsList.setVisibility(View.VISIBLE);
+ }, error -> Log.e(TAG, Log.getStackTraceString(error)));
+ }
+}
diff --git a/app/src/main/res/layout/feed_statistics.xml b/ui/statistics/src/main/res/layout/feed_statistics.xml
index f8f5ac555..7897a7d5f 100644
--- a/app/src/main/res/layout/feed_statistics.xml
+++ b/ui/statistics/src/main/res/layout/feed_statistics.xml
@@ -46,24 +46,6 @@
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:text="@string/statistics_duration_played_episodes" />
-
- <TextView
- android:id="@+id/durationPlayedLabel"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_marginLeft="8dp"
- android:layout_marginStart="8dp"
- tools:text="0 min" />
-
- </TableRow>
-
- <TableRow
- android:tag="detailed">
-
- <TextView
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
android:text="@string/statistics_total_duration" />
<TextView
diff --git a/app/src/main/res/layout/feed_statistics_dialog.xml b/ui/statistics/src/main/res/layout/feed_statistics_dialog.xml
index fcd36fe7a..fcd36fe7a 100644
--- a/app/src/main/res/layout/feed_statistics_dialog.xml
+++ b/ui/statistics/src/main/res/layout/feed_statistics_dialog.xml
diff --git a/ui/statistics/src/main/res/layout/statistics_filter_dialog.xml b/ui/statistics/src/main/res/layout/statistics_filter_dialog.xml
new file mode 100644
index 000000000..684ceabd2
--- /dev/null
+++ b/ui/statistics/src/main/res/layout/statistics_filter_dialog.xml
@@ -0,0 +1,95 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:padding="16dp">
+
+ <CheckBox
+ android:id="@+id/includeMarkedCheckbox"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/statistics_include_marked"
+ android:layout_marginBottom="8dp" />
+
+ <LinearLayout
+ android:id="@+id/dateSelectionContainer"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <TextView
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:text="@string/statistics_from"
+ android:padding="4dp"
+ android:layout_weight="1" />
+
+ <TextView
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:text="@string/statistics_to"
+ android:padding="4dp"
+ android:layout_weight="1" />
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <Spinner
+ android:id="@+id/timeFromSpinner"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1" />
+
+ <Spinner
+ android:id="@+id/timeToSpinner"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1" />
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <Button
+ android:id="@+id/past_year_button"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:text="@string/statistics_filter_past_year"
+ android:layout_weight="1"
+ android:layout_marginEnd="4dp"
+ style="@style/Widget.MaterialComponents.Button.OutlinedButton" />
+
+ <Button
+ android:id="@+id/allTimeButton"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:text="@string/statistics_filter_all_time"
+ android:layout_weight="1"
+ android:layout_marginStart="4dp"
+ style="@style/Widget.MaterialComponents.Button.OutlinedButton" />
+
+ </LinearLayout>
+
+ </LinearLayout>
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/statistics_speed_not_counted"
+ android:layout_marginTop="16dp" />
+
+</LinearLayout>
diff --git a/app/src/main/res/layout/statistics_activity.xml b/ui/statistics/src/main/res/layout/statistics_fragment.xml
index 9d9cad438..9d9cad438 100644
--- a/app/src/main/res/layout/statistics_activity.xml
+++ b/ui/statistics/src/main/res/layout/statistics_fragment.xml
diff --git a/app/src/main/res/layout/statistics_listitem.xml b/ui/statistics/src/main/res/layout/statistics_listitem.xml
index 5989595e1..5989595e1 100644
--- a/app/src/main/res/layout/statistics_listitem.xml
+++ b/ui/statistics/src/main/res/layout/statistics_listitem.xml
diff --git a/ui/statistics/src/main/res/layout/statistics_listitem_barchart.xml b/ui/statistics/src/main/res/layout/statistics_listitem_barchart.xml
new file mode 100644
index 000000000..d70e1da07
--- /dev/null
+++ b/ui/statistics/src/main/res/layout/statistics_listitem_barchart.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:padding="16dp">
+
+ <de.danoeh.antennapod.ui.statistics.years.BarChartView
+ android:id="@+id/barChart"
+ android:layout_width="match_parent"
+ android:layout_height="200dp" />
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:layout_marginTop="16dp"
+ android:background="?android:attr/dividerVertical" />
+
+</LinearLayout>
diff --git a/app/src/main/res/layout/statistics_listitem_total.xml b/ui/statistics/src/main/res/layout/statistics_listitem_total.xml
index 628e26c1f..4d5a77fec 100644
--- a/app/src/main/res/layout/statistics_listitem_total.xml
+++ b/ui/statistics/src/main/res/layout/statistics_listitem_total.xml
@@ -1,11 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
-<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<RelativeLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp">
- <de.danoeh.antennapod.view.PieChartView
+ <de.danoeh.antennapod.ui.statistics.PieChartView
android:id="@+id/pie_chart"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@@ -16,33 +17,32 @@
android:layout_marginLeft="8dp" />
<TextView
- android:id="@+id/total_description"
- android:layout_height="wrap_content"
- android:layout_width="wrap_content"
- android:layout_centerHorizontal="true"
- android:textAlignment="center"
- android:layout_marginLeft="56dp"
- android:layout_marginRight="56dp"
- android:maxLines="3"
- android:layout_above="@id/total_time"
- android:textSize="14sp" />
-
- <TextView
+ android:id="@+id/total_time"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:id="@+id/total_time"
android:textColor="?android:attr/textColorPrimary"
android:gravity="center_horizontal"
android:textSize="28sp"
+ android:layout_marginBottom="4dp"
+ android:layout_above="@id/total_description"
+ tools:text="10.0 hours" />
+
+ <TextView
+ android:id="@+id/total_description"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerHorizontal="true"
+ android:textAlignment="center"
+ android:maxLines="3"
+ android:textSize="14sp"
android:layout_marginBottom="16dp"
- android:layout_alignBottom="@id/pie_chart"
- tools:text="10.0 hours"/>
+ android:layout_alignBottom="@id/pie_chart" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="16dp"
android:background="?android:attr/dividerVertical"
- android:layout_below="@+id/pie_chart"/>
+ android:layout_below="@+id/pie_chart" />
-</RelativeLayout> \ No newline at end of file
+</RelativeLayout>
diff --git a/ui/statistics/src/main/res/layout/statistics_year_listitem.xml b/ui/statistics/src/main/res/layout/statistics_year_listitem.xml
new file mode 100644
index 000000000..48b910c7f
--- /dev/null
+++ b/ui/statistics/src/main/res/layout/statistics_year_listitem.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:paddingLeft="16dp"
+ android:paddingRight="16dp"
+ android:paddingTop="16dp"
+ android:paddingBottom="8dp"
+ android:background="?android:attr/selectableItemBackground">
+
+ <TextView
+ android:id="@+id/yearLabel"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:lines="1"
+ android:textColor="?android:attr/textColorPrimary"
+ android:textSize="16sp"
+ tools:text="2020" />
+
+ <TextView
+ android:id="@+id/hoursLabel"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:lines="1"
+ android:textColor="?android:attr/textColorTertiary"
+ android:textSize="14sp"
+ tools:text="23 hours" />
+
+</LinearLayout>
diff --git a/app/src/main/res/menu/statistics.xml b/ui/statistics/src/main/res/menu/statistics.xml
index 9e4b7fab1..4610a7726 100644
--- a/app/src/main/res/menu/statistics.xml
+++ b/ui/statistics/src/main/res/menu/statistics.xml
@@ -5,14 +5,13 @@
<item
android:id="@+id/statistics_reset"
android:title="@string/statistics_reset_data"
- custom:showAsAction="never"
- />
+ custom:showAsAction="never" />
<item
- android:id="@+id/statistics_mode"
+ android:id="@+id/statistics_filter"
android:icon="@drawable/ic_filter"
- android:title="@string/statistics_mode"
- custom:showAsAction="never">
+ android:title="@string/filter"
+ custom:showAsAction="ifRoom">
</item>
</menu>