summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/ISSUE_TEMPLATE/bug_report.md50
-rw-r--r--.github/ISSUE_TEMPLATE/bug_report.yml78
-rw-r--r--.github/ISSUE_TEMPLATE/feature_request.md31
-rw-r--r--.github/ISSUE_TEMPLATE/feature_request.yml48
-rw-r--r--.github/workflows/android-emulator.yml4
-rw-r--r--app/build.gradle15
-rw-r--r--app/src/androidTest/java/de/test/antennapod/playback/PlaybackTest.java2
-rw-r--r--app/src/androidTest/java/de/test/antennapod/service/playback/CancelablePSMPCallback.java52
-rw-r--r--app/src/androidTest/java/de/test/antennapod/service/playback/DefaultPSMPCallback.java82
-rw-r--r--app/src/androidTest/java/de/test/antennapod/service/playback/PlaybackServiceMediaPlayerTest.java65
-rw-r--r--app/src/androidTest/java/de/test/antennapod/service/playback/PlaybackServiceTaskManagerTest.java131
-rw-r--r--app/src/androidTest/java/de/test/antennapod/ui/PreferencesTest.java5
-rw-r--r--app/src/androidTest/java/de/test/antennapod/ui/UITestUtils.java4
-rw-r--r--app/src/androidTest/java/de/test/antennapod/util/event/FeedItemEventListener.java2
-rw-r--r--app/src/free/java/de/danoeh/antennapod/config/CastCallbackImpl.java7
-rw-r--r--app/src/free/java/de/danoeh/antennapod/preferences/PreferenceControllerFlavorHelper.java14
-rw-r--r--app/src/main/AndroidManifest.xml74
-rw-r--r--app/src/main/java/de/danoeh/antennapod/activity/BugReportActivity.java26
-rw-r--r--app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java11
-rw-r--r--app/src/main/java/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java67
-rw-r--r--app/src/main/java/de/danoeh/antennapod/activity/OpmlImportActivity.java41
-rw-r--r--app/src/main/java/de/danoeh/antennapod/activity/PreferenceActivity.java11
-rw-r--r--app/src/main/java/de/danoeh/antennapod/activity/SelectSubscriptionActivity.java162
-rw-r--r--app/src/main/java/de/danoeh/antennapod/activity/VideoplayerActivity.java73
-rw-r--r--app/src/main/java/de/danoeh/antennapod/activity/WidgetConfigActivity.java18
-rw-r--r--app/src/main/java/de/danoeh/antennapod/adapter/CoverLoader.java41
-rw-r--r--app/src/main/java/de/danoeh/antennapod/adapter/DataFolderAdapter.java3
-rw-r--r--app/src/main/java/de/danoeh/antennapod/adapter/FeedItemlistDescriptionAdapter.java2
-rw-r--r--app/src/main/java/de/danoeh/antennapod/adapter/NavListAdapter.java10
-rw-r--r--app/src/main/java/de/danoeh/antennapod/adapter/PlaybackStatisticsListAdapter.java24
-rw-r--r--app/src/main/java/de/danoeh/antennapod/adapter/QueueRecyclerAdapter.java3
-rw-r--r--app/src/main/java/de/danoeh/antennapod/adapter/SubscriptionsRecyclerAdapter.java55
-rw-r--r--app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/CancelDownloadActionButton.java2
-rw-r--r--app/src/main/java/de/danoeh/antennapod/config/ClientConfigurator.java1
-rw-r--r--app/src/main/java/de/danoeh/antennapod/config/DownloadServiceCallbacksImpl.java15
-rw-r--r--app/src/main/java/de/danoeh/antennapod/dialog/EpisodeFilterDialog.java21
-rw-r--r--app/src/main/java/de/danoeh/antennapod/dialog/FeedSortDialog.java2
-rw-r--r--app/src/main/java/de/danoeh/antennapod/dialog/PlaybackControlsDialog.java29
-rw-r--r--app/src/main/java/de/danoeh/antennapod/dialog/ProxyDialog.java70
-rw-r--r--app/src/main/java/de/danoeh/antennapod/dialog/RemoveFeedDialog.java16
-rw-r--r--app/src/main/java/de/danoeh/antennapod/dialog/RenameFeedDialog.java50
-rw-r--r--app/src/main/java/de/danoeh/antennapod/dialog/RenameItemDialog.java81
-rw-r--r--app/src/main/java/de/danoeh/antennapod/dialog/SleepTimerDialog.java39
-rw-r--r--app/src/main/java/de/danoeh/antennapod/dialog/SubscriptionsFilterDialog.java2
-rw-r--r--app/src/main/java/de/danoeh/antennapod/dialog/TagSettingsDialog.java44
-rw-r--r--app/src/main/java/de/danoeh/antennapod/dialog/VariableSpeedDialog.java35
-rw-r--r--app/src/main/java/de/danoeh/antennapod/discovery/GpodnetPodcastSearcher.java6
-rw-r--r--app/src/main/java/de/danoeh/antennapod/discovery/ItunesPodcastSearcher.java10
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/AddFeedFragment.java66
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/AudioPlayerFragment.java125
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/ChaptersFragment.java4
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/CompletedDownloadsFragment.java8
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/CoverFragment.java14
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/DiscoveryFragment.java2
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/DownloadLogFragment.java2
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/EpisodesListFragment.java28
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/ExternalPlayerFragment.java46
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/FavoriteEpisodesFragment.java2
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/FeedInfoFragment.java135
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/FeedItemlistFragment.java52
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/FeedSettingsFragment.java76
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/FeedStatisticsDialogFragment.java42
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/FeedStatisticsFragment.java93
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/ItemFragment.java31
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/ItemPagerFragment.java5
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/NavDrawerFragment.java60
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/OnlineSearchFragment.java32
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/PlaybackHistoryFragment.java28
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java36
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/QuickFeedDiscoveryFragment.java2
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/SearchFragment.java35
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/SubscriptionFragment.java34
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/actions/EpisodeMultiSelectActionHandler.java13
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/actions/FeedMultiSelectActionHandler.java54
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/PodcastListFragment.java8
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/TagListFragment.java6
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/preferences/DownloadStatisticsFragment.java3
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/preferences/GpodderPreferencesFragment.java128
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/preferences/ImportExportPreferencesFragment.java218
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/preferences/MainPreferencesFragment.java28
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/preferences/NotificationPreferencesFragment.java6
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/preferences/PlaybackPreferencesFragment.java4
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/preferences/PlaybackStatisticsFragment.java7
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/preferences/UserInterfacePreferencesFragment.java4
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/preferences/synchronization/GpodderAuthenticationFragment.java (renamed from app/src/main/java/de/danoeh/antennapod/fragment/preferences/GpodderAuthenticationFragment.java)52
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/preferences/synchronization/NextcloudAuthenticationFragment.java92
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/preferences/synchronization/SynchronizationPreferencesFragment.java222
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/SwipeActions.java4
-rw-r--r--app/src/main/java/de/danoeh/antennapod/menuhandler/FeedItemMenuHandler.java12
-rw-r--r--app/src/main/java/de/danoeh/antennapod/preferences/PreferenceUpgrader.java7
-rw-r--r--app/src/main/java/de/danoeh/antennapod/receiver/ConnectivityActionReceiver.java32
-rw-r--r--app/src/main/java/de/danoeh/antennapod/view/PlaybackSpeedSeekBar.java40
-rw-r--r--app/src/main/java/de/danoeh/antennapod/view/ToolbarIconTintManager.java3
-rw-r--r--app/src/main/java/de/danoeh/antennapod/view/viewholder/EpisodeItemViewHolder.java2
-rw-r--r--app/src/main/res/layout/alertdialog_sync_provider_chooser.xml24
-rw-r--r--app/src/main/res/layout/audioplayer_fragment.xml1
-rw-r--r--app/src/main/res/layout/edit_tags_dialog.xml10
-rw-r--r--app/src/main/res/layout/episode_filter_dialog.xml17
-rw-r--r--app/src/main/res/layout/feed_statistics.xml113
-rw-r--r--app/src/main/res/layout/feed_statistics_dialog.xml7
-rw-r--r--app/src/main/res/layout/feedinfo.xml294
-rw-r--r--app/src/main/res/layout/feeditem_pager_fragment.xml1
-rw-r--r--app/src/main/res/layout/feedsettings.xml1
-rw-r--r--app/src/main/res/layout/fragment_subscriptions.xml5
-rw-r--r--app/src/main/res/layout/nextcloud_auth_dialog.xml63
-rw-r--r--app/src/main/res/layout/playback_speed_feed_setting_dialog.xml37
-rw-r--r--app/src/main/res/layout/playback_speed_seek_bar.xml2
-rw-r--r--app/src/main/res/layout/quick_feed_discovery_item.xml4
-rw-r--r--app/src/main/res/layout/subscription_selection_activity.xml58
-rw-r--r--app/src/main/res/layout/time_dialog.xml196
-rw-r--r--app/src/main/res/menu/cast_enabled.xml10
-rw-r--r--app/src/main/res/menu/nav_feed_action_speeddial.xml5
-rw-r--r--app/src/main/res/menu/nav_feed_context.xml4
-rw-r--r--app/src/main/res/menu/nav_folder_context.xml7
-rw-r--r--app/src/main/res/xml/feed_settings.xml97
-rw-r--r--app/src/main/res/xml/network_security_config.xml9
-rw-r--r--app/src/main/res/xml/preferences.xml2
-rw-r--r--app/src/main/res/xml/preferences_gpodder.xml28
-rw-r--r--app/src/main/res/xml/preferences_playback.xml8
-rw-r--r--app/src/main/res/xml/preferences_synchronization.xml31
-rw-r--r--app/src/main/res/xml/preferences_user_interface.xml5
-rw-r--r--app/src/main/res/xml/provider_paths.xml2
-rw-r--r--app/src/play/java/de/danoeh/antennapod/activity/CastEnabledActivity.java157
-rw-r--r--app/src/play/java/de/danoeh/antennapod/config/CastCallbackImpl.java21
-rw-r--r--app/src/play/java/de/danoeh/antennapod/dialog/CustomMRControllerDialog.java480
-rw-r--r--app/src/play/java/de/danoeh/antennapod/fragment/CustomMRControllerDialogFragment.java15
-rw-r--r--app/src/play/java/de/danoeh/antennapod/preferences/PreferenceControllerFlavorHelper.java48
-rw-r--r--app/src/play/res/layout/media_router_controller.xml41
-rw-r--r--build.gradle20
-rw-r--r--common.gradle4
-rw-r--r--config/spotbugs/exclude.xml2
-rw-r--r--core/build.gradle13
-rw-r--r--core/lint.xml4
-rw-r--r--core/src/debug/res/drawable-nodpi/ic_launcher_foreground_no_finish.pngbin0 -> 70874 bytes
-rw-r--r--core/src/free/java/de/danoeh/antennapod/core/CastCallbacks.java7
-rw-r--r--core/src/free/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceFlavorHelper.java54
-rw-r--r--core/src/free/java/de/danoeh/antennapod/core/service/playback/WearMediaSession.java15
-rw-r--r--core/src/free/res/values/strings.xml4
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/ClientConfig.java (renamed from core/src/free/java/de/danoeh/antennapod/core/ClientConfig.java)2
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/export/favorites/FavoritesWriter.java15
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/feed/LocalFeedUpdater.java2
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/glide/ApGlideModule.java5
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/glide/NoHttpStringLoader.java39
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/glide/PaletteBitmap.java20
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/glide/PaletteBitmapResource.java40
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/glide/PaletteBitmapTranscoder.java32
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/glide/ResizingOkHttpStreamFetcher.java5
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/preferences/GpodnetPreferences.java115
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/preferences/PlaybackPreferences.java4
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/preferences/UserPreferences.java24
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadService.java33
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/download/HttpDownloader.java25
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/download/NewEpisodesNotification.java9
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/download/handler/MediaDownloadedHandler.java15
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/playback/ExoPlayerWrapper.java78
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/playback/LocalPSMP.java104
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java481
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceNotificationBuilder.java33
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceTaskManager.java48
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackVolumeUpdater.java2
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/playback/PlayerStatus.java33
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/storage/AutomaticDownloadAlgorithm.java4
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java11
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java38
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/storage/DBUpgrader.java12
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java73
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/storage/NavDrawerData.java8
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/storage/PodDBAdapter.java104
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/storage/mapper/FeedItemCursorMapper.java2
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/storage/mapper/FeedMediaCursorMapper.java2
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/storage/mapper/FeedPreferencesCursorMapper.java6
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/sync/EpisodeActionFilter.java1
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/sync/LockingAsyncExecutor.java35
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/sync/SyncService.java400
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/sync/SynchronizationCredentials.java67
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/sync/SynchronizationProviderViewData.java47
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/sync/SynchronizationSettings.java83
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/sync/queue/SynchronizationQueueSink.java67
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/sync/queue/SynchronizationQueueStorage.java140
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/FeedItemPermutors.java14
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/NetworkUtils.java113
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/ShareUtils.java41
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/StorageUtils.java23
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/comparator/CompareCompat.java29
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/download/AutoUpdateManager.java24
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/download/ConnectionStateMonitor.java46
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java74
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/widget/WidgetUpdater.java82
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/widget/WidgetUpdaterJobService.java2
-rw-r--r--core/src/main/res/drawable-nodpi/nextcloud_logo.pngbin0 -> 3432 bytes
-rw-r--r--core/src/main/res/drawable/ic_download_black.xml9
-rw-r--r--core/src/main/res/drawable/ic_tag.xml9
-rw-r--r--core/src/main/res/layout/player_widget.xml7
-rw-r--r--core/src/main/res/values-land/dimens.xml4
-rw-r--r--core/src/main/res/values-v21/styles.xml3
-rw-r--r--core/src/main/res/values-v23/styles.xml3
-rw-r--r--core/src/main/res/values-v27/styles.xml11
-rw-r--r--core/src/main/res/values/arrays.xml52
-rw-r--r--core/src/main/res/values/colors.xml1
-rw-r--r--core/src/main/res/values/dimens.xml7
-rw-r--r--core/src/main/res/values/strings.xml76
-rw-r--r--core/src/play/java/de/danoeh/antennapod/core/CastCallbacks.java12
-rw-r--r--core/src/play/java/de/danoeh/antennapod/core/ClientConfig.java64
-rw-r--r--core/src/play/java/de/danoeh/antennapod/core/cast/CastButtonVisibilityManager.java120
-rw-r--r--core/src/play/java/de/danoeh/antennapod/core/cast/CastConsumer.java11
-rw-r--r--core/src/play/java/de/danoeh/antennapod/core/cast/CastManager.java1091
-rw-r--r--core/src/play/java/de/danoeh/antennapod/core/cast/CastUtils.java303
-rw-r--r--core/src/play/java/de/danoeh/antennapod/core/cast/DefaultCastConsumer.java10
-rw-r--r--core/src/play/java/de/danoeh/antennapod/core/cast/MediaInfoCreator.java57
-rw-r--r--core/src/play/java/de/danoeh/antennapod/core/cast/SwitchableMediaRouteActionProvider.java106
-rw-r--r--core/src/play/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceFlavorHelper.java314
-rw-r--r--core/src/play/java/de/danoeh/antennapod/core/service/playback/WearMediaSession.java28
-rw-r--r--core/src/play/res/values/strings.xml4
-rw-r--r--core/src/test/java/de/danoeh/antennapod/core/feed/FeedFilterTest.java31
-rw-r--r--core/src/test/java/de/danoeh/antennapod/core/feed/FeedItemTest.java37
-rw-r--r--core/src/test/java/de/danoeh/antennapod/core/service/playback/PlaybackVolumeUpdaterTest.java2
-rw-r--r--core/src/test/java/de/danoeh/antennapod/core/storage/DbWriterTest.java5
-rw-r--r--core/src/test/java/de/danoeh/antennapod/core/sync/GuidValidatorTest.java1
-rw-r--r--event/build.gradle8
-rw-r--r--event/src/main/AndroidManifest.xml1
-rw-r--r--event/src/main/java/de/danoeh/antennapod/event/DiscoveryDefaultUpdateEvent.java (renamed from core/src/main/java/de/danoeh/antennapod/core/event/DiscoveryDefaultUpdateEvent.java)2
-rw-r--r--event/src/main/java/de/danoeh/antennapod/event/FavoritesEvent.java (renamed from core/src/main/java/de/danoeh/antennapod/core/event/FavoritesEvent.java)17
-rw-r--r--event/src/main/java/de/danoeh/antennapod/event/FeedItemEvent.java (renamed from core/src/main/java/de/danoeh/antennapod/core/event/FeedItemEvent.java)15
-rw-r--r--event/src/main/java/de/danoeh/antennapod/event/FeedListUpdateEvent.java (renamed from core/src/main/java/de/danoeh/antennapod/core/event/FeedListUpdateEvent.java)2
-rw-r--r--event/src/main/java/de/danoeh/antennapod/event/MessageEvent.java (renamed from core/src/main/java/de/danoeh/antennapod/core/event/MessageEvent.java)2
-rw-r--r--event/src/main/java/de/danoeh/antennapod/event/PlayerErrorEvent.java13
-rw-r--r--event/src/main/java/de/danoeh/antennapod/event/PlayerStatusEvent.java (renamed from core/src/main/java/de/danoeh/antennapod/core/event/PlayerStatusEvent.java)2
-rw-r--r--event/src/main/java/de/danoeh/antennapod/event/QueueEvent.java (renamed from core/src/main/java/de/danoeh/antennapod/core/event/QueueEvent.java)15
-rw-r--r--event/src/main/java/de/danoeh/antennapod/event/SyncServiceEvent.java (renamed from core/src/main/java/de/danoeh/antennapod/core/event/SyncServiceEvent.java)2
-rw-r--r--event/src/main/java/de/danoeh/antennapod/event/UnreadItemsUpdateEvent.java (renamed from core/src/main/java/de/danoeh/antennapod/core/event/UnreadItemsUpdateEvent.java)2
-rw-r--r--event/src/main/java/de/danoeh/antennapod/event/playback/BufferUpdateEvent.java35
-rw-r--r--event/src/main/java/de/danoeh/antennapod/event/playback/PlaybackHistoryEvent.java (renamed from core/src/main/java/de/danoeh/antennapod/core/event/PlaybackHistoryEvent.java)2
-rw-r--r--event/src/main/java/de/danoeh/antennapod/event/playback/PlaybackPositionEvent.java (renamed from core/src/main/java/de/danoeh/antennapod/core/event/PlaybackPositionEvent.java)2
-rw-r--r--event/src/main/java/de/danoeh/antennapod/event/playback/PlaybackServiceEvent.java (renamed from core/src/main/java/de/danoeh/antennapod/core/event/ServiceEvent.java)6
-rw-r--r--event/src/main/java/de/danoeh/antennapod/event/playback/SleepTimerUpdatedEvent.java38
-rw-r--r--event/src/main/java/de/danoeh/antennapod/event/playback/SpeedChangedEvent.java13
-rw-r--r--event/src/main/java/de/danoeh/antennapod/event/settings/SkipIntroEndingChangedEvent.java (renamed from core/src/main/java/de/danoeh/antennapod/core/event/settings/SkipIntroEndingChangedEvent.java)2
-rw-r--r--event/src/main/java/de/danoeh/antennapod/event/settings/SpeedPresetChangedEvent.java (renamed from core/src/main/java/de/danoeh/antennapod/core/event/settings/SpeedPresetChangedEvent.java)2
-rw-r--r--event/src/main/java/de/danoeh/antennapod/event/settings/VolumeAdaptionChangedEvent.java (renamed from core/src/main/java/de/danoeh/antennapod/core/event/settings/VolumeAdaptionChangedEvent.java)2
-rw-r--r--model/src/main/java/de/danoeh/antennapod/model/feed/FeedFilter.java35
-rw-r--r--model/src/main/java/de/danoeh/antennapod/model/feed/FeedItem.java45
-rw-r--r--net/sync/gpoddernet/build.gradle3
-rw-r--r--net/sync/gpoddernet/src/main/java/de/danoeh/antennapod/net/sync/HostnameParser.java41
-rw-r--r--net/sync/gpoddernet/src/main/java/de/danoeh/antennapod/net/sync/gpoddernet/GpodnetService.java133
-rw-r--r--net/sync/gpoddernet/src/main/java/de/danoeh/antennapod/net/sync/gpoddernet/mapper/ResponseMapper.java60
-rw-r--r--net/sync/gpoddernet/src/main/java/de/danoeh/antennapod/net/sync/nextcloud/NextcloudLoginFlow.java107
-rw-r--r--net/sync/gpoddernet/src/main/java/de/danoeh/antennapod/net/sync/nextcloud/NextcloudSyncService.java169
-rw-r--r--net/sync/gpoddernet/src/main/java/de/danoeh/antennapod/net/sync/nextcloud/NextcloudSynchronizationServiceException.java9
-rw-r--r--net/sync/model/build.gradle1
-rw-r--r--net/sync/model/src/main/java/de/danoeh/antennapod/net/sync/model/EpisodeAction.java10
-rw-r--r--parser/feed/build.gradle4
-rw-r--r--parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/namespace/Itunes.java77
-rw-r--r--parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/namespace/Rss20.java19
-rw-r--r--parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/util/TypeGetter.java7
-rw-r--r--parser/feed/src/test/java/de/danoeh/antennapod/parser/feed/element/namespace/RssParserTest.java8
-rw-r--r--parser/feed/src/test/resources/feed-rss-testUnsupportedElements.xml14
-rw-r--r--parser/media/build.gradle2
-rw-r--r--parser/media/src/main/java/de/danoeh/antennapod/parser/media/vorbis/OggInputStream.java81
-rw-r--r--parser/media/src/main/java/de/danoeh/antennapod/parser/media/vorbis/VorbisCommentChapterReader.java5
-rw-r--r--parser/media/src/main/java/de/danoeh/antennapod/parser/media/vorbis/VorbisCommentReader.java64
-rw-r--r--playback/README.md3
-rw-r--r--playback/base/README.md3
-rw-r--r--playback/base/build.gradle10
-rw-r--r--playback/base/src/main/AndroidManifest.xml1
-rw-r--r--playback/base/src/main/java/de/danoeh/antennapod/playback/base/PlaybackServiceMediaPlayer.java (renamed from core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceMediaPlayer.java)40
-rw-r--r--playback/base/src/main/java/de/danoeh/antennapod/playback/base/PlayerStatus.java33
-rw-r--r--playback/base/src/main/java/de/danoeh/antennapod/playback/base/RewindAfterPauseUtils.java (renamed from core/src/main/java/de/danoeh/antennapod/core/util/RewindAfterPauseUtils.java)2
-rw-r--r--playback/base/src/test/java/de/danoeh/antennapod/playback/base/RewindAfterPauseUtilTest.java (renamed from core/src/test/java/de/danoeh/antennapod/core/util/RewindAfterPauseUtilTest.java)2
-rw-r--r--playback/cast/README.md3
-rw-r--r--playback/cast/build.gradle17
-rw-r--r--playback/cast/src/free/java/de/danoeh/antennapod/playback/cast/CastEnabledActivity.java (renamed from app/src/free/java/de/danoeh/antennapod/activity/CastEnabledActivity.java)2
-rw-r--r--playback/cast/src/free/java/de/danoeh/antennapod/playback/cast/CastPsmp.java17
-rw-r--r--playback/cast/src/free/java/de/danoeh/antennapod/playback/cast/CastStateListener.java15
-rw-r--r--playback/cast/src/main/AndroidManifest.xml1
-rw-r--r--playback/cast/src/play/java/de/danoeh/antennapod/playback/cast/CastEnabledActivity.java35
-rw-r--r--playback/cast/src/play/java/de/danoeh/antennapod/playback/cast/CastOptionsProvider.java26
-rw-r--r--playback/cast/src/play/java/de/danoeh/antennapod/playback/cast/CastPsmp.java (renamed from core/src/play/java/de/danoeh/antennapod/core/service/playback/RemotePSMP.java)359
-rw-r--r--playback/cast/src/play/java/de/danoeh/antennapod/playback/cast/CastStateListener.java69
-rw-r--r--playback/cast/src/play/java/de/danoeh/antennapod/playback/cast/CastUtils.java181
-rw-r--r--playback/cast/src/play/java/de/danoeh/antennapod/playback/cast/MediaInfoCreator.java135
-rw-r--r--playback/cast/src/play/res/menu/cast_button.xml11
-rw-r--r--settings.gradle4
-rw-r--r--ui/app-start-intent/src/main/java/de/danoeh/antennapod/ui/appstartintent/MainActivityStarter.java5
-rw-r--r--ui/app-start-intent/src/main/java/de/danoeh/antennapod/ui/appstartintent/VideoPlayerActivityStarter.java4
-rw-r--r--ui/common/src/main/java/de/danoeh/antennapod/ui/common/ThemeUtils.java4
-rw-r--r--ui/png-icons/src/main/res/drawable/ic_notification_cast_off.xml5
286 files changed, 5840 insertions, 6777 deletions
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
deleted file mode 100644
index f1d96dc7a..000000000
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ /dev/null
@@ -1,50 +0,0 @@
----
-name: Bug report
-about: Create a report to help us improve existing features
-labels: 'Type: Possible bug'
----
-
-# Checklist
-<!-- Place an x in the boxes to tick them: [x] -->
-
-- [ ] I have used the search function to see if someone else has already submitted the same bug report.
-- [ ] I will describe the problem with as much detail as possible.
-- [ ] If the bug only to occurs with a certain podcast, I will include the URL of that podcast.
-
-# System info
-<!-- The following information is very important to fill out because some bugs may only occur on certain devices or versions of Android. -->
-
-**App version**: x.y.z
-<!-- The latest version may be different depending on your device. You can find the version in AntennaPod's settings. -->
-
-**App source**: Google Play / F-Droid / ...
-<!-- Please delete irrelevant answer or fill in the blank -->
-
-**Android version**: 5.x (Please mention if you are using a custom rom!)
-
-**Device model**:
-
-# Bug description
-
-**Steps to reproduce**:
-1. This
-2. Then that
-3. Then this
-4. Etc.
-
-**Expected behaviour**:
-<!-- After following the steps, what did you think AntennaPod would do? -->
-
-**Current behaviour**:
-<!-- What did AntennaPod do instead? Screenshots might help. Usually, you can take a screenshot of your smartphone by pressing *Power* + *Volume down* for a few seconds. -->
-
-**First occurred**: (e.g. about x days/weeks ago)
-
-**Environment**:
-<!-- Settings you have changed (e.g. Auto Download, changed media player). "Unusual" devices you use (e.g. Bluetooth headphones). -->
-
-**Stacktrace/Logcat**:
-<!-- If you are experiencing a crash, including the stacktrace will likely get it fixed sooner. AntennaPod has an `export logs` feature for this. -->
-```
-[if available]
-```
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
new file mode 100644
index 000000000..96c33d973
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -0,0 +1,78 @@
+name: Bug report
+description: Create a report to help us improve existing features
+labels: ["Type: Possible bug"]
+body:
+ - type: markdown
+ attributes:
+ value: |
+ Thanks for taking the time to fill out this bug report!
+ - type: checkboxes
+ id: checklist
+ attributes:
+ label: Checklist
+ options:
+ - label: I have used the search function 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
+ - label: If the bug only to occurs with a certain podcast, I will include the URL of that podcast.
+ required: true
+ - type: input
+ id: version
+ attributes:
+ label: App version
+ description: The latest version is different on each device, so we need the actual version number found on the settings screen.
+ placeholder: x.y.z
+ validations:
+ required: true
+ - type: dropdown
+ id: source
+ attributes:
+ label: Where did you get the app from
+ multiple: false
+ options:
+ - Google Play
+ - F-Droid
+ - Other
+ validations:
+ required: true
+ - type: input
+ id: android_version
+ attributes:
+ label: Android version
+ description: Please mention if you are using a custom rom!
+ validations:
+ required: true
+ - type: input
+ id: device
+ attributes:
+ label: Device model
+ - type: input
+ id: first
+ attributes:
+ label: First occurred
+ placeholder: about x days/weeks ago
+ - type: textarea
+ id: steps
+ attributes:
+ label: Steps to reproduce
+ placeholder: |
+ 1. This
+ 2. Then that
+ 3. Then this
+ 4. Etc.
+ - type: textarea
+ id: expected
+ attributes:
+ label: Expected behaviour
+ description: After following the steps, what did you think AntennaPod would do?
+ - type: textarea
+ id: current
+ attributes:
+ label: Current behaviour
+ description: What did AntennaPod do instead? Screenshots might help. Usually, you can take a screenshot of your smartphone by pressing *Power* + *Volume down* for a few seconds.
+ - type: textarea
+ id: logs
+ attributes:
+ label: Logs
+ description: If you are experiencing a crash, including the stacktrace will likely get it fixed sooner. AntennaPod has an `export logs` feature for this.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
deleted file mode 100644
index 24f2f5772..000000000
--- a/.github/ISSUE_TEMPLATE/feature_request.md
+++ /dev/null
@@ -1,31 +0,0 @@
----
-name: Feature request
-about: Request a new feature or enhancement
-
----
-
-# Checklist
-<!-- Place an x in the boxes to tick them: [x] -->
-
-- [ ] I have used the search function to see if someone else has already submitted the same feature request.
-- [ ] I will only create one feature request per issue.
-- [ ] I will describe the problem with as much detail as possible.
-
-# System info
-
-**App version**: x.y.z
-<!-- The latest version may be different depending on your device. You can find the version in AntennaPod's settings. -->
-
-**App source**: Google Play / F-Droid / ...
-<!-- Please delete irrelevant answer or fill in the blank -->
-
-# Feature description
-
-**Problem you may be having, or feature you want**:
-<!-- Give a brief explanation about the problem that may currently exist -->
-
-**Suggested solution**:
-<!-- Describe 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. -->
-
-**Screenshots / Drawings / Technical details**:
-<!-- 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. -->
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml
new file mode 100644
index 000000000..58ac86f8b
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.yml
@@ -0,0 +1,48 @@
+name: Feature request
+description: Request a new feature or enhancement
+body:
+ - type: checkboxes
+ id: checklist
+ attributes:
+ label: Checklist
+ options:
+ - label: I have used the search function 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
+ - label: This request contains only one single feature, **not** a list of multiple (related) features.
+ required: true
+ - type: input
+ id: version
+ attributes:
+ label: App version
+ description: The latest version is different on each device, so we need the actual version number found on the settings screen.
+ placeholder: x.y.z
+ validations:
+ required: true
+ - type: dropdown
+ id: source
+ attributes:
+ label: Where did you get the app from
+ multiple: false
+ options:
+ - Google Play
+ - F-Droid
+ - Other
+ validations:
+ required: true
+ - type: textarea
+ id: problem
+ attributes:
+ label: Problem you may be having, or feature you want
+ description: Give a brief explanation about the problem that may currently exist
+ - type: textarea
+ id: solution
+ attributes:
+ label: Suggested solution
+ description: Describe 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.
+ - type: textarea
+ id: screenshots
+ attributes:
+ label: Screenshots / Drawings / Technical details
+ description: 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.
diff --git a/.github/workflows/android-emulator.yml b/.github/workflows/android-emulator.yml
index eed69911a..c8e66e14f 100644
--- a/.github/workflows/android-emulator.yml
+++ b/.github/workflows/android-emulator.yml
@@ -9,11 +9,11 @@ jobs:
runs-on: macOS-latest
steps:
- uses: actions/checkout@v2
- - name: Set up JDK 8
+ - name: Set up JDK 11
uses: actions/setup-java@v2
with:
distribution: 'adopt'
- java-version: '8'
+ java-version: '11'
- name: Wrapper validation
uses: gradle/wrapper-validation-action@v1
- name: Build with Gradle
diff --git a/app/build.gradle b/app/build.gradle
index 06d04bd49..e407390be 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -1,7 +1,7 @@
plugins {
id('com.android.application')
id('com.getkeepsafe.dexcount')
- id('com.github.triplet.play') version '3.4.0' apply false
+ id('com.github.triplet.play') version '3.6.0-agp4.2' apply false
}
apply from: "../common.gradle"
apply from: "../playFlavor.gradle"
@@ -76,9 +76,9 @@ android {
dexcount {
if (project.hasProperty("enableDexcountInDebug")) {
- runOnEachPackage enableDexcountInDebug.toBoolean()
+ runOnEachPackage = enableDexcountInDebug.toBoolean()
} else { // default to not running dexcount
- runOnEachPackage false
+ runOnEachPackage = false
}
}
}
@@ -111,10 +111,13 @@ android {
dependencies {
implementation project(":core")
+ implementation project(":event")
implementation project(':model')
implementation project(':net:sync:gpoddernet')
implementation project(':net:sync:model')
implementation project(':parser:feed')
+ implementation project(':playback:base')
+ implementation project(':playback:cast')
implementation project(':ui:app-start-intent')
implementation project(':ui:common')
@@ -122,11 +125,13 @@ dependencies {
implementation "androidx.appcompat:appcompat:$appcompatVersion"
implementation 'androidx.coordinatorlayout:coordinatorlayout:1.1.0'
implementation "androidx.core:core:$coreVersion"
+ implementation "androidx.fragment:fragment:$fragmentVersion"
implementation 'androidx.gridlayout:gridlayout:1.0.0'
implementation "androidx.media:media:$mediaVersion"
+ implementation "androidx.palette:palette:$paletteVersion"
implementation "androidx.preference:preference:$preferenceVersion"
- implementation 'androidx.recyclerview:recyclerview:1.1.0'
- implementation 'androidx.viewpager2:viewpager2:1.1.0-alpha01'
+ implementation "androidx.recyclerview:recyclerview:$recyclerViewVersion"
+ implementation "androidx.viewpager2:viewpager2:$viewPager2Version"
implementation "androidx.work:work-runtime:$workManagerVersion"
implementation "com.google.android.material:material:$googleMaterialVersion"
diff --git a/app/src/androidTest/java/de/test/antennapod/playback/PlaybackTest.java b/app/src/androidTest/java/de/test/antennapod/playback/PlaybackTest.java
index 9f7af3a16..2ab2361d7 100644
--- a/app/src/androidTest/java/de/test/antennapod/playback/PlaybackTest.java
+++ b/app/src/androidTest/java/de/test/antennapod/playback/PlaybackTest.java
@@ -11,6 +11,7 @@ import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.rule.ActivityTestRule;
import de.danoeh.antennapod.model.feed.FeedItemFilter;
+import de.danoeh.antennapod.playback.base.PlayerStatus;
import org.awaitility.Awaitility;
import org.hamcrest.Matcher;
import org.junit.After;
@@ -32,7 +33,6 @@ 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.core.service.playback.PlaybackService;
-import de.danoeh.antennapod.core.service.playback.PlayerStatus;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.storage.DBWriter;
import de.danoeh.antennapod.core.util.IntentUtils;
diff --git a/app/src/androidTest/java/de/test/antennapod/service/playback/CancelablePSMPCallback.java b/app/src/androidTest/java/de/test/antennapod/service/playback/CancelablePSMPCallback.java
index ae38fd5e7..4d57b9b43 100644
--- a/app/src/androidTest/java/de/test/antennapod/service/playback/CancelablePSMPCallback.java
+++ b/app/src/androidTest/java/de/test/antennapod/service/playback/CancelablePSMPCallback.java
@@ -1,9 +1,10 @@
package de.test.antennapod.service.playback;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import de.danoeh.antennapod.model.playback.MediaType;
-import de.danoeh.antennapod.core.service.playback.PlaybackServiceMediaPlayer;
import de.danoeh.antennapod.model.playback.Playable;
+import de.danoeh.antennapod.playback.base.PlaybackServiceMediaPlayer;
public class CancelablePSMPCallback implements PlaybackServiceMediaPlayer.PSMPCallback {
@@ -35,22 +36,6 @@ public class CancelablePSMPCallback implements PlaybackServiceMediaPlayer.PSMPCa
}
@Override
- public void playbackSpeedChanged(float s) {
- if (isCancelled) {
- return;
- }
- originalCallback.playbackSpeedChanged(s);
- }
-
- @Override
- public void onBufferingUpdate(int percent) {
- if (isCancelled) {
- return;
- }
- originalCallback.onBufferingUpdate(percent);
- }
-
- @Override
public void onMediaChanged(boolean reloadUI) {
if (isCancelled) {
return;
@@ -59,22 +44,6 @@ public class CancelablePSMPCallback implements PlaybackServiceMediaPlayer.PSMPCa
}
@Override
- public boolean onMediaPlayerInfo(int code, int resourceId) {
- if (isCancelled) {
- return true;
- }
- return originalCallback.onMediaPlayerInfo(code, resourceId);
- }
-
- @Override
- public boolean onMediaPlayerError(Object inObj, int what, int extra) {
- if (isCancelled) {
- return true;
- }
- return originalCallback.onMediaPlayerError(inObj, what, extra);
- }
-
- @Override
public void onPostPlayback(@NonNull Playable media, boolean ended, boolean skipped, boolean playingNext) {
if (isCancelled) {
return;
@@ -106,6 +75,15 @@ public class CancelablePSMPCallback implements PlaybackServiceMediaPlayer.PSMPCa
return originalCallback.getNextInQueue(currentMedia);
}
+ @Nullable
+ @Override
+ public Playable findMedia(@NonNull String url) {
+ if (isCancelled) {
+ return null;
+ }
+ return originalCallback.findMedia(url);
+ }
+
@Override
public void onPlaybackEnded(MediaType mediaType, boolean stopPlaying) {
if (isCancelled) {
@@ -113,4 +91,12 @@ public class CancelablePSMPCallback implements PlaybackServiceMediaPlayer.PSMPCa
}
originalCallback.onPlaybackEnded(mediaType, stopPlaying);
}
+
+ @Override
+ public void ensureMediaInfoLoaded(@NonNull Playable media) {
+ if (isCancelled) {
+ return;
+ }
+ originalCallback.ensureMediaInfoLoaded(media);
+ }
} \ No newline at end of file
diff --git a/app/src/androidTest/java/de/test/antennapod/service/playback/DefaultPSMPCallback.java b/app/src/androidTest/java/de/test/antennapod/service/playback/DefaultPSMPCallback.java
index 29a854f20..fb55c7ad0 100644
--- a/app/src/androidTest/java/de/test/antennapod/service/playback/DefaultPSMPCallback.java
+++ b/app/src/androidTest/java/de/test/antennapod/service/playback/DefaultPSMPCallback.java
@@ -1,69 +1,59 @@
package de.test.antennapod.service.playback;
import androidx.annotation.NonNull;
-import androidx.annotation.StringRes;
+import androidx.annotation.Nullable;
import de.danoeh.antennapod.model.playback.MediaType;
-import de.danoeh.antennapod.core.service.playback.PlaybackServiceMediaPlayer;
import de.danoeh.antennapod.model.playback.Playable;
+import de.danoeh.antennapod.playback.base.PlaybackServiceMediaPlayer;
public class DefaultPSMPCallback implements PlaybackServiceMediaPlayer.PSMPCallback {
- @Override
- public void statusChanged(PlaybackServiceMediaPlayer.PSMPInfo newInfo) {
+ @Override
+ public void statusChanged(PlaybackServiceMediaPlayer.PSMPInfo newInfo) {
- }
+ }
- @Override
- public void shouldStop() {
+ @Override
+ public void shouldStop() {
- }
+ }
- @Override
- public void playbackSpeedChanged(float s) {
+ @Override
+ public void onMediaChanged(boolean reloadUI) {
- }
+ }
- @Override
- public void onBufferingUpdate(int percent) {
+ @Override
+ public void onPostPlayback(@NonNull Playable media, boolean ended, boolean skipped, boolean playingNext) {
- }
+ }
- @Override
- public void onMediaChanged(boolean reloadUI) {
+ @Override
+ public void onPlaybackStart(@NonNull Playable playable, int position) {
- }
+ }
- @Override
- public boolean onMediaPlayerInfo(int code, @StringRes int resourceId) {
- return false;
- }
+ @Override
+ public void onPlaybackPause(Playable playable, int position) {
- @Override
- public boolean onMediaPlayerError(Object inObj, int what, int extra) {
- return false;
- }
+ }
- @Override
- public void onPostPlayback(@NonNull Playable media, boolean ended, boolean skipped, boolean playingNext) {
+ @Override
+ public Playable getNextInQueue(Playable currentMedia) {
+ return null;
+ }
- }
+ @Nullable
+ @Override
+ public Playable findMedia(@NonNull String url) {
+ return null;
+ }
- @Override
- public void onPlaybackStart(@NonNull Playable playable, int position) {
+ @Override
+ public void onPlaybackEnded(MediaType mediaType, boolean stopPlaying) {
- }
+ }
- @Override
- public void onPlaybackPause(Playable playable, int position) {
-
- }
-
- @Override
- public Playable getNextInQueue(Playable currentMedia) {
- return null;
- }
-
- @Override
- public void onPlaybackEnded(MediaType mediaType, boolean stopPlaying) {
-
- }
- } \ No newline at end of file
+ @Override
+ public void ensureMediaInfoLoaded(@NonNull Playable media) {
+ }
+} \ No newline at end of file
diff --git a/app/src/androidTest/java/de/test/antennapod/service/playback/PlaybackServiceMediaPlayerTest.java b/app/src/androidTest/java/de/test/antennapod/service/playback/PlaybackServiceMediaPlayerTest.java
index dfb0e3e36..0d05c7624 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
@@ -2,9 +2,12 @@ package de.test.antennapod.service.playback;
import android.content.Context;
+import androidx.test.annotation.UiThreadTest;
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.test.antennapod.EspressoTestUtils;
import junit.framework.AssertionFailedError;
@@ -24,8 +27,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.service.playback.PlaybackServiceMediaPlayer;
-import de.danoeh.antennapod.core.service.playback.PlayerStatus;
import de.danoeh.antennapod.core.storage.PodDBAdapter;
import de.danoeh.antennapod.model.playback.Playable;
import de.test.antennapod.util.service.download.HTTPBin;
@@ -56,12 +57,14 @@ public class PlaybackServiceMediaPlayerTest {
private volatile AssertionFailedError assertionError;
@After
+ @UiThreadTest
public void tearDown() throws Exception {
PodDBAdapter.deleteDatabase();
httpServer.stop();
}
@Before
+ @UiThreadTest
public void setUp() throws Exception {
assertionError = null;
EspressoTestUtils.clearPreferences();
@@ -117,6 +120,7 @@ public class PlaybackServiceMediaPlayerTest {
}
@Test
+ @UiThreadTest
public void testInit() {
final Context c = getInstrumentation().getTargetContext();
PlaybackServiceMediaPlayer psmp = new LocalPSMP(c, new DefaultPSMPCallback());
@@ -141,6 +145,7 @@ public class PlaybackServiceMediaPlayerTest {
}
@Test
+ @UiThreadTest
public void testPlayMediaObjectStreamNoStartNoPrepare() throws InterruptedException {
final Context c = getInstrumentation().getTargetContext();
final CountDownLatch countDownLatch = new CountDownLatch(2);
@@ -181,6 +186,7 @@ public class PlaybackServiceMediaPlayerTest {
}
@Test
+ @UiThreadTest
public void testPlayMediaObjectStreamStartNoPrepare() throws InterruptedException {
final Context c = getInstrumentation().getTargetContext();
final CountDownLatch countDownLatch = new CountDownLatch(2);
@@ -222,6 +228,7 @@ public class PlaybackServiceMediaPlayerTest {
}
@Test
+ @UiThreadTest
public void testPlayMediaObjectStreamNoStartPrepare() throws InterruptedException {
final Context c = getInstrumentation().getTargetContext();
final CountDownLatch countDownLatch = new CountDownLatch(4);
@@ -264,6 +271,7 @@ public class PlaybackServiceMediaPlayerTest {
}
@Test
+ @UiThreadTest
public void testPlayMediaObjectStreamStartPrepare() throws InterruptedException {
final Context c = getInstrumentation().getTargetContext();
final CountDownLatch countDownLatch = new CountDownLatch(5);
@@ -308,6 +316,7 @@ public class PlaybackServiceMediaPlayerTest {
}
@Test
+ @UiThreadTest
public void testPlayMediaObjectLocalNoStartNoPrepare() throws InterruptedException {
final Context c = getInstrumentation().getTargetContext();
final CountDownLatch countDownLatch = new CountDownLatch(2);
@@ -347,6 +356,7 @@ public class PlaybackServiceMediaPlayerTest {
}
@Test
+ @UiThreadTest
public void testPlayMediaObjectLocalStartNoPrepare() throws InterruptedException {
final Context c = getInstrumentation().getTargetContext();
final CountDownLatch countDownLatch = new CountDownLatch(2);
@@ -386,6 +396,7 @@ public class PlaybackServiceMediaPlayerTest {
}
@Test
+ @UiThreadTest
public void testPlayMediaObjectLocalNoStartPrepare() throws InterruptedException {
final Context c = getInstrumentation().getTargetContext();
final CountDownLatch countDownLatch = new CountDownLatch(4);
@@ -427,6 +438,7 @@ public class PlaybackServiceMediaPlayerTest {
}
@Test
+ @UiThreadTest
public void testPlayMediaObjectLocalStartPrepare() throws InterruptedException {
final Context c = getInstrumentation().getTargetContext();
final CountDownLatch countDownLatch = new CountDownLatch(5);
@@ -514,13 +526,6 @@ public class PlaybackServiceMediaPlayerTest {
if (assertionError == null)
assertionError = new AssertionFailedError("Unexpected call to shouldStop");
}
-
- @Override
- public boolean onMediaPlayerError(Object inObj, int what, int extra) {
- if (assertionError == null)
- assertionError = new AssertionFailedError("Unexpected call to onMediaPlayerError");
- return false;
- }
});
PlaybackServiceMediaPlayer psmp = new LocalPSMP(c, callback);
Playable p = writeTestPlayable(playableFileUrl, PLAYABLE_LOCAL_URL);
@@ -537,46 +542,55 @@ public class PlaybackServiceMediaPlayerTest {
}
@Test
+ @UiThreadTest
public void testPauseDefaultState() throws InterruptedException {
pauseTestSkeleton(PlayerStatus.STOPPED, false, false, false, 1);
}
@Test
+ @UiThreadTest
public void testPausePlayingStateNoAbandonNoReinitNoStream() throws InterruptedException {
pauseTestSkeleton(PlayerStatus.PLAYING, false, false, false, LATCH_TIMEOUT_SECONDS);
}
@Test
+ @UiThreadTest
public void testPausePlayingStateNoAbandonNoReinitStream() throws InterruptedException {
pauseTestSkeleton(PlayerStatus.PLAYING, true, false, false, LATCH_TIMEOUT_SECONDS);
}
@Test
+ @UiThreadTest
public void testPausePlayingStateAbandonNoReinitNoStream() throws InterruptedException {
pauseTestSkeleton(PlayerStatus.PLAYING, false, true, false, LATCH_TIMEOUT_SECONDS);
}
@Test
+ @UiThreadTest
public void testPausePlayingStateAbandonNoReinitStream() throws InterruptedException {
pauseTestSkeleton(PlayerStatus.PLAYING, true, true, false, LATCH_TIMEOUT_SECONDS);
}
@Test
+ @UiThreadTest
public void testPausePlayingStateNoAbandonReinitNoStream() throws InterruptedException {
pauseTestSkeleton(PlayerStatus.PLAYING, false, false, true, LATCH_TIMEOUT_SECONDS);
}
@Test
+ @UiThreadTest
public void testPausePlayingStateNoAbandonReinitStream() throws InterruptedException {
pauseTestSkeleton(PlayerStatus.PLAYING, true, false, true, LATCH_TIMEOUT_SECONDS);
}
@Test
+ @UiThreadTest
public void testPausePlayingStateAbandonReinitNoStream() throws InterruptedException {
pauseTestSkeleton(PlayerStatus.PLAYING, false, true, true, LATCH_TIMEOUT_SECONDS);
}
@Test
+ @UiThreadTest
public void testPausePlayingStateAbandonReinitStream() throws InterruptedException {
pauseTestSkeleton(PlayerStatus.PLAYING, true, true, true, LATCH_TIMEOUT_SECONDS);
}
@@ -604,14 +618,6 @@ public class PlaybackServiceMediaPlayerTest {
}
}
-
- @Override
- public boolean onMediaPlayerError(Object inObj, int what, int extra) {
- if (assertionError == null) {
- assertionError = new AssertionFailedError("Unexpected call of onMediaPlayerError");
- }
- return false;
- }
});
PlaybackServiceMediaPlayer psmp = new LocalPSMP(c, callback);
if (initialState == PlayerStatus.PREPARED || initialState == PlayerStatus.PLAYING || initialState == PlayerStatus.PAUSED) {
@@ -631,16 +637,19 @@ public class PlaybackServiceMediaPlayerTest {
}
@Test
+ @UiThreadTest
public void testResumePausedState() throws InterruptedException {
resumeTestSkeleton(PlayerStatus.PAUSED, LATCH_TIMEOUT_SECONDS);
}
@Test
+ @UiThreadTest
public void testResumePreparedState() throws InterruptedException {
resumeTestSkeleton(PlayerStatus.PREPARED, LATCH_TIMEOUT_SECONDS);
}
@Test
+ @UiThreadTest
public void testResumePlayingState() throws InterruptedException {
resumeTestSkeleton(PlayerStatus.PLAYING, 1);
}
@@ -664,13 +673,6 @@ public class PlaybackServiceMediaPlayerTest {
}
}
}
-
- @Override
- public boolean onMediaPlayerError(Object inObj, int what, int extra) {
- if (assertionError == null)
- assertionError = new AssertionFailedError("Unexpected call to onMediaPlayerError");
- return false;
- }
});
PlaybackServiceMediaPlayer psmp = new LocalPSMP(c, callback);
Playable p = writeTestPlayable(playableFileUrl, PLAYABLE_LOCAL_URL);
@@ -700,21 +702,25 @@ public class PlaybackServiceMediaPlayerTest {
}
@Test
+ @UiThreadTest
public void testPrepareInitializedState() throws InterruptedException {
prepareTestSkeleton(PlayerStatus.INITIALIZED, LATCH_TIMEOUT_SECONDS);
}
@Test
+ @UiThreadTest
public void testPreparePlayingState() throws InterruptedException {
prepareTestSkeleton(PlayerStatus.PLAYING, 1);
}
@Test
+ @UiThreadTest
public void testPreparePausedState() throws InterruptedException {
prepareTestSkeleton(PlayerStatus.PAUSED, 1);
}
@Test
+ @UiThreadTest
public void testPreparePreparedState() throws InterruptedException {
prepareTestSkeleton(PlayerStatus.PREPARED, 1);
}
@@ -738,13 +744,6 @@ public class PlaybackServiceMediaPlayerTest {
}
}
}
-
- @Override
- public boolean onMediaPlayerError(Object inObj, int what, int extra) {
- if (assertionError == null)
- assertionError = new AssertionFailedError("Unexpected call to onMediaPlayerError");
- return false;
- }
});
PlaybackServiceMediaPlayer psmp = new LocalPSMP(c, callback);
Playable p = writeTestPlayable(playableFileUrl, PLAYABLE_LOCAL_URL);
@@ -764,21 +763,25 @@ public class PlaybackServiceMediaPlayerTest {
}
@Test
+ @UiThreadTest
public void testReinitPlayingState() throws InterruptedException {
reinitTestSkeleton(PlayerStatus.PLAYING, LATCH_TIMEOUT_SECONDS);
}
@Test
+ @UiThreadTest
public void testReinitPausedState() throws InterruptedException {
reinitTestSkeleton(PlayerStatus.PAUSED, LATCH_TIMEOUT_SECONDS);
}
@Test
+ @UiThreadTest
public void testPreparedPlayingState() throws InterruptedException {
reinitTestSkeleton(PlayerStatus.PREPARED, LATCH_TIMEOUT_SECONDS);
}
@Test
+ @UiThreadTest
public void testReinitInitializedState() throws InterruptedException {
reinitTestSkeleton(PlayerStatus.INITIALIZED, LATCH_TIMEOUT_SECONDS);
}
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 7803144e1..013d4db50 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
@@ -5,10 +5,12 @@ import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.annotation.UiThreadTest;
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 org.awaitility.Awaitility;
import org.greenrobot.eventbus.EventBus;
+import org.greenrobot.eventbus.Subscribe;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@@ -19,7 +21,7 @@ import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
-import de.danoeh.antennapod.core.event.QueueEvent;
+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;
@@ -144,7 +146,7 @@ public class PlaybackServiceTaskManagerTest {
FeedItem item = DBReader.getFeedItem(testItem.getId());
item.getMedia().setDownloaded(true);
item.getMedia().setFile_url("file://123");
- item.setAutoDownload(false);
+ item.disableAutoDownload();
DBWriter.setFeedMedia(item.getMedia()).get();
DBWriter.setFeedItem(item).get();
@@ -173,21 +175,6 @@ public class PlaybackServiceTaskManagerTest {
}
@Override
- public void onSleepTimerAlmostExpired(long timeLeft) {
-
- }
-
- @Override
- public void onSleepTimerExpired() {
-
- }
-
- @Override
- public void onSleepTimerReset() {
-
- }
-
- @Override
public WidgetUpdater.WidgetState requestWidgetState() {
return null;
}
@@ -234,21 +221,6 @@ public class PlaybackServiceTaskManagerTest {
}
@Override
- public void onSleepTimerAlmostExpired(long timeLeft) {
-
- }
-
- @Override
- public void onSleepTimerExpired() {
-
- }
-
- @Override
- public void onSleepTimerReset() {
-
- }
-
- @Override
public WidgetUpdater.WidgetState requestWidgetState() {
countDownLatch.countDown();
return null;
@@ -325,42 +297,20 @@ public class PlaybackServiceTaskManagerTest {
final long TIME = 2000;
final long TIMEOUT = 2 * TIME;
final CountDownLatch countDownLatch = new CountDownLatch(1);
- PlaybackServiceTaskManager pstm = new PlaybackServiceTaskManager(c, new PlaybackServiceTaskManager.PSTMCallback() {
- @Override
- public void positionSaverTick() {
-
- }
-
- @Override
- public void onSleepTimerAlmostExpired(long timeLeft) {
-
- }
-
- @Override
- public void onSleepTimerExpired() {
+ Object timerReceiver = new Object() {
+ @Subscribe
+ public void sleepTimerUpdate(SleepTimerUpdatedEvent event) {
if (countDownLatch.getCount() == 0) {
fail();
}
countDownLatch.countDown();
}
-
- @Override
- public void onSleepTimerReset() {
-
- }
-
- @Override
- public WidgetUpdater.WidgetState requestWidgetState() {
- return null;
- }
-
- @Override
- public void onChapterLoaded(Playable media) {
-
- }
- });
+ };
+ EventBus.getDefault().register(timerReceiver);
+ PlaybackServiceTaskManager pstm = new PlaybackServiceTaskManager(c, defaultPSTM);
pstm.setSleepTimer(TIME);
countDownLatch.await(TIMEOUT, TimeUnit.MILLISECONDS);
+ EventBus.getDefault().unregister(timerReceiver);
pstm.shutdown();
}
@@ -368,44 +318,26 @@ public class PlaybackServiceTaskManagerTest {
@UiThreadTest
public void testDisableSleepTimer() throws InterruptedException {
final Context c = InstrumentationRegistry.getInstrumentation().getTargetContext();
- final long TIME = 1000;
+ final long TIME = 5000;
final long TIMEOUT = 2 * TIME;
final CountDownLatch countDownLatch = new CountDownLatch(1);
- PlaybackServiceTaskManager pstm = new PlaybackServiceTaskManager(c, new PlaybackServiceTaskManager.PSTMCallback() {
- @Override
- public void positionSaverTick() {
-
- }
-
- @Override
- public void onSleepTimerAlmostExpired(long timeLeft) {
-
- }
-
- @Override
- public void onSleepTimerExpired() {
- fail("Sleeptimer expired");
- }
-
- @Override
- public void onSleepTimerReset() {
-
- }
-
- @Override
- public WidgetUpdater.WidgetState requestWidgetState() {
- return null;
- }
-
- @Override
- public void onChapterLoaded(Playable media) {
-
+ Object timerReceiver = new Object() {
+ @Subscribe
+ public void sleepTimerUpdate(SleepTimerUpdatedEvent event) {
+ if (event.isOver()) {
+ countDownLatch.countDown();
+ } else if (event.getTimeLeft() == 1) {
+ fail("Arrived at 1 but should have been cancelled");
+ }
}
- });
+ };
+ PlaybackServiceTaskManager pstm = new PlaybackServiceTaskManager(c, defaultPSTM);
+ EventBus.getDefault().register(timerReceiver);
pstm.setSleepTimer(TIME);
pstm.disableSleepTimer();
assertFalse(countDownLatch.await(TIMEOUT, TimeUnit.MILLISECONDS));
pstm.shutdown();
+ EventBus.getDefault().unregister(timerReceiver);
}
@Test
@@ -436,21 +368,6 @@ public class PlaybackServiceTaskManagerTest {
}
@Override
- public void onSleepTimerAlmostExpired(long timeLeft) {
-
- }
-
- @Override
- public void onSleepTimerExpired() {
-
- }
-
- @Override
- public void onSleepTimerReset() {
-
- }
-
- @Override
public WidgetUpdater.WidgetState requestWidgetState() {
return null;
}
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 c71bff357..74414240f 100644
--- a/app/src/androidTest/java/de/test/antennapod/ui/PreferencesTest.java
+++ b/app/src/androidTest/java/de/test/antennapod/ui/PreferencesTest.java
@@ -126,8 +126,9 @@ public class PreferencesTest {
clickPreference(R.string.user_interface_label);
String[] buttons = res.getStringArray(R.array.compact_notification_buttons_options);
clickPreference(R.string.pref_compact_notification_buttons_title);
- // First uncheck checkbox
- onView(withText(buttons[2])).perform(click());
+ // First uncheck checkboxes
+ onView(withText(buttons[0])).perform(click());
+ onView(withText(buttons[1])).perform(click());
// Now try to check all checkboxes
onView(withText(buttons[0])).perform(click());
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 b25f957d3..eedb2d9de 100644
--- a/app/src/androidTest/java/de/test/antennapod/ui/UITestUtils.java
+++ b/app/src/androidTest/java/de/test/antennapod/ui/UITestUtils.java
@@ -2,8 +2,8 @@ package de.test.antennapod.ui;
import android.content.Context;
import android.util.Log;
-import de.danoeh.antennapod.core.event.FeedListUpdateEvent;
-import de.danoeh.antennapod.core.event.QueueEvent;
+import de.danoeh.antennapod.event.FeedListUpdateEvent;
+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;
diff --git a/app/src/androidTest/java/de/test/antennapod/util/event/FeedItemEventListener.java b/app/src/androidTest/java/de/test/antennapod/util/event/FeedItemEventListener.java
index 601bba853..7e8fc1205 100644
--- a/app/src/androidTest/java/de/test/antennapod/util/event/FeedItemEventListener.java
+++ b/app/src/androidTest/java/de/test/antennapod/util/event/FeedItemEventListener.java
@@ -8,7 +8,7 @@ import org.greenrobot.eventbus.Subscribe;
import java.util.ArrayList;
import java.util.List;
-import de.danoeh.antennapod.core.event.FeedItemEvent;
+import de.danoeh.antennapod.event.FeedItemEvent;
import io.reactivex.functions.Consumer;
/**
diff --git a/app/src/free/java/de/danoeh/antennapod/config/CastCallbackImpl.java b/app/src/free/java/de/danoeh/antennapod/config/CastCallbackImpl.java
deleted file mode 100644
index fb23dfa1a..000000000
--- a/app/src/free/java/de/danoeh/antennapod/config/CastCallbackImpl.java
+++ /dev/null
@@ -1,7 +0,0 @@
-package de.danoeh.antennapod.config;
-
-import de.danoeh.antennapod.core.CastCallbacks;
-
-class CastCallbackImpl implements CastCallbacks {
-
-}
diff --git a/app/src/free/java/de/danoeh/antennapod/preferences/PreferenceControllerFlavorHelper.java b/app/src/free/java/de/danoeh/antennapod/preferences/PreferenceControllerFlavorHelper.java
deleted file mode 100644
index e096f883f..000000000
--- a/app/src/free/java/de/danoeh/antennapod/preferences/PreferenceControllerFlavorHelper.java
+++ /dev/null
@@ -1,14 +0,0 @@
-package de.danoeh.antennapod.preferences;
-
-import de.danoeh.antennapod.core.preferences.UserPreferences;
-import de.danoeh.antennapod.fragment.preferences.PlaybackPreferencesFragment;
-
-/**
- * Implements functions from PreferenceController that are flavor dependent.
- */
-public class PreferenceControllerFlavorHelper {
-
- public static void setupFlavoredUI(PlaybackPreferencesFragment ui) {
- ui.findPreference(UserPreferences.PREF_CAST_ENABLED).setEnabled(false);
- }
-}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 47648f9d3..0f8242e63 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -40,7 +40,8 @@
android:supportsRtl="true"
android:logo="@mipmap/ic_launcher"
android:resizeableActivity="true"
- android:allowAudioPlaybackCapture="true">
+ android:allowAudioPlaybackCapture="true"
+ android:networkSecurityConfig="@xml/network_security_config">
<meta-data android:name="android.webkit.WebView.MetricsOptOut"
@@ -53,6 +54,9 @@
<meta-data
android:name="com.google.android.backup.api_key"
android:value="AEdPqrEAAAAI3a05VToCTlqBymJrbFGaKQMvF-bBAuLsOdavBA"/>
+ <meta-data
+ android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
+ android:value="de.danoeh.antennapod.playback.cast.CastOptionsProvider" />
<!-- Version < 3.0. DeX Mode and Screen Mirroring support -->
<meta-data android:name="com.samsung.android.keepalive.density" android:value="true"/>
@@ -65,15 +69,13 @@
android:configChanges="keyboardHidden|orientation|screenSize"
android:exported="true">
<intent-filter>
- <action android:name="android.intent.action.MAIN"/>
- <category android:name="android.intent.category.LAUNCHER"/>
- </intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
+ <action android:name="android.intent.action.MUSIC_PLAYER" />
- <intent-filter>
- <action android:name=
- "android.media.action.MEDIA_PLAY_FROM_SEARCH" />
- <category android:name=
- "android.intent.category.DEFAULT" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ <category android:name="android.intent.category.APP_MUSIC" />
</intent-filter>
<meta-data
@@ -98,13 +100,6 @@
android:host="antennapod.org"
android:pathPrefix="/deeplink/main"
android:scheme="https" />
- </intent-filter>
- <intent-filter>
- <action android:name="android.intent.action.VIEW" />
-
- <category android:name="android.intent.category.DEFAULT" />
- <category android:name="android.intent.category.BROWSABLE" />
-
<data
android:host="antennapod.org"
android:pathPrefix="/deeplink/search"
@@ -144,11 +139,7 @@
android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
- </intent-filter>
- <intent-filter>
<action android:name="de.danoeh.antennapod.FORCE_WIDGET_UPDATE"/>
- </intent-filter>
- <intent-filter>
<action android:name="de.danoeh.antennapod.STOP_WIDGET_UPDATE"/>
</intent-filter>
<meta-data
@@ -165,6 +156,7 @@
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
+ <action android:name="android.intent.action.SEND"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
@@ -172,27 +164,15 @@
<data android:mimeType="text/xml"/>
<data android:mimeType="text/x-opml"/>
<data android:mimeType="application/xml"/>
- <data android:mimeType="application/octet-stream"/>
<data android:scheme="file"/>
<data android:scheme="content"/>
-
- <data android:host="*"/>
- </intent-filter>
- <intent-filter>
- <action android:name="android.intent.action.SEND"/>
-
- <category android:name="android.intent.category.DEFAULT"/>
- <category android:name="android.intent.category.BROWSABLE"/>
-
- <data android:mimeType="text/xml"/>
- <data android:mimeType="text/plain"/>
- <data android:mimeType="text/x-opml"/>
- <data android:mimeType="application/xml"/>
- <data android:mimeType="application/octet-stream"/>
-
<data android:scheme="http"/>
<data android:scheme="https"/>
+
+ <data android:host="*"/>
+ <data android:pathPattern=".*.xml" />
+ <data android:pathPattern=".*.opml" />
</intent-filter>
</activity>
<activity
@@ -315,6 +295,18 @@
</intent-filter>
<intent-filter>
+ <action android:name="android.intent.action.VIEW" />
+
+ <category android:name="android.intent.category.DEFAULT" />
+ <category android:name="android.intent.category.BROWSABLE" />
+
+ <data android:pathPattern="/.*/podcast/.*" />
+ <data android:host="podcasts.apple.com" />
+ <data android:scheme="http" />
+ <data android:scheme="https" />
+ </intent-filter>
+
+ <intent-filter>
<action android:name="android.intent.action.SEND"/>
<category android:name="android.intent.category.DEFAULT"/>
@@ -324,6 +316,16 @@
</activity>
+ <activity android:name=".activity.SelectSubscriptionActivity"
+ android:label="@string/shortcut_subscription_label"
+ android:icon="@drawable/ic_folder_shortcut"
+ android:theme="@style/Theme.AntennaPod.Dark.Translucent"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.intent.action.CREATE_SHORTCUT" />
+ </intent-filter>
+ </activity>
+
<receiver
android:name=".receiver.ConnectivityActionReceiver"
android:exported="true">
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 aa59e4e96..f7c96a93a 100644
--- a/app/src/main/java/de/danoeh/antennapod/activity/BugReportActivity.java
+++ b/app/src/main/java/de/danoeh/antennapod/activity/BugReportActivity.java
@@ -7,7 +7,6 @@ import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.net.Uri;
-import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import com.google.android.material.snackbar.Snackbar;
@@ -103,22 +102,21 @@ public class BugReportActivity extends AppCompatActivity {
Runtime.getRuntime().exec(cmd);
//share file
try {
- Intent i = new Intent(Intent.ACTION_SEND);
- i.setType("text/*");
+ 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);
- i.putExtra(Intent.EXTRA_STREAM, fileUri);
- i.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
- if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) {
- PackageManager pm = getPackageManager();
- List<ResolveInfo> resInfos = pm.queryIntentActivities(i, PackageManager.MATCH_DEFAULT_ONLY);
- for (ResolveInfo resolveInfo : resInfos) {
- String packageName = resolveInfo.activityInfo.packageName;
- grantUriPermission(packageName, fileUri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
- }
- }
+ 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);
- startActivity(Intent.createChooser(i, chooserTitle));
+ 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);
} 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 f07ad6ad5..3e4782f1f 100644
--- a/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java
+++ b/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java
@@ -27,7 +27,6 @@ import androidx.appcompat.app.ActionBarDrawerToggle;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.Toolbar;
import androidx.core.content.ContextCompat;
-import androidx.core.view.ViewCompat;
import androidx.drawerlayout.widget.DrawerLayout;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
@@ -38,6 +37,7 @@ import com.bumptech.glide.Glide;
import com.google.android.material.bottomsheet.BottomSheetBehavior;
import com.google.android.material.snackbar.Snackbar;
+import de.danoeh.antennapod.playback.cast.CastEnabledActivity;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.Validate;
import org.greenrobot.eventbus.EventBus;
@@ -45,7 +45,7 @@ import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import de.danoeh.antennapod.R;
-import de.danoeh.antennapod.core.event.MessageEvent;
+import de.danoeh.antennapod.event.MessageEvent;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.receiver.MediaButtonReceiver;
import de.danoeh.antennapod.core.service.playback.PlaybackService;
@@ -152,14 +152,14 @@ public class MainActivity extends CastEnabledActivity {
}
/**
- * ViewCompat.generateViewId stores the current ID in a static variable.
+ * View.generateViewId stores the current ID in a static variable.
* When the process is killed, the variable gets reset.
* This makes sure that we do not get ID collisions
* and therefore errors when trying to restore state from another view.
*/
@SuppressWarnings("StatementWithEmptyBody")
private void ensureGeneratedViewIdGreaterThan(int minimum) {
- while (ViewCompat.generateViewId() <= minimum) {
+ while (View.generateViewId() <= minimum) {
// Generate new IDs
}
}
@@ -167,7 +167,7 @@ public class MainActivity extends CastEnabledActivity {
@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
- outState.putInt(KEY_GENERATED_VIEW_ID, ViewCompat.generateViewId());
+ outState.putInt(KEY_GENERATED_VIEW_ID, View.generateViewId());
}
private final BottomSheetBehavior.BottomSheetCallback bottomSheetCallback =
@@ -624,6 +624,7 @@ public class MainActivity extends CastEnabledActivity {
AudioManager audioManager = (AudioManager) getSystemService(AUDIO_SERVICE);
Integer customKeyCode = null;
+ EventBus.getDefault().post(event);
switch (keyCode) {
case KeyEvent.KEYCODE_P:
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 4dca1fda7..9148a9949 100644
--- a/app/src/main/java/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java
+++ b/app/src/main/java/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java
@@ -6,7 +6,6 @@ import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.LightingColorFilter;
-import android.os.Build;
import android.os.Bundle;
import android.text.Spannable;
import android.text.SpannableString;
@@ -19,6 +18,7 @@ import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.TextView;
+import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
@@ -31,8 +31,8 @@ import de.danoeh.antennapod.R;
import de.danoeh.antennapod.adapter.FeedItemlistDescriptionAdapter;
import de.danoeh.antennapod.core.dialog.DownloadRequestErrorDialogCreator;
import de.danoeh.antennapod.core.event.DownloadEvent;
-import de.danoeh.antennapod.core.event.FeedListUpdateEvent;
-import de.danoeh.antennapod.core.event.PlayerStatusEvent;
+import de.danoeh.antennapod.event.FeedListUpdateEvent;
+import de.danoeh.antennapod.event.PlayerStatusEvent;
import de.danoeh.antennapod.core.glide.ApGlideSettings;
import de.danoeh.antennapod.core.glide.FastBlurTransformation;
import de.danoeh.antennapod.core.preferences.PlaybackPreferences;
@@ -60,7 +60,6 @@ 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.feed.VolumeAdaptionSetting;
import de.danoeh.antennapod.model.playback.RemoteMedia;
import de.danoeh.antennapod.parser.feed.UnsupportedFeedtypeException;
import io.reactivex.Maybe;
@@ -101,6 +100,8 @@ public class OnlineFeedViewActivity extends AppCompatActivity {
private Feed feed;
private String selectedDownloadUrl;
private Downloader downloader;
+ private String username = null;
+ private String password = null;
private boolean isPaused;
private boolean didPressSubscribe = false;
@@ -144,12 +145,11 @@ public class OnlineFeedViewActivity extends AppCompatActivity {
if (feedUrl.contains("subscribeonandroid.com")) {
feedUrl = feedUrl.replaceFirst("((www.)?(subscribeonandroid.com/))", "");
}
- if (savedInstanceState == null) {
- lookupUrlAndDownload(feedUrl, null, null);
- } else {
- lookupUrlAndDownload(feedUrl, savedInstanceState.getString("username"),
- savedInstanceState.getString("password"));
+ if (savedInstanceState != null) {
+ username = savedInstanceState.getString("username");
+ password = savedInstanceState.getString("password");
}
+ lookupUrlAndDownload(feedUrl);
}
}
@@ -210,10 +210,8 @@ public class OnlineFeedViewActivity extends AppCompatActivity {
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
- if (feed != null && feed.getPreferences() != null) {
- outState.putString("username", feed.getPreferences().getUsername());
- outState.putString("password", feed.getPreferences().getPassword());
- }
+ outState.putString("username", username);
+ outState.putString("password", password);
}
private void resetIntent(String url) {
@@ -242,32 +240,23 @@ public class OnlineFeedViewActivity extends AppCompatActivity {
return super.onOptionsItemSelected(item);
}
- private void lookupUrlAndDownload(String url, String username, String password) {
+ private void lookupUrlAndDownload(String url) {
download = PodcastSearcherRegistry.lookupUrl(url)
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.io())
- .subscribe(lookedUpUrl -> startFeedDownload(lookedUpUrl, username, password),
+ .subscribe(this::startFeedDownload,
error -> {
showNoPodcastFoundError();
Log.e(TAG, Log.getStackTraceString(error));
});
}
- private void startFeedDownload(String url, String username, String password) {
+ private void startFeedDownload(String url) {
Log.d(TAG, "Starting feed download");
url = URLChecker.prepareURL(url);
feed = new Feed(url, null);
- if (username != null && password != null) {
- feed.setPreferences(new FeedPreferences(0, false, FeedPreferences.AutoDeleteAction.GLOBAL,
- VolumeAdaptionSetting.OFF, username, password));
- }
- String fileUrl;
- try {
- fileUrl = DownloadRequester.getInstance().getDownloadPathForFeed(feed).getAbsolutePath();
- } catch (DownloadRequestException e) {
- e.printStackTrace();
- fileUrl = new File(getCacheDir(), FileNameGenerator.generateFileName(feed.getDownload_url())).toString();
- }
+ String fileUrl = new File(getExternalCacheDir(),
+ FileNameGenerator.generateFileName(feed.getDownload_url())).toString();
feed.setFile_url(fileUrl);
final DownloadRequest request = new DownloadRequest(feed.getFile_url(),
feed.getDownload_url(), "OnlineFeed", 0, Feed.FEEDFILETYPE_FEED, username, password,
@@ -293,6 +282,9 @@ public class OnlineFeedViewActivity extends AppCompatActivity {
parseFeed();
} else if (status.getReason() == DownloadError.ERROR_UNAUTHORIZED) {
if (!isFinishing() && !isPaused) {
+ if (username != null && password != null) {
+ Toast.makeText(this, R.string.download_error_unauthorized, Toast.LENGTH_LONG).show();
+ }
dialog = new FeedViewAuthenticationDialog(OnlineFeedViewActivity.this,
R.string.authentication_notification_title,
downloader.getDownloadRequest().getSource()).create();
@@ -458,8 +450,7 @@ public class OnlineFeedViewActivity extends AppCompatActivity {
final int MAX_LINES_COLLAPSED = 10;
description.setMaxLines(MAX_LINES_COLLAPSED);
description.setOnClickListener(v -> {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN
- && description.getMaxLines() > MAX_LINES_COLLAPSED) {
+ if (description.getMaxLines() > MAX_LINES_COLLAPSED) {
description.setMaxLines(MAX_LINES_COLLAPSED);
} else {
description.setMaxLines(2000);
@@ -642,21 +633,17 @@ public class OnlineFeedViewActivity extends AppCompatActivity {
if (urls.size() == 1) {
// Skip dialog and display the item directly
resetIntent(urls.get(0));
- startFeedDownload(urls.get(0), null, null);
+ startFeedDownload(urls.get(0));
return true;
}
- final ArrayAdapter<String> adapter = new ArrayAdapter<>(OnlineFeedViewActivity.this, R.layout.ellipsize_start_listitem, R.id.txtvTitle, titles);
+ final ArrayAdapter<String> adapter = new ArrayAdapter<>(OnlineFeedViewActivity.this,
+ R.layout.ellipsize_start_listitem, R.id.txtvTitle, titles);
DialogInterface.OnClickListener onClickListener = (dialog, which) -> {
String selectedUrl = urls.get(which);
dialog.dismiss();
resetIntent(selectedUrl);
- FeedPreferences prefs = feed.getPreferences();
- if(prefs != null) {
- startFeedDownload(selectedUrl, prefs.getUsername(), prefs.getPassword());
- } else {
- startFeedDownload(selectedUrl, null, null);
- }
+ startFeedDownload(selectedUrl);
};
AlertDialog.Builder ab = new AlertDialog.Builder(OnlineFeedViewActivity.this)
@@ -679,7 +666,7 @@ public class OnlineFeedViewActivity extends AppCompatActivity {
private final String feedUrl;
FeedViewAuthenticationDialog(Context context, int titleRes, String feedUrl) {
- super(context, titleRes, true, null, null);
+ super(context, titleRes, true, username, password);
this.feedUrl = feedUrl;
}
@@ -691,7 +678,9 @@ public class OnlineFeedViewActivity extends AppCompatActivity {
@Override
protected void onConfirmed(String username, String password) {
- startFeedDownload(feedUrl, username, password);
+ OnlineFeedViewActivity.this.username = username;
+ OnlineFeedViewActivity.this.password = password;
+ startFeedDownload(feedUrl);
}
}
diff --git a/app/src/main/java/de/danoeh/antennapod/activity/OpmlImportActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/OpmlImportActivity.java
index a6810715c..3d0c9d113 100644
--- a/app/src/main/java/de/danoeh/antennapod/activity/OpmlImportActivity.java
+++ b/app/src/main/java/de/danoeh/antennapod/activity/OpmlImportActivity.java
@@ -1,5 +1,6 @@
package de.danoeh.antennapod.activity;
+import android.Manifest;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
@@ -15,7 +16,9 @@ import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.Toast;
-import androidx.annotation.NonNull;
+
+import androidx.activity.result.ActivityResultLauncher;
+import androidx.activity.result.contract.ActivityResultContracts.RequestPermission;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
@@ -35,7 +38,6 @@ import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
import org.apache.commons.io.ByteOrderMark;
import org.apache.commons.io.input.BOMInputStream;
-import org.apache.commons.lang3.ArrayUtils;
import java.io.InputStream;
import java.io.InputStreamReader;
@@ -48,7 +50,6 @@ import java.util.List;
* */
public class OpmlImportActivity extends AppCompatActivity {
private static final String TAG = "OpmlImportBaseActivity";
- private static final int PERMISSION_REQUEST_READ_EXTERNAL_STORAGE = 5;
@Nullable private Uri uri;
OpmlSelectionBinding viewBinding;
private ArrayAdapter<String> listAdapter;
@@ -198,27 +199,23 @@ public class OpmlImportActivity extends AppCompatActivity {
}
private void requestPermission() {
- String[] permissions = { android.Manifest.permission.READ_EXTERNAL_STORAGE };
- ActivityCompat.requestPermissions(this, permissions, PERMISSION_REQUEST_READ_EXTERNAL_STORAGE);
+ requestPermissionLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE);
}
- @Override
- public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
- @NonNull int[] grantResults) {
- super.onRequestPermissionsResult(requestCode, permissions, grantResults);
- if (requestCode != PERMISSION_REQUEST_READ_EXTERNAL_STORAGE) {
- return;
- }
- if (grantResults.length > 0 && ArrayUtils.contains(grantResults, PackageManager.PERMISSION_GRANTED)) {
- startImport();
- } else {
- new AlertDialog.Builder(this)
- .setMessage(R.string.opml_import_ask_read_permission)
- .setPositiveButton(android.R.string.ok, (dialog, which) -> requestPermission())
- .setNegativeButton(R.string.cancel_label, (dialog, which) -> finish())
- .show();
- }
- }
+ private final ActivityResultLauncher<String> requestPermissionLauncher =
+ registerForActivityResult(new RequestPermission(), isGranted -> {
+ if (isGranted) {
+ startImport();
+ } else {
+ new AlertDialog.Builder(this)
+ .setMessage(R.string.opml_import_ask_read_permission)
+ .setPositiveButton(android.R.string.ok, (dialog, which) ->
+ requestPermission())
+ .setNegativeButton(R.string.cancel_label, (dialog, which) ->
+ finish())
+ .show();
+ }
+ });
/** Starts the import process. */
private void startImport() {
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 600204554..1fc16ab32 100644
--- a/app/src/main/java/de/danoeh/antennapod/activity/PreferenceActivity.java
+++ b/app/src/main/java/de/danoeh/antennapod/activity/PreferenceActivity.java
@@ -6,6 +6,7 @@ import android.os.Bundle;
import android.provider.Settings;
import android.view.Menu;
import android.view.MenuItem;
+
import android.view.View;
import android.view.inputmethod.InputMethodManager;
@@ -21,13 +22,13 @@ import de.danoeh.antennapod.R;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.databinding.SettingsActivityBinding;
import de.danoeh.antennapod.fragment.preferences.AutoDownloadPreferencesFragment;
-import de.danoeh.antennapod.fragment.preferences.GpodderPreferencesFragment;
import de.danoeh.antennapod.fragment.preferences.ImportExportPreferencesFragment;
import de.danoeh.antennapod.fragment.preferences.MainPreferencesFragment;
import de.danoeh.antennapod.fragment.preferences.NetworkPreferencesFragment;
import de.danoeh.antennapod.fragment.preferences.NotificationPreferencesFragment;
import de.danoeh.antennapod.fragment.preferences.PlaybackPreferencesFragment;
import de.danoeh.antennapod.fragment.preferences.StoragePreferencesFragment;
+import de.danoeh.antennapod.fragment.preferences.synchronization.SynchronizationPreferencesFragment;
import de.danoeh.antennapod.fragment.preferences.SwipePreferencesFragment;
import de.danoeh.antennapod.fragment.preferences.UserInterfacePreferencesFragment;
@@ -76,8 +77,8 @@ public class PreferenceActivity extends AppCompatActivity implements SearchPrefe
prefFragment = new ImportExportPreferencesFragment();
} else if (screen == R.xml.preferences_autodownload) {
prefFragment = new AutoDownloadPreferencesFragment();
- } else if (screen == R.xml.preferences_gpodder) {
- prefFragment = new GpodderPreferencesFragment();
+ } else if (screen == R.xml.preferences_synchronization) {
+ prefFragment = new SynchronizationPreferencesFragment();
} else if (screen == R.xml.preferences_playback) {
prefFragment = new PlaybackPreferencesFragment();
} else if (screen == R.xml.preferences_notifications) {
@@ -101,8 +102,8 @@ public class PreferenceActivity extends AppCompatActivity implements SearchPrefe
return R.string.import_export_pref;
} else if (preferences == R.xml.preferences_user_interface) {
return R.string.user_interface_label;
- } else if (preferences == R.xml.preferences_gpodder) {
- return R.string.gpodnet_main_label;
+ } else if (preferences == R.xml.preferences_synchronization) {
+ return R.string.synchronization_pref;
} else if (preferences == R.xml.preferences_notifications) {
return R.string.notification_pref_fragment;
} else if (preferences == R.xml.feed_settings) {
diff --git a/app/src/main/java/de/danoeh/antennapod/activity/SelectSubscriptionActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/SelectSubscriptionActivity.java
new file mode 100644
index 000000000..4ffed949e
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/activity/SelectSubscriptionActivity.java
@@ -0,0 +1,162 @@
+package de.danoeh.antennapod.activity;
+
+import static de.danoeh.antennapod.activity.MainActivity.EXTRA_FEED_ID;
+
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.os.Bundle;
+import android.util.Log;
+import android.widget.ArrayAdapter;
+import android.widget.ListView;
+
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.core.content.pm.ShortcutInfoCompat;
+import androidx.core.content.pm.ShortcutManagerCompat;
+import androidx.core.graphics.drawable.IconCompat;
+
+import com.bumptech.glide.Glide;
+import com.bumptech.glide.load.DataSource;
+import com.bumptech.glide.load.engine.GlideException;
+import com.bumptech.glide.request.RequestListener;
+import com.bumptech.glide.request.RequestOptions;
+import com.bumptech.glide.request.target.Target;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.core.preferences.UserPreferences;
+import de.danoeh.antennapod.core.storage.DBReader;
+import de.danoeh.antennapod.core.storage.NavDrawerData;
+import de.danoeh.antennapod.databinding.SubscriptionSelectionActivityBinding;
+import de.danoeh.antennapod.model.feed.Feed;
+import io.reactivex.Observable;
+import io.reactivex.android.schedulers.AndroidSchedulers;
+import io.reactivex.disposables.Disposable;
+import io.reactivex.schedulers.Schedulers;
+
+public class SelectSubscriptionActivity extends AppCompatActivity {
+
+ private static final String TAG = "SelectSubscription";
+
+ private Disposable disposable;
+ private volatile List<Feed> listItems;
+
+ private SubscriptionSelectionActivityBinding viewBinding;
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ setTheme(UserPreferences.getTranslucentTheme());
+ super.onCreate(savedInstanceState);
+
+ viewBinding = SubscriptionSelectionActivityBinding.inflate(getLayoutInflater());
+ setContentView(viewBinding.getRoot());
+ setSupportActionBar(viewBinding.toolbar);
+ setTitle(R.string.shortcut_select_subscription);
+
+ viewBinding.transparentBackground.setOnClickListener(v -> finish());
+ viewBinding.card.setOnClickListener(null);
+
+ loadSubscriptions();
+
+ final Integer[] checkedPosition = new Integer[1];
+ viewBinding.list.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
+ viewBinding.list.setOnItemClickListener((listView, view1, position, rowId) ->
+ checkedPosition[0] = position
+ );
+ viewBinding.shortcutBtn.setOnClickListener(view -> {
+ if (checkedPosition[0] != null && Intent.ACTION_CREATE_SHORTCUT.equals(
+ getIntent().getAction())) {
+ getBitmapFromUrl(listItems.get(checkedPosition[0]));
+ }
+ });
+
+ }
+
+ public List<Feed> getFeedItems(List<NavDrawerData.DrawerItem> items, List<Feed> result) {
+ for (NavDrawerData.DrawerItem item : items) {
+ if (item.type == NavDrawerData.DrawerItem.Type.TAG) {
+ getFeedItems(((NavDrawerData.TagDrawerItem) item).children, result);
+ } else {
+ Feed feed = ((NavDrawerData.FeedDrawerItem) item).feed;
+ if (!result.contains(feed)) {
+ result.add(feed);
+ }
+ }
+ }
+ return result;
+ }
+
+ private void addShortcut(Feed feed, Bitmap bitmap) {
+ Intent intent = new Intent(this, MainActivity.class);
+ intent.setAction(Intent.ACTION_MAIN);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ intent.putExtra(EXTRA_FEED_ID, feed.getId());
+ String id = "subscription-" + feed.getId();
+ IconCompat icon;
+
+ if (bitmap != null) {
+ icon = IconCompat.createWithAdaptiveBitmap(bitmap);
+ } else {
+ icon = IconCompat.createWithResource(this, R.drawable.ic_folder_shortcut);
+ }
+
+ ShortcutInfoCompat shortcut = new ShortcutInfoCompat.Builder(this, id)
+ .setShortLabel(feed.getTitle())
+ .setLongLabel(feed.getFeedTitle())
+ .setIntent(intent)
+ .setIcon(icon)
+ .build();
+
+ setResult(RESULT_OK, ShortcutManagerCompat.createShortcutResultIntent(this, shortcut));
+ finish();
+ }
+
+ private void getBitmapFromUrl(Feed feed) {
+ int iconSize = (int) (128 * getResources().getDisplayMetrics().density);
+ Glide.with(this)
+ .asBitmap()
+ .load(feed.getImageUrl())
+ .apply(new RequestOptions().override(iconSize, iconSize))
+ .listener(new RequestListener<Bitmap>() {
+ @Override
+ public boolean onLoadFailed(@Nullable GlideException e, Object model,
+ Target<Bitmap> target, boolean isFirstResource) {
+ addShortcut(feed, null);
+ return true;
+ }
+
+ @Override
+ public boolean onResourceReady(Bitmap resource, Object model,
+ Target<Bitmap> target, DataSource dataSource, boolean isFirstResource) {
+ addShortcut(feed, resource);
+ return true;
+ }
+ }).submit();
+ }
+
+ private void loadSubscriptions() {
+ if (disposable != null) {
+ disposable.dispose();
+ }
+ disposable = Observable.fromCallable(
+ () -> {
+ NavDrawerData data = DBReader.getNavDrawerData();
+ return getFeedItems(data.items, new ArrayList<>());
+ })
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(
+ result -> {
+ listItems = result;
+ ArrayList<String> titles = new ArrayList<>();
+ for (Feed feed: result) {
+ titles.add(feed.getTitle());
+ }
+ ArrayAdapter<String> adapter = new ArrayAdapter<>(this,
+ R.layout.simple_list_item_multiple_choice_on_start, titles);
+ viewBinding.list.setAdapter(adapter);
+ }, error -> Log.e(TAG, Log.getStackTraceString(error)));
+ }
+} \ No newline at end of file
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 d436acf0d..4ff2a5775 100644
--- a/app/src/main/java/de/danoeh/antennapod/activity/VideoplayerActivity.java
+++ b/app/src/main/java/de/danoeh/antennapod/activity/VideoplayerActivity.java
@@ -36,11 +36,13 @@ import androidx.core.view.WindowCompat;
import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
import com.bumptech.glide.Glide;
import de.danoeh.antennapod.R;
-import de.danoeh.antennapod.core.event.PlaybackPositionEvent;
-import de.danoeh.antennapod.core.event.ServiceEvent;
+import de.danoeh.antennapod.event.playback.BufferUpdateEvent;
+import de.danoeh.antennapod.event.playback.PlaybackPositionEvent;
+import de.danoeh.antennapod.event.PlayerErrorEvent;
+import de.danoeh.antennapod.event.playback.PlaybackServiceEvent;
+import de.danoeh.antennapod.event.playback.SleepTimerUpdatedEvent;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.service.playback.PlaybackService;
-import de.danoeh.antennapod.core.service.playback.PlayerStatus;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.storage.DBWriter;
import de.danoeh.antennapod.core.util.Converter;
@@ -50,7 +52,6 @@ import de.danoeh.antennapod.core.util.ShareUtils;
import de.danoeh.antennapod.core.util.StorageUtils;
import de.danoeh.antennapod.core.util.TimeSpeedConverter;
import de.danoeh.antennapod.core.util.gui.PictureInPictureUtil;
-import de.danoeh.antennapod.core.util.playback.MediaPlayerError;
import de.danoeh.antennapod.core.util.playback.PlaybackController;
import de.danoeh.antennapod.databinding.VideoplayerActivityBinding;
import de.danoeh.antennapod.dialog.PlaybackControlsDialog;
@@ -60,6 +61,8 @@ import de.danoeh.antennapod.dialog.SleepTimerDialog;
import de.danoeh.antennapod.model.feed.FeedItem;
import de.danoeh.antennapod.model.feed.FeedMedia;
import de.danoeh.antennapod.model.playback.Playable;
+import de.danoeh.antennapod.playback.base.PlayerStatus;
+import de.danoeh.antennapod.playback.cast.CastEnabledActivity;
import de.danoeh.antennapod.ui.appstartintent.MainActivityStarter;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
@@ -193,40 +196,11 @@ public class VideoplayerActivity extends CastEnabledActivity implements SeekBar.
}
@Override
- public void onBufferStart() {
- viewBinding.progressBar.setVisibility(View.VISIBLE);
- }
-
- @Override
- public void onBufferEnd() {
- viewBinding.progressBar.setVisibility(View.INVISIBLE);
- }
-
- @Override
- public void onBufferUpdate(float progress) {
- viewBinding.sbPosition.setSecondaryProgress((int) (progress * viewBinding.sbPosition.getMax()));
- }
-
- @Override
- public void handleError(int code) {
- final AlertDialog.Builder errorDialog = new AlertDialog.Builder(VideoplayerActivity.this);
- errorDialog.setTitle(R.string.error_label);
- errorDialog.setMessage(MediaPlayerError.getErrorString(VideoplayerActivity.this, code));
- errorDialog.setNeutralButton(android.R.string.ok, (dialog, which) -> finish());
- errorDialog.show();
- }
-
- @Override
public void onReloadNotification(int code) {
VideoplayerActivity.this.onReloadNotification(code);
}
@Override
- public void onSleepTimerUpdate() {
- supportInvalidateOptionsMenu();
- }
-
- @Override
protected void updatePlayButtonShowsPlay(boolean showPlay) {
viewBinding.playButton.setIsShowPlay(showPlay);
}
@@ -261,6 +235,26 @@ public class VideoplayerActivity extends CastEnabledActivity implements SeekBar.
};
}
+ @Subscribe(threadMode = ThreadMode.MAIN)
+ @SuppressWarnings("unused")
+ public void bufferUpdate(BufferUpdateEvent event) {
+ if (event.hasStarted()) {
+ viewBinding.progressBar.setVisibility(View.VISIBLE);
+ } else if (event.hasEnded()) {
+ viewBinding.progressBar.setVisibility(View.INVISIBLE);
+ } else {
+ viewBinding.sbPosition.setSecondaryProgress((int) (event.getProgress() * viewBinding.sbPosition.getMax()));
+ }
+ }
+
+ @Subscribe(threadMode = ThreadMode.MAIN)
+ @SuppressWarnings("unused")
+ public void sleepTimerUpdate(SleepTimerUpdatedEvent event) {
+ if (event.isCancelled() || event.wasJustEnabled()) {
+ supportInvalidateOptionsMenu();
+ }
+ }
+
protected void loadMediaInfo() {
Log.d(TAG, "loadMediaInfo()");
if (controller == null || controller.getMedia() == null) {
@@ -544,12 +538,21 @@ public class VideoplayerActivity extends CastEnabledActivity implements SeekBar.
}
@Subscribe(threadMode = ThreadMode.MAIN)
- public void onPlaybackServiceChanged(ServiceEvent event) {
- if (event.action == ServiceEvent.Action.SERVICE_SHUT_DOWN) {
+ public void onPlaybackServiceChanged(PlaybackServiceEvent event) {
+ if (event.action == PlaybackServiceEvent.Action.SERVICE_SHUT_DOWN) {
finish();
}
}
+ @Subscribe(threadMode = ThreadMode.MAIN)
+ public void onMediaPlayerError(PlayerErrorEvent event) {
+ final AlertDialog.Builder errorDialog = new AlertDialog.Builder(VideoplayerActivity.this);
+ errorDialog.setTitle(R.string.error_label);
+ errorDialog.setMessage(event.getMessage());
+ errorDialog.setNeutralButton(android.R.string.ok, (dialog, which) -> finish());
+ errorDialog.show();
+ }
+
@Override
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
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 3020aba43..674071294 100644
--- a/app/src/main/java/de/danoeh/antennapod/activity/WidgetConfigActivity.java
+++ b/app/src/main/java/de/danoeh/antennapod/activity/WidgetConfigActivity.java
@@ -1,21 +1,14 @@
package de.danoeh.antennapod.activity;
-import android.Manifest;
-import android.app.WallpaperManager;
import android.appwidget.AppWidgetManager;
import android.content.Intent;
import android.content.SharedPreferences;
-import android.content.pm.PackageManager;
-import android.graphics.drawable.Drawable;
-import android.os.Build;
import android.os.Bundle;
import android.view.View;
import android.widget.CheckBox;
-import android.widget.ImageView;
import android.widget.SeekBar;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
-import androidx.core.content.ContextCompat;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.receiver.PlayerWidget;
@@ -51,7 +44,6 @@ public class WidgetConfigActivity extends AppCompatActivity {
finish();
}
- displayDeviceBackground();
opacityTextView = findViewById(R.id.widget_opacity_textView);
opacitySeekBar = findViewById(R.id.widget_opacity_seekBar);
widgetPreview = findViewById(R.id.widgetLayout);
@@ -102,16 +94,6 @@ public class WidgetConfigActivity extends AppCompatActivity {
widgetPreview.findViewById(R.id.butRew).setVisibility(ckRewind.isChecked() ? View.VISIBLE : View.GONE);
}
- private void displayDeviceBackground() {
- int permission = ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE);
- if (Build.VERSION.SDK_INT < 27 || permission == PackageManager.PERMISSION_GRANTED) {
- final WallpaperManager wallpaperManager = WallpaperManager.getInstance(this);
- final Drawable wallpaperDrawable = wallpaperManager.getDrawable();
- ImageView background = findViewById(R.id.widget_config_background);
- background.setImageDrawable(wallpaperDrawable);
- }
- }
-
private void confirmCreateWidget(View v) {
int backgroundColor = getColorWithAlpha(PlayerWidget.DEFAULT_COLOR, opacitySeekBar.getProgress());
diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/CoverLoader.java b/app/src/main/java/de/danoeh/antennapod/adapter/CoverLoader.java
index 5d7593564..34726e248 100644
--- a/app/src/main/java/de/danoeh/antennapod/adapter/CoverLoader.java
+++ b/app/src/main/java/de/danoeh/antennapod/adapter/CoverLoader.java
@@ -3,6 +3,9 @@ package de.danoeh.antennapod.adapter;
import android.graphics.drawable.Drawable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.core.graphics.ColorUtils;
+import androidx.palette.graphics.Palette;
+
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
@@ -19,6 +22,9 @@ import com.bumptech.glide.request.transition.Transition;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.core.glide.ApGlideSettings;
+import de.danoeh.antennapod.core.glide.PaletteBitmap;
+import de.danoeh.antennapod.core.preferences.UserPreferences;
+import de.danoeh.antennapod.ui.common.ThemeUtils;
public class CoverLoader {
private int resource = 0;
@@ -77,7 +83,7 @@ public class CoverLoader {
if (resource != 0) {
Glide.with(activity).clear(coverTarget);
imgvCover.setImageResource(resource);
- CoverTarget.setPlaceholderVisibility(txtvPlaceholder, textAndImageCombined);
+ CoverTarget.setPlaceholderVisibility(txtvPlaceholder, textAndImageCombined, null);
return;
}
@@ -86,12 +92,14 @@ public class CoverLoader {
.fitCenter()
.dontAnimate();
- RequestBuilder<Drawable> builder = Glide.with(activity)
+ RequestBuilder<PaletteBitmap> builder = Glide.with(activity)
+ .as(PaletteBitmap.class)
.load(uri)
.apply(options);
if (fallbackUri != null && txtvPlaceholder != null && imgvCover != null) {
builder = builder.error(Glide.with(activity)
+ .as(PaletteBitmap.class)
.load(fallbackUri)
.apply(options));
}
@@ -99,7 +107,7 @@ public class CoverLoader {
builder.into(coverTarget);
}
- static class CoverTarget extends CustomViewTarget<ImageView, Drawable> {
+ static class CoverTarget extends CustomViewTarget<ImageView, PaletteBitmap> {
private final WeakReference<TextView> placeholder;
private final WeakReference<ImageView> cover;
private boolean textAndImageCombined;
@@ -120,23 +128,38 @@ public class CoverLoader {
}
@Override
- public void onResourceReady(@NonNull Drawable resource, @Nullable Transition<? super Drawable> transition) {
- setPlaceholderVisibility(placeholder.get(), textAndImageCombined);
+ public void onResourceReady(@NonNull PaletteBitmap resource,
+ @Nullable Transition<? super PaletteBitmap> transition) {
ImageView ivCover = cover.get();
- ivCover.setImageDrawable(resource);
+ ivCover.setImageBitmap(resource.bitmap);
+ setPlaceholderVisibility(placeholder.get(), textAndImageCombined, resource.palette);
}
@Override
protected void onResourceCleared(@Nullable Drawable placeholder) {
ImageView ivCover = cover.get();
ivCover.setImageDrawable(placeholder);
+ setPlaceholderVisibility(this.placeholder.get(), textAndImageCombined, null);
}
- static void setPlaceholderVisibility(TextView placeholder, boolean textAndImageCombined) {
+ static void setPlaceholderVisibility(TextView placeholder, boolean textAndImageCombined, Palette palette) {
+ boolean showTitle = UserPreferences.shouldShowSubscriptionTitle();
if (placeholder != null) {
- if (textAndImageCombined) {
+ if (textAndImageCombined || showTitle) {
int bgColor = placeholder.getContext().getResources().getColor(R.color.feed_text_bg);
- placeholder.setBackgroundColor(bgColor);
+ if (palette == null || !showTitle) {
+ placeholder.setBackgroundColor(bgColor);
+ placeholder.setTextColor(ThemeUtils.getColorFromAttr(placeholder.getContext(),
+ android.R.attr.textColorPrimary));
+ return;
+ }
+ int dominantColor = palette.getDominantColor(bgColor);
+ int textColor = placeholder.getContext().getResources().getColor(R.color.white);
+ if (ColorUtils.calculateLuminance(dominantColor) > 0.5) {
+ textColor = placeholder.getContext().getResources().getColor(R.color.black);
+ }
+ placeholder.setTextColor(textColor);
+ placeholder.setBackgroundColor(dominantColor);
} else {
placeholder.setVisibility(View.INVISIBLE);
}
diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/DataFolderAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/DataFolderAdapter.java
index bcad1b5a4..862dc2fe2 100644
--- a/app/src/main/java/de/danoeh/antennapod/adapter/DataFolderAdapter.java
+++ b/app/src/main/java/de/danoeh/antennapod/adapter/DataFolderAdapter.java
@@ -9,7 +9,6 @@ import android.widget.ProgressBar;
import android.widget.RadioButton;
import android.widget.TextView;
import androidx.annotation.NonNull;
-import androidx.core.content.ContextCompat;
import androidx.core.util.Consumer;
import androidx.recyclerview.widget.RecyclerView;
import de.danoeh.antennapod.R;
@@ -74,7 +73,7 @@ public class DataFolderAdapter extends RecyclerView.Adapter<DataFolderAdapter.Vi
}
private List<StoragePath> getStorageEntries(Context context) {
- File[] mediaDirs = ContextCompat.getExternalFilesDirs(context, null);
+ File[] mediaDirs = context.getExternalFilesDirs(null);
final List<StoragePath> entries = new ArrayList<>(mediaDirs.length);
for (File dir : mediaDirs) {
if (!isWritable(dir)) {
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 2ab96e84d..5ddb6407c 100644
--- a/app/src/main/java/de/danoeh/antennapod/adapter/FeedItemlistDescriptionAdapter.java
+++ b/app/src/main/java/de/danoeh/antennapod/adapter/FeedItemlistDescriptionAdapter.java
@@ -95,7 +95,7 @@ public class FeedItemlistDescriptionAdapter extends ArrayAdapter<FeedItem> {
holder.preview.setVisibility(View.GONE);
holder.description.setTag(Boolean.FALSE);
} else {
- holder.description.setMaxLines(2000);
+ holder.description.setMaxLines(30);
holder.description.setTag(Boolean.TRUE);
holder.preview.setVisibility(item.getMedia() != null ? View.VISIBLE : View.GONE);
diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/NavListAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/NavListAdapter.java
index ff0311ab6..34eb48b6b 100644
--- a/app/src/main/java/de/danoeh/antennapod/adapter/NavListAdapter.java
+++ b/app/src/main/java/de/danoeh/antennapod/adapter/NavListAdapter.java
@@ -194,10 +194,10 @@ public class NavListAdapter extends RecyclerView.Adapter<NavListAdapter.Holder>
bindListItem(item, (FeedHolder) holder);
if (item.type == NavDrawerData.DrawerItem.Type.FEED) {
bindFeedView((NavDrawerData.FeedDrawerItem) item, (FeedHolder) holder);
- holder.itemView.setOnCreateContextMenuListener(itemAccess);
} else {
- bindFolderView((NavDrawerData.FolderDrawerItem) item, (FeedHolder) holder);
+ bindTagView((NavDrawerData.TagDrawerItem) item, (FeedHolder) holder);
}
+ holder.itemView.setOnCreateContextMenuListener(itemAccess);
}
if (viewType != VIEW_TYPE_SECTION_DIVIDER) {
TypedValue typedValue = new TypedValue();
@@ -327,16 +327,16 @@ public class NavListAdapter extends RecyclerView.Adapter<NavListAdapter.Holder>
}
}
- private void bindFolderView(NavDrawerData.FolderDrawerItem folder, FeedHolder holder) {
+ private void bindTagView(NavDrawerData.TagDrawerItem tag, FeedHolder holder) {
Activity context = activity.get();
if (context == null) {
return;
}
- if (folder.isOpen) {
+ if (tag.isOpen) {
holder.count.setVisibility(View.GONE);
}
Glide.with(context).clear(holder.image);
- holder.image.setImageResource(R.drawable.ic_folder);
+ holder.image.setImageResource(R.drawable.ic_tag);
holder.failure.setVisibility(View.GONE);
}
diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/PlaybackStatisticsListAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/PlaybackStatisticsListAdapter.java
index 5fec5f063..26674b2b2 100644
--- a/app/src/main/java/de/danoeh/antennapod/adapter/PlaybackStatisticsListAdapter.java
+++ b/app/src/main/java/de/danoeh/antennapod/adapter/PlaybackStatisticsListAdapter.java
@@ -1,13 +1,12 @@
package de.danoeh.antennapod.adapter;
-import android.content.Context;
-import androidx.appcompat.app.AlertDialog;
-
+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;
@@ -18,10 +17,12 @@ import java.util.List;
*/
public class PlaybackStatisticsListAdapter extends StatisticsListAdapter {
+ private final Fragment fragment;
boolean countAll = true;
- public PlaybackStatisticsListAdapter(Context context) {
- super(context);
+ public PlaybackStatisticsListAdapter(Fragment fragment) {
+ super(fragment.getContext());
+ this.fragment = fragment;
}
public void setCountAll(boolean countAll) {
@@ -60,16 +61,9 @@ public class PlaybackStatisticsListAdapter extends StatisticsListAdapter {
holder.value.setText(Converter.shortLocalizedDuration(context, time));
holder.itemView.setOnClickListener(v -> {
- AlertDialog.Builder dialog = new AlertDialog.Builder(context);
- dialog.setTitle(statsItem.feed.getTitle());
- dialog.setMessage(context.getString(R.string.statistics_details_dialog,
- countAll ? statsItem.episodesStartedIncludingMarked : statsItem.episodesStarted,
- statsItem.episodes, Converter.shortLocalizedDuration(context,
- countAll ? statsItem.timePlayedCountAll : statsItem.timePlayed),
- Converter.shortLocalizedDuration(context, statsItem.time)));
- dialog.setPositiveButton(android.R.string.ok, null);
- dialog.show();
+ 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/QueueRecyclerAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/QueueRecyclerAdapter.java
index 383d670f1..42813c8d6 100644
--- a/app/src/main/java/de/danoeh/antennapod/adapter/QueueRecyclerAdapter.java
+++ b/app/src/main/java/de/danoeh/antennapod/adapter/QueueRecyclerAdapter.java
@@ -7,7 +7,6 @@ import android.view.MenuInflater;
import android.view.MotionEvent;
import android.view.View;
-import androidx.core.view.ViewCompat;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.core.preferences.UserPreferences;
@@ -53,7 +52,7 @@ public class QueueRecyclerAdapter extends EpisodeItemListAdapter {
});
holder.coverHolder.setOnTouchListener((v1, event) -> {
if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
- boolean isLtr = ViewCompat.getLayoutDirection(holder.itemView) == ViewCompat.LAYOUT_DIRECTION_LTR;
+ boolean isLtr = holder.itemView.getLayoutDirection() == View.LAYOUT_DIRECTION_LTR;
float factor = isLtr ? 1 : -1;
if (factor * event.getX() < factor * 0.5 * v1.getWidth()) {
Log.d(TAG, "startDrag()");
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 73f67d016..21c5e1897 100644
--- a/app/src/main/java/de/danoeh/antennapod/adapter/SubscriptionsRecyclerAdapter.java
+++ b/app/src/main/java/de/danoeh/antennapod/adapter/SubscriptionsRecyclerAdapter.java
@@ -4,6 +4,7 @@ import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
+import android.text.TextUtils;
import android.view.ContextMenu;
import android.view.LayoutInflater;
import android.view.MenuInflater;
@@ -13,12 +14,11 @@ import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.FrameLayout;
import android.widget.ImageView;
+import android.widget.RelativeLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.appcompat.content.res.AppCompatResources;
-import androidx.core.text.TextUtilsCompat;
-import androidx.core.view.ViewCompat;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.RecyclerView;
@@ -31,6 +31,7 @@ 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;
import de.danoeh.antennapod.fragment.SubscriptionFragment;
@@ -42,9 +43,11 @@ import jp.shts.android.library.TriangleLabelView;
*/
public class SubscriptionsRecyclerAdapter extends SelectableAdapter<SubscriptionsRecyclerAdapter.SubscriptionViewHolder>
implements View.OnCreateContextMenuListener {
+ private static final int COVER_WITH_TITLE = 1;
+
private final WeakReference<MainActivity> mainActivityRef;
private List<NavDrawerData.DrawerItem> listItems;
- private Feed selectedFeed = null;
+ private NavDrawerData.DrawerItem selectedItem = null;
int longPressedPosition = 0; // used to init actionMode
public SubscriptionsRecyclerAdapter(MainActivity mainActivity) {
@@ -58,14 +61,31 @@ public class SubscriptionsRecyclerAdapter extends SelectableAdapter<Subscription
return listItems.get(position);
}
- public Feed getSelectedFeed() {
- return selectedFeed;
+ public NavDrawerData.DrawerItem getSelectedItem() {
+ return selectedItem;
}
@NonNull
@Override
public SubscriptionViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View itemView = LayoutInflater.from(mainActivityRef.get()).inflate(R.layout.subscription_item, parent, false);
+ TextView feedTitle = itemView.findViewById(R.id.txtvTitle);
+ RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) feedTitle.getLayoutParams();
+ int topAndBottomItemId = R.id.imgvCover;
+ int belowItemId = 0;
+
+ if (viewType == COVER_WITH_TITLE) {
+ topAndBottomItemId = 0;
+ belowItemId = R.id.imgvCover;
+ feedTitle.setBackgroundColor(feedTitle.getContext().getResources().getColor(R.color.feed_text_bg));
+ int padding = (int) convertDpToPixel(feedTitle.getContext(), 6);
+ feedTitle.setPadding(padding, padding, padding, padding);
+ }
+ params.addRule(RelativeLayout.BELOW, belowItemId);
+ params.addRule(RelativeLayout.ALIGN_TOP, topAndBottomItemId);
+ params.addRule(RelativeLayout.ALIGN_BOTTOM, topAndBottomItemId);
+ feedTitle.setLayoutParams(params);
+ feedTitle.setSingleLine(viewType == COVER_WITH_TITLE);
return new SubscriptionViewHolder(itemView);
}
@@ -93,11 +113,9 @@ public class SubscriptionsRecyclerAdapter extends SelectableAdapter<Subscription
holder.itemView.setOnLongClickListener(v -> {
if (!inActionMode()) {
if (isFeed) {
- selectedFeed = ((NavDrawerData.FeedDrawerItem) getItem(holder.getBindingAdapterPosition())).feed;
longPressedPosition = holder.getBindingAdapterPosition();
- } else {
- selectedFeed = null;
}
+ selectedItem = (NavDrawerData.DrawerItem) getItem(holder.getBindingAdapterPosition());
}
return false;
});
@@ -131,12 +149,17 @@ public class SubscriptionsRecyclerAdapter extends SelectableAdapter<Subscription
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
- if (selectedFeed != null && !inActionMode()) {
- MenuInflater inflater = mainActivityRef.get().getMenuInflater();
+ if (inActionMode() || selectedItem == null) {
+ return;
+ }
+ MenuInflater inflater = mainActivityRef.get().getMenuInflater();
+ if (selectedItem.type == NavDrawerData.DrawerItem.Type.FEED) {
inflater.inflate(R.menu.nav_feed_context, menu);
- menu.setHeaderTitle(selectedFeed.getTitle());
menu.findItem(R.id.multi_select).setVisible(true);
+ } else {
+ inflater.inflate(R.menu.nav_folder_context, menu);
}
+ menu.setHeaderTitle(selectedItem.getTitle());
}
public boolean onContextItemSelected(MenuItem item) {
@@ -173,6 +196,11 @@ public class SubscriptionsRecyclerAdapter extends SelectableAdapter<Subscription
}
}
+ @Override
+ public int getItemViewType(int position) {
+ return UserPreferences.shouldShowSubscriptionTitle() ? COVER_WITH_TITLE : 0;
+ }
+
public class SubscriptionViewHolder extends RecyclerView.ViewHolder {
private final TextView feedTitle;
private final ImageView imageView;
@@ -196,8 +224,7 @@ public class SubscriptionsRecyclerAdapter extends SelectableAdapter<Subscription
feedTitle.setText(drawerItem.getTitle());
imageView.setContentDescription(drawerItem.getTitle());
feedTitle.setVisibility(View.VISIBLE);
- if (TextUtilsCompat.getLayoutDirectionFromLocale(Locale.getDefault())
- == ViewCompat.LAYOUT_DIRECTION_RTL) {
+ if (TextUtils.getLayoutDirectionFromLocale(Locale.getDefault()) == View.LAYOUT_DIRECTION_RTL) {
count.setCorner(TriangleLabelView.Corner.TOP_LEFT);
}
@@ -219,7 +246,7 @@ public class SubscriptionsRecyclerAdapter extends SelectableAdapter<Subscription
.load();
} else {
new CoverLoader(mainActivityRef.get())
- .withResource(R.drawable.ic_folder)
+ .withResource(R.drawable.ic_tag)
.withPlaceholderView(feedTitle, true)
.withCoverView(imageView)
.load();
diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/CancelDownloadActionButton.java b/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/CancelDownloadActionButton.java
index dedf8e5e6..a2b0e98c3 100644
--- a/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/CancelDownloadActionButton.java
+++ b/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/CancelDownloadActionButton.java
@@ -34,7 +34,7 @@ public class CancelDownloadActionButton extends ItemActionButton {
FeedMedia media = item.getMedia();
DownloadRequester.getInstance().cancelDownload(context, media);
if (UserPreferences.isEnableAutodownload()) {
- item.setAutoDownload(false);
+ item.disableAutoDownload();
DBWriter.setFeedItem(item);
}
}
diff --git a/app/src/main/java/de/danoeh/antennapod/config/ClientConfigurator.java b/app/src/main/java/de/danoeh/antennapod/config/ClientConfigurator.java
index a45eb5199..1f4f657b1 100644
--- a/app/src/main/java/de/danoeh/antennapod/config/ClientConfigurator.java
+++ b/app/src/main/java/de/danoeh/antennapod/config/ClientConfigurator.java
@@ -15,6 +15,5 @@ class ClientConfigurator {
ClientConfig.USER_AGENT = "AntennaPod/" + BuildConfig.VERSION_NAME;
ClientConfig.applicationCallbacks = new ApplicationCallbacksImpl();
ClientConfig.downloadServiceCallbacks = new DownloadServiceCallbacksImpl();
- ClientConfig.castCallbacks = new CastCallbackImpl();
}
}
diff --git a/app/src/main/java/de/danoeh/antennapod/config/DownloadServiceCallbacksImpl.java b/app/src/main/java/de/danoeh/antennapod/config/DownloadServiceCallbacksImpl.java
index 938bb5931..590b7c897 100644
--- a/app/src/main/java/de/danoeh/antennapod/config/DownloadServiceCallbacksImpl.java
+++ b/app/src/main/java/de/danoeh/antennapod/config/DownloadServiceCallbacksImpl.java
@@ -3,6 +3,7 @@ package de.danoeh.antennapod.config;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
+import android.os.Build;
import android.os.Bundle;
import de.danoeh.antennapod.R;
@@ -24,7 +25,8 @@ public class DownloadServiceCallbacksImpl implements DownloadServiceCallbacks {
args.putInt(DownloadsFragment.ARG_SELECTED_TAB, DownloadsFragment.POS_LOG);
intent.putExtra(MainActivity.EXTRA_FRAGMENT_ARGS, args);
return PendingIntent.getActivity(context,
- R.id.pending_intent_download_service_notification, intent, PendingIntent.FLAG_UPDATE_CURRENT);
+ R.id.pending_intent_download_service_notification, intent,
+ PendingIntent.FLAG_UPDATE_CURRENT | (Build.VERSION.SDK_INT >= 23 ? PendingIntent.FLAG_IMMUTABLE : 0));
}
@Override
@@ -33,7 +35,8 @@ public class DownloadServiceCallbacksImpl implements DownloadServiceCallbacks {
activityIntent.setAction("request" + request.getFeedfileId());
activityIntent.putExtra(DownloadAuthenticationActivity.ARG_DOWNLOAD_REQUEST, request);
return PendingIntent.getActivity(context.getApplicationContext(),
- R.id.pending_intent_download_service_auth, activityIntent, PendingIntent.FLAG_ONE_SHOT);
+ R.id.pending_intent_download_service_auth, activityIntent,
+ PendingIntent.FLAG_ONE_SHOT | (Build.VERSION.SDK_INT >= 23 ? PendingIntent.FLAG_IMMUTABLE : 0));
}
@Override
@@ -43,15 +46,15 @@ public class DownloadServiceCallbacksImpl implements DownloadServiceCallbacks {
Bundle args = new Bundle();
args.putInt(DownloadsFragment.ARG_SELECTED_TAB, DownloadsFragment.POS_LOG);
intent.putExtra(MainActivity.EXTRA_FRAGMENT_ARGS, args);
- return PendingIntent.getActivity(context, R.id.pending_intent_download_service_report,
- intent, PendingIntent.FLAG_UPDATE_CURRENT);
+ return PendingIntent.getActivity(context, R.id.pending_intent_download_service_report, intent,
+ PendingIntent.FLAG_UPDATE_CURRENT | (Build.VERSION.SDK_INT >= 23 ? PendingIntent.FLAG_IMMUTABLE : 0));
}
@Override
public PendingIntent getAutoDownloadReportNotificationContentIntent(Context context) {
Intent intent = new Intent(context, MainActivity.class);
intent.putExtra(MainActivity.EXTRA_FRAGMENT_TAG, QueueFragment.TAG);
- return PendingIntent.getActivity(context, R.id.pending_intent_download_service_autodownload_report,
- intent, PendingIntent.FLAG_UPDATE_CURRENT);
+ return PendingIntent.getActivity(context, R.id.pending_intent_download_service_autodownload_report, intent,
+ PendingIntent.FLAG_UPDATE_CURRENT | (Build.VERSION.SDK_INT >= 23 ? PendingIntent.FLAG_IMMUTABLE : 0));
}
}
diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/EpisodeFilterDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/EpisodeFilterDialog.java
index ee19a0339..595f37e40 100644
--- a/app/src/main/java/de/danoeh/antennapod/dialog/EpisodeFilterDialog.java
+++ b/app/src/main/java/de/danoeh/antennapod/dialog/EpisodeFilterDialog.java
@@ -2,6 +2,7 @@ package de.danoeh.antennapod.dialog;
import android.content.Context;
import android.view.View;
+import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.RadioButton;
@@ -14,7 +15,6 @@ import de.danoeh.antennapod.model.feed.FeedFilter;
* Displays a dialog with a text box for filtering episodes and two radio buttons for exclusion/inclusion
*/
public abstract class EpisodeFilterDialog extends AlertDialog.Builder {
-
private final FeedFilter initialFilter;
public EpisodeFilterDialog(Context context, FeedFilter filter) {
@@ -26,8 +26,10 @@ public abstract class EpisodeFilterDialog extends AlertDialog.Builder {
setView(rootView);
final EditText etxtEpisodeFilterText = rootView.findViewById(R.id.etxtEpisodeFilterText);
+ final EditText etxtEpisodeFilterDurationText = rootView.findViewById(R.id.etxtEpisodeFilterDurationText);
final RadioButton radioInclude = rootView.findViewById(R.id.radio_filter_include);
final RadioButton radioExclude = rootView.findViewById(R.id.radio_filter_exclude);
+ final CheckBox checkboxDuration = rootView.findViewById(R.id.checkbox_filter_duration);
if (initialFilter.includeOnly()) {
radioInclude.setChecked(true);
@@ -40,18 +42,31 @@ public abstract class EpisodeFilterDialog extends AlertDialog.Builder {
radioInclude.setChecked(false);
etxtEpisodeFilterText.setText("");
}
+ if (initialFilter.hasMinimalDurationFilter()) {
+ checkboxDuration.setChecked(true);
+ // Store minimal duration in seconds, show in minutes
+ etxtEpisodeFilterDurationText.setText(String.valueOf(initialFilter.getMinimalDurationFilter() / 60));
+ }
setNegativeButton(R.string.cancel_label, null);
setPositiveButton(R.string.confirm_label, (dialog, which) -> {
String includeString = "";
String excludeString = "";
+ int minimalDuration = -1;
if (radioInclude.isChecked()) {
includeString = etxtEpisodeFilterText.getText().toString();
} else {
excludeString = etxtEpisodeFilterText.getText().toString();
}
-
- onConfirmed(new FeedFilter(includeString, excludeString));
+ if (checkboxDuration.isChecked()) {
+ try {
+ // Store minimal duration in seconds
+ minimalDuration = Integer.parseInt(etxtEpisodeFilterDurationText.getText().toString()) * 60;
+ } catch (NumberFormatException e) {
+ // Do not change anything on error
+ }
+ }
+ onConfirmed(new FeedFilter(includeString, excludeString, minimalDuration));
}
);
}
diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/FeedSortDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/FeedSortDialog.java
index 96d1b9b67..b89d05f88 100644
--- a/app/src/main/java/de/danoeh/antennapod/dialog/FeedSortDialog.java
+++ b/app/src/main/java/de/danoeh/antennapod/dialog/FeedSortDialog.java
@@ -10,7 +10,7 @@ import java.util.Arrays;
import java.util.List;
import de.danoeh.antennapod.R;
-import de.danoeh.antennapod.core.event.UnreadItemsUpdateEvent;
+import de.danoeh.antennapod.event.UnreadItemsUpdateEvent;
import de.danoeh.antennapod.core.preferences.UserPreferences;
public class FeedSortDialog {
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 3186cbe2e..5cc1f99c6 100644
--- a/app/src/main/java/de/danoeh/antennapod/dialog/PlaybackControlsDialog.java
+++ b/app/src/main/java/de/danoeh/antennapod/dialog/PlaybackControlsDialog.java
@@ -12,9 +12,13 @@ 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;
@@ -22,6 +26,8 @@ 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();
@@ -42,10 +48,12 @@ public class PlaybackControlsDialog extends DialogFragment {
public void loadMediaInfo() {
setupUi();
setupAudioTracks();
+ updateSpeed(new SpeedChangedEvent(getCurrentPlaybackSpeedMultiplier()));
}
};
controller.init();
setupUi();
+ EventBus.getDefault().register(this);
}
@Override
@@ -53,6 +61,7 @@ public class PlaybackControlsDialog extends DialogFragment {
super.onStop();
controller.release();
controller = null;
+ EventBus.getDefault().unregister(this);
}
@NonNull
@@ -66,12 +75,14 @@ public class PlaybackControlsDialog extends DialogFragment {
}
private void setupUi() {
- final TextView txtvPlaybackSpeed = dialog.findViewById(R.id.txtvPlaybackSpeed);
-
- PlaybackSpeedSeekBar speedSeekBar = dialog.findViewById(R.id.speed_seek_bar);
- speedSeekBar.setController(controller);
- speedSeekBar.setProgressChangedListener(speed
- -> txtvPlaybackSpeed.setText(String.format(Locale.getDefault(), "%.2fx", speed)));
+ 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());
@@ -100,6 +111,12 @@ 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 13258b4ec..ad2ed3499 100644
--- a/app/src/main/java/de/danoeh/antennapod/dialog/ProxyDialog.java
+++ b/app/src/main/java/de/danoeh/antennapod/dialog/ProxyDialog.java
@@ -67,6 +67,7 @@ public class ProxyDialog {
.setView(content)
.setNegativeButton(R.string.cancel_label, null)
.setPositiveButton(R.string.proxy_test_label, null)
+ .setNeutralButton(R.string.reset, null)
.show();
// To prevent cancelling the dialog on button click
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener((view) -> {
@@ -75,36 +76,19 @@ public class ProxyDialog {
test();
return;
}
- String type = (String) spType.getSelectedItem();
- ProxyConfig proxy;
- if (Proxy.Type.valueOf(type) == Proxy.Type.DIRECT) {
- proxy = ProxyConfig.direct();
- } else {
- String host = etHost.getText().toString();
- String port = etPort.getText().toString();
- String username = etUsername.getText().toString();
- if (TextUtils.isEmpty(username)) {
- username = null;
- }
- String password = etPassword.getText().toString();
- if (TextUtils.isEmpty(password)) {
- password = null;
- }
- int portValue = 0;
- if (!TextUtils.isEmpty(port)) {
- portValue = Integer.parseInt(port);
- }
- if (Proxy.Type.valueOf(type) == Proxy.Type.SOCKS) {
- proxy = ProxyConfig.socks(host, portValue, username, password);
- } else {
- proxy = ProxyConfig.http(host, portValue, username, password);
- }
- }
- UserPreferences.setProxyConfig(proxy);
+ setProxyConfig();
AntennapodHttpClient.reinit();
dialog.dismiss();
});
+ dialog.getButton(AlertDialog.BUTTON_NEUTRAL).setOnClickListener((view) -> {
+ etHost.getText().clear();
+ etPort.getText().clear();
+ etUsername.getText().clear();
+ etPassword.getText().clear();
+ setProxyConfig();
+ });
+
List<String> types = new ArrayList<>();
types.add(Proxy.Type.DIRECT.name());
types.add(Proxy.Type.HTTP.name());
@@ -144,6 +128,11 @@ public class ProxyDialog {
spType.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
+ if (position == 0) {
+ dialog.getButton(AlertDialog.BUTTON_NEUTRAL).setVisibility(View.GONE);
+ } else {
+ dialog.getButton(AlertDialog.BUTTON_NEUTRAL).setVisibility(View.VISIBLE);
+ }
enableSettings(position > 0);
setTestRequired(position > 0);
}
@@ -158,6 +147,35 @@ public class ProxyDialog {
return dialog;
}
+ private void setProxyConfig() {
+ String type = (String) spType.getSelectedItem();
+ ProxyConfig proxy;
+ if (Proxy.Type.valueOf(type) == Proxy.Type.DIRECT) {
+ proxy = ProxyConfig.direct();
+ } else {
+ String host = etHost.getText().toString();
+ String port = etPort.getText().toString();
+ String username = etUsername.getText().toString();
+ if (TextUtils.isEmpty(username)) {
+ username = null;
+ }
+ String password = etPassword.getText().toString();
+ if (TextUtils.isEmpty(password)) {
+ password = null;
+ }
+ int portValue = 0;
+ if (!TextUtils.isEmpty(port)) {
+ portValue = Integer.parseInt(port);
+ }
+ if (Proxy.Type.valueOf(type) == Proxy.Type.SOCKS) {
+ proxy = ProxyConfig.socks(host, portValue, username, password);
+ } else {
+ proxy = ProxyConfig.http(host, portValue, username, password);
+ }
+ }
+ UserPreferences.setProxyConfig(proxy);
+ }
+
private final TextWatcher requireTestOnChange = new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/RemoveFeedDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/RemoveFeedDialog.java
index 9fcf8be69..23c032248 100644
--- a/app/src/main/java/de/danoeh/antennapod/dialog/RemoveFeedDialog.java
+++ b/app/src/main/java/de/danoeh/antennapod/dialog/RemoveFeedDialog.java
@@ -19,18 +19,18 @@ import io.reactivex.schedulers.Schedulers;
public class RemoveFeedDialog {
private static final String TAG = "RemoveFeedDialog";
- public static void show(Context context, Feed feed, Runnable onSuccess) {
+ public static void show(Context context, Feed feed) {
List<Feed> feeds = Collections.singletonList(feed);
String message = getMessageId(context, feeds);
- showDialog(context, feeds, message, onSuccess);
+ showDialog(context, feeds, message);
}
- public static void show(Context context, List<Feed> feeds, Runnable onSuccess) {
+ public static void show(Context context, List<Feed> feeds) {
String message = getMessageId(context, feeds);
- showDialog(context, feeds, message, onSuccess);
+ showDialog(context, feeds, message);
}
- private static void showDialog(Context context, List<Feed> feeds, String message, Runnable onSuccess) {
+ private static void showDialog(Context context, List<Feed> feeds, String message) {
ConfirmationDialog dialog = new ConfirmationDialog(context, R.string.remove_feed_label, message) {
@Override
public void onConfirmButtonPressed(DialogInterface clickedDialog) {
@@ -42,20 +42,16 @@ public class RemoveFeedDialog {
progressDialog.setCancelable(false);
progressDialog.show();
- Completable.fromCallable(() -> {
+ Completable.fromAction(() -> {
for (Feed feed : feeds) {
DBWriter.deleteFeed(context, feed.getId()).get();
}
- return null;
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
() -> {
Log.d(TAG, "Feed(s) deleted");
- if (onSuccess != null) {
- onSuccess.run();
- }
progressDialog.dismiss();
}, error -> {
Log.e(TAG, Log.getStackTraceString(error));
diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/RenameFeedDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/RenameFeedDialog.java
deleted file mode 100644
index 42a854cd8..000000000
--- a/app/src/main/java/de/danoeh/antennapod/dialog/RenameFeedDialog.java
+++ /dev/null
@@ -1,50 +0,0 @@
-package de.danoeh.antennapod.dialog;
-
-import android.app.Activity;
-
-import java.lang.ref.WeakReference;
-
-import android.view.View;
-import androidx.appcompat.app.AlertDialog;
-import de.danoeh.antennapod.R;
-import de.danoeh.antennapod.model.feed.Feed;
-import de.danoeh.antennapod.core.storage.DBWriter;
-import de.danoeh.antennapod.databinding.EditTextDialogBinding;
-
-public class RenameFeedDialog {
-
- private final WeakReference<Activity> activityRef;
- private final Feed feed;
-
- public RenameFeedDialog(Activity activity, Feed feed) {
- this.activityRef = new WeakReference<>(activity);
- this.feed = feed;
- }
-
- public void show() {
- Activity activity = activityRef.get();
- if(activity == null) {
- return;
- }
-
- View content = View.inflate(activity, R.layout.edit_text_dialog, null);
- EditTextDialogBinding alertViewBinding = EditTextDialogBinding.bind(content);
-
- alertViewBinding.urlEditText.setText(feed.getTitle());
- AlertDialog dialog = new AlertDialog.Builder(activity)
- .setView(content)
- .setTitle(de.danoeh.antennapod.core.R.string.rename_feed_label)
- .setPositiveButton(android.R.string.ok, (d, input) -> {
- feed.setCustomTitle(alertViewBinding.urlEditText.getText().toString());
- DBWriter.setFeedCustomTitle(feed);
- })
- .setNeutralButton(de.danoeh.antennapod.core.R.string.reset, null)
- .setNegativeButton(de.danoeh.antennapod.core.R.string.cancel_label, null)
- .show();
-
- // To prevent cancelling the dialog on button click
- dialog.getButton(AlertDialog.BUTTON_NEUTRAL).setOnClickListener(
- (view) -> alertViewBinding.urlEditText.setText(feed.getFeedTitle()));
- }
-
-}
diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/RenameItemDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/RenameItemDialog.java
new file mode 100644
index 000000000..2f9516e0c
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/dialog/RenameItemDialog.java
@@ -0,0 +1,81 @@
+package de.danoeh.antennapod.dialog;
+
+import android.app.Activity;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.List;
+
+import android.view.View;
+import androidx.appcompat.app.AlertDialog;
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.core.storage.NavDrawerData;
+import de.danoeh.antennapod.model.feed.Feed;
+import de.danoeh.antennapod.core.storage.DBWriter;
+import de.danoeh.antennapod.databinding.EditTextDialogBinding;
+import de.danoeh.antennapod.model.feed.FeedPreferences;
+
+public class RenameItemDialog {
+
+ private final WeakReference<Activity> activityRef;
+ private Feed feed = null;
+ private NavDrawerData.DrawerItem drawerItem = null;
+
+ public RenameItemDialog(Activity activity, Feed feed) {
+ this.activityRef = new WeakReference<>(activity);
+ this.feed = feed;
+ }
+
+ public RenameItemDialog(Activity activity, NavDrawerData.DrawerItem drawerItem) {
+ this.activityRef = new WeakReference<>(activity);
+ this.drawerItem = drawerItem;
+ }
+
+ public void show() {
+ Activity activity = activityRef.get();
+ if (activity == null) {
+ return;
+ }
+
+ View content = View.inflate(activity, R.layout.edit_text_dialog, null);
+ EditTextDialogBinding alertViewBinding = EditTextDialogBinding.bind(content);
+ String title = feed != null ? feed.getTitle() : drawerItem.getTitle();
+
+ alertViewBinding.urlEditText.setText(title);
+ AlertDialog dialog = new AlertDialog.Builder(activity)
+ .setView(content)
+ .setTitle(feed != null ? R.string.rename_feed_label : R.string.rename_tag_label)
+ .setPositiveButton(android.R.string.ok, (d, input) -> {
+ String newTitle = alertViewBinding.urlEditText.getText().toString();
+ if (feed != null) {
+ feed.setCustomTitle(newTitle);
+ DBWriter.setFeedCustomTitle(feed);
+ } else {
+ renameTag(newTitle);
+ }
+ })
+ .setNeutralButton(de.danoeh.antennapod.core.R.string.reset, null)
+ .setNegativeButton(de.danoeh.antennapod.core.R.string.cancel_label, null)
+ .show();
+
+ // To prevent cancelling the dialog on button click
+ dialog.getButton(AlertDialog.BUTTON_NEUTRAL).setOnClickListener(
+ (view) -> alertViewBinding.urlEditText.setText(title));
+ }
+
+ private void renameTag(String title) {
+ if (NavDrawerData.DrawerItem.Type.TAG == drawerItem.type) {
+ List<FeedPreferences> feedPreferences = new ArrayList<>();
+ for (NavDrawerData.DrawerItem item : ((NavDrawerData.TagDrawerItem) drawerItem).children) {
+ feedPreferences.add(((NavDrawerData.FeedDrawerItem) item).feed.getPreferences());
+ }
+
+ for (FeedPreferences preferences : feedPreferences) {
+ preferences.getTags().remove(drawerItem.getTitle());
+ preferences.getTags().add(title);
+ DBWriter.setFeedPreferences(preferences);
+ }
+ }
+ }
+
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/SleepTimerDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/SleepTimerDialog.java
index 691bd65e8..8cd34b5f8 100644
--- a/app/src/main/java/de/danoeh/antennapod/dialog/SleepTimerDialog.java
+++ b/app/src/main/java/de/danoeh/antennapod/dialog/SleepTimerDialog.java
@@ -18,20 +18,17 @@ import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.DialogFragment;
import com.google.android.material.snackbar.Snackbar;
import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.event.playback.SleepTimerUpdatedEvent;
import de.danoeh.antennapod.core.preferences.SleepTimerPreferences;
import de.danoeh.antennapod.core.service.playback.PlaybackService;
import de.danoeh.antennapod.core.util.Converter;
import de.danoeh.antennapod.core.util.playback.PlaybackController;
-import io.reactivex.Observable;
-import io.reactivex.android.schedulers.AndroidSchedulers;
-import io.reactivex.disposables.Disposable;
-
-import java.util.concurrent.TimeUnit;
+import org.greenrobot.eventbus.EventBus;
+import org.greenrobot.eventbus.Subscribe;
+import org.greenrobot.eventbus.ThreadMode;
public class SleepTimerDialog extends DialogFragment {
private PlaybackController controller;
- private Disposable timeUpdater;
-
private EditText etxtTime;
private Spinner spTimeUnit;
private LinearLayout timeSetup;
@@ -47,19 +44,11 @@ public class SleepTimerDialog extends DialogFragment {
super.onStart();
controller = new PlaybackController(getActivity()) {
@Override
- public void onSleepTimerUpdate() {
- updateTime();
- }
-
- @Override
public void loadMediaInfo() {
- updateTime();
}
};
controller.init();
- timeUpdater = Observable.interval(1, TimeUnit.SECONDS)
- .observeOn(AndroidSchedulers.mainThread())
- .subscribe(tick -> updateTime());
+ EventBus.getDefault().register(this);
}
@Override
@@ -68,9 +57,7 @@ public class SleepTimerDialog extends DialogFragment {
if (controller != null) {
controller.release();
}
- if (timeUpdater != null) {
- timeUpdater.dispose();
- }
+ EventBus.getDefault().unregister(this);
}
@NonNull
@@ -86,6 +73,7 @@ public class SleepTimerDialog extends DialogFragment {
spTimeUnit = content.findViewById(R.id.spTimeUnit);
timeSetup = content.findViewById(R.id.timeSetup);
timeDisplay = content.findViewById(R.id.timeDisplay);
+ timeDisplay.setVisibility(View.GONE);
time = content.findViewById(R.id.time);
Button extendSleepFiveMinutesButton = content.findViewById(R.id.extendSleepFiveMinutesButton);
extendSleepFiveMinutesButton.setText(getString(R.string.extend_sleep_timer_label, 5));
@@ -170,13 +158,12 @@ public class SleepTimerDialog extends DialogFragment {
return builder.create();
}
- private void updateTime() {
- if (controller == null) {
- return;
- }
- timeSetup.setVisibility(controller.sleepTimerActive() ? View.GONE : View.VISIBLE);
- timeDisplay.setVisibility(controller.sleepTimerActive() ? View.VISIBLE : View.GONE);
- time.setText(Converter.getDurationStringLong((int) controller.getSleepTimerTimeLeft()));
+ @Subscribe(threadMode = ThreadMode.MAIN)
+ @SuppressWarnings("unused")
+ public void timerUpdated(SleepTimerUpdatedEvent event) {
+ timeDisplay.setVisibility(event.isOver() || event.isCancelled() ? View.GONE : View.VISIBLE);
+ timeSetup.setVisibility(event.isOver() || event.isCancelled() ? View.VISIBLE : View.GONE);
+ time.setText(Converter.getDurationStringLong((int) event.getTimeLeft()));
}
private void closeKeyboard(View content) {
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 29172bb5e..9e524188f 100644
--- a/app/src/main/java/de/danoeh/antennapod/dialog/SubscriptionsFilterDialog.java
+++ b/app/src/main/java/de/danoeh/antennapod/dialog/SubscriptionsFilterDialog.java
@@ -16,7 +16,7 @@ import java.util.HashSet;
import java.util.Set;
import de.danoeh.antennapod.R;
-import de.danoeh.antennapod.core.event.UnreadItemsUpdateEvent;
+import de.danoeh.antennapod.event.UnreadItemsUpdateEvent;
import de.danoeh.antennapod.core.feed.SubscriptionsFilter;
import de.danoeh.antennapod.core.feed.SubscriptionsFilterGroup;
import de.danoeh.antennapod.core.preferences.UserPreferences;
diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/TagSettingsDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/TagSettingsDialog.java
index 8ef01590f..8f5f1b802 100644
--- a/app/src/main/java/de/danoeh/antennapod/dialog/TagSettingsDialog.java
+++ b/app/src/main/java/de/danoeh/antennapod/dialog/TagSettingsDialog.java
@@ -27,7 +27,9 @@ import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
import java.util.ArrayList;
+import java.util.HashSet;
import java.util.List;
+import java.util.Set;
public class TagSettingsDialog extends DialogFragment {
public static final String TAG = "TagSettingsDialog";
@@ -36,10 +38,10 @@ public class TagSettingsDialog extends DialogFragment {
private EditTagsDialogBinding viewBinding;
private TagSelectionAdapter adapter;
- public static TagSettingsDialog newInstance(FeedPreferences preferences) {
+ public static TagSettingsDialog newInstance(List<FeedPreferences> preferencesList) {
TagSettingsDialog fragment = new TagSettingsDialog();
Bundle args = new Bundle();
- args.putSerializable(ARG_FEED_PREFERENCES, preferences);
+ args.putSerializable(ARG_FEED_PREFERENCES, new ArrayList<>(preferencesList));
fragment.setArguments(args);
return fragment;
}
@@ -47,8 +49,14 @@ public class TagSettingsDialog extends DialogFragment {
@NonNull
@Override
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
- FeedPreferences preferences = (FeedPreferences) getArguments().getSerializable(ARG_FEED_PREFERENCES);
- displayedTags = new ArrayList<>(preferences.getTags());
+ ArrayList<FeedPreferences> feedPreferencesList =
+ (ArrayList<FeedPreferences>) getArguments().getSerializable(ARG_FEED_PREFERENCES);
+ Set<String> commonTags = new HashSet<>(feedPreferencesList.get(0).getTags());
+
+ for (FeedPreferences preference : feedPreferencesList) {
+ commonTags.retainAll(preference.getTags());
+ }
+ displayedTags = new ArrayList<>(commonTags);
displayedTags.remove(FeedPreferences.TAG_ROOT);
viewBinding = EditTagsDialogBinding.inflate(getLayoutInflater());
@@ -57,7 +65,7 @@ public class TagSettingsDialog extends DialogFragment {
adapter = new TagSelectionAdapter();
adapter.setHasStableIds(true);
viewBinding.tagsRecycler.setAdapter(adapter);
- viewBinding.rootFolderCheckbox.setChecked(preferences.getTags().contains(FeedPreferences.TAG_ROOT));
+ viewBinding.rootFolderCheckbox.setChecked(commonTags.contains(FeedPreferences.TAG_ROOT));
viewBinding.newTagButton.setOnClickListener(v ->
addTag(viewBinding.newTagEditText.getText().toString().trim()));
@@ -73,17 +81,16 @@ public class TagSettingsDialog extends DialogFragment {
}
});
+ if (feedPreferencesList.size() > 1) {
+ viewBinding.commonTagsInfo.setVisibility(View.VISIBLE);
+ }
+
AlertDialog.Builder dialog = new AlertDialog.Builder(getContext());
dialog.setView(viewBinding.getRoot());
- dialog.setTitle(R.string.feed_folders_label);
+ dialog.setTitle(R.string.feed_tags_label);
dialog.setPositiveButton(android.R.string.ok, (d, input) -> {
addTag(viewBinding.newTagEditText.getText().toString().trim());
- preferences.getTags().clear();
- preferences.getTags().addAll(displayedTags);
- if (viewBinding.rootFolderCheckbox.isChecked()) {
- preferences.getTags().add(FeedPreferences.TAG_ROOT);
- }
- DBWriter.setFeedPreferences(preferences);
+ updatePreferencesTags(feedPreferencesList, commonTags);
});
dialog.setNegativeButton(R.string.cancel_label, null);
return dialog.create();
@@ -96,7 +103,7 @@ public class TagSettingsDialog extends DialogFragment {
List<NavDrawerData.DrawerItem> items = data.items;
List<String> folders = new ArrayList<String>();
for (NavDrawerData.DrawerItem item : items) {
- if (item.type == NavDrawerData.DrawerItem.Type.FOLDER) {
+ if (item.type == NavDrawerData.DrawerItem.Type.TAG) {
folders.add(item.getTitle());
}
}
@@ -123,6 +130,17 @@ public class TagSettingsDialog extends DialogFragment {
adapter.notifyDataSetChanged();
}
+ private void updatePreferencesTags(List<FeedPreferences> feedPreferencesList, Set<String> commonTags) {
+ if (viewBinding.rootFolderCheckbox.isChecked()) {
+ displayedTags.add(FeedPreferences.TAG_ROOT);
+ }
+ for (FeedPreferences preferences : feedPreferencesList) {
+ preferences.getTags().removeAll(commonTags);
+ preferences.getTags().addAll(displayedTags);
+ DBWriter.setFeedPreferences(preferences);
+ }
+ }
+
public class TagSelectionAdapter extends RecyclerView.Adapter<TagSelectionAdapter.ViewHolder> {
@Override
diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/VariableSpeedDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/VariableSpeedDialog.java
index def2e56a7..2bce73b79 100644
--- a/app/src/main/java/de/danoeh/antennapod/dialog/VariableSpeedDialog.java
+++ b/app/src/main/java/de/danoeh/antennapod/dialog/VariableSpeedDialog.java
@@ -1,6 +1,5 @@
package de.danoeh.antennapod.dialog;
-import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
@@ -15,10 +14,14 @@ import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
import com.google.android.material.chip.Chip;
import com.google.android.material.snackbar.Snackbar;
import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.event.playback.SpeedChangedEvent;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.util.playback.PlaybackController;
import de.danoeh.antennapod.view.ItemOffsetDecoration;
import de.danoeh.antennapod.view.PlaybackSpeedSeekBar;
+import org.greenrobot.eventbus.EventBus;
+import org.greenrobot.eventbus.Subscribe;
+import org.greenrobot.eventbus.ThreadMode;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
@@ -47,22 +50,12 @@ public class VariableSpeedDialog extends BottomSheetDialogFragment {
super.onStart();
controller = new PlaybackController(getActivity()) {
@Override
- public void onPlaybackSpeedChange() {
- updateSpeed();
- }
-
- @Override
public void loadMediaInfo() {
- updateSpeed();
+ updateSpeed(new SpeedChangedEvent(controller.getCurrentPlaybackSpeedMultiplier()));
}
};
controller.init();
- speedSeekBar.setController(controller);
- }
-
- private void updateSpeed() {
- speedSeekBar.updateSpeed();
- addCurrentSpeedChip.setText(speedFormat.format(controller.getCurrentPlaybackSpeedMultiplier()));
+ EventBus.getDefault().register(this);
}
@Override
@@ -70,6 +63,13 @@ public class VariableSpeedDialog extends BottomSheetDialogFragment {
super.onStop();
controller.release();
controller = null;
+ EventBus.getDefault().unregister(this);
+ }
+
+ @Subscribe(threadMode = ThreadMode.MAIN)
+ public void updateSpeed(SpeedChangedEvent event) {
+ speedSeekBar.updateSpeed(event.getNewSpeed());
+ addCurrentSpeedChip.setText(speedFormat.format(event.getNewSpeed()));
}
@Nullable
@@ -78,6 +78,11 @@ public class VariableSpeedDialog extends BottomSheetDialogFragment {
@Nullable Bundle savedInstanceState) {
View root = View.inflate(getContext(), R.layout.speed_select_dialog, null);
speedSeekBar = root.findViewById(R.id.speed_seek_bar);
+ speedSeekBar.setProgressChangedListener(multiplier -> {
+ if (controller != null) {
+ controller.setPlaybackSpeed(multiplier);
+ }
+ });
RecyclerView selectedSpeedsGrid = root.findViewById(R.id.selected_speeds_grid);
selectedSpeedsGrid.setLayoutManager(new GridLayoutManager(getContext(), 3));
selectedSpeedsGrid.addItemDecoration(new ItemOffsetDecoration(getContext(), 4));
@@ -112,9 +117,7 @@ public class VariableSpeedDialog extends BottomSheetDialogFragment {
@NonNull
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
Chip chip = new Chip(getContext());
- if (Build.VERSION.SDK_INT >= 17) {
- chip.setTextAlignment(View.TEXT_ALIGNMENT_CENTER);
- }
+ chip.setTextAlignment(View.TEXT_ALIGNMENT_CENTER);
return new ViewHolder(chip);
}
diff --git a/app/src/main/java/de/danoeh/antennapod/discovery/GpodnetPodcastSearcher.java b/app/src/main/java/de/danoeh/antennapod/discovery/GpodnetPodcastSearcher.java
index f97c1c7ab..340783208 100644
--- a/app/src/main/java/de/danoeh/antennapod/discovery/GpodnetPodcastSearcher.java
+++ b/app/src/main/java/de/danoeh/antennapod/discovery/GpodnetPodcastSearcher.java
@@ -1,6 +1,6 @@
package de.danoeh.antennapod.discovery;
-import de.danoeh.antennapod.core.preferences.GpodnetPreferences;
+import de.danoeh.antennapod.core.sync.SynchronizationCredentials;
import de.danoeh.antennapod.core.service.download.AntennapodHttpClient;
import de.danoeh.antennapod.net.sync.gpoddernet.GpodnetService;
import de.danoeh.antennapod.net.sync.gpoddernet.GpodnetServiceException;
@@ -18,8 +18,8 @@ public class GpodnetPodcastSearcher implements PodcastSearcher {
return Single.create((SingleOnSubscribe<List<PodcastSearchResult>>) subscriber -> {
try {
GpodnetService service = new GpodnetService(AntennapodHttpClient.getHttpClient(),
- GpodnetPreferences.getHosturl(), GpodnetPreferences.getDeviceID(),
- GpodnetPreferences.getUsername(), GpodnetPreferences.getPassword());
+ SynchronizationCredentials.getHosturl(), SynchronizationCredentials.getDeviceID(),
+ SynchronizationCredentials.getUsername(), SynchronizationCredentials.getPassword());
List<GpodnetPodcast> gpodnetPodcasts = service.searchPodcasts(query, 0);
List<PodcastSearchResult> results = new ArrayList<>();
for (GpodnetPodcast podcast : gpodnetPodcasts) {
diff --git a/app/src/main/java/de/danoeh/antennapod/discovery/ItunesPodcastSearcher.java b/app/src/main/java/de/danoeh/antennapod/discovery/ItunesPodcastSearcher.java
index 6e894176f..5f3dd5f61 100644
--- a/app/src/main/java/de/danoeh/antennapod/discovery/ItunesPodcastSearcher.java
+++ b/app/src/main/java/de/danoeh/antennapod/discovery/ItunesPodcastSearcher.java
@@ -17,9 +17,12 @@ import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
public class ItunesPodcastSearcher implements PodcastSearcher {
private static final String ITUNES_API_URL = "https://itunes.apple.com/search?media=podcast&term=%s";
+ private static final String PATTERN_BY_ID = ".*/podcasts\\.apple\\.com/.*/podcast/.*/id(\\d+).*";
public ItunesPodcastSearcher() {
}
@@ -70,9 +73,12 @@ public class ItunesPodcastSearcher implements PodcastSearcher {
@Override
public Single<String> lookupUrl(String url) {
+ Pattern pattern = Pattern.compile(PATTERN_BY_ID);
+ Matcher matcher = pattern.matcher(url);
+ final String lookupUrl = matcher.find() ? ("https://itunes.apple.com/lookup?id=" + matcher.group(1)) : url;
return Single.create(emitter -> {
OkHttpClient client = AntennapodHttpClient.getHttpClient();
- Request.Builder httpReq = new Request.Builder().url(url);
+ Request.Builder httpReq = new Request.Builder().url(lookupUrl);
try {
Response response = client.newCall(httpReq.build()).execute();
if (response.isSuccessful()) {
@@ -92,7 +98,7 @@ public class ItunesPodcastSearcher implements PodcastSearcher {
@Override
public boolean urlNeedsLookup(String url) {
- return url.contains("itunes.apple.com");
+ return url.contains("itunes.apple.com") || url.matches(PATTERN_BY_ID);
}
@Override
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 64e7f161e..8c01a4563 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,5 @@
package de.danoeh.antennapod.fragment;
-import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.ClipboardManager;
import android.content.Context;
@@ -12,9 +11,14 @@ import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
+import android.view.inputmethod.InputMethodManager;
+import androidx.activity.result.ActivityResultLauncher;
+import androidx.activity.result.contract.ActivityResultContracts;
+import androidx.activity.result.contract.ActivityResultContracts.GetContent;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.Toolbar;
import androidx.documentfile.provider.DocumentFile;
@@ -48,14 +52,17 @@ import java.util.Collections;
public class AddFeedFragment extends Fragment {
public static final String TAG = "AddFeedFragment";
- private static final int REQUEST_CODE_CHOOSE_OPML_IMPORT_PATH = 1;
- private static final int REQUEST_CODE_ADD_LOCAL_FOLDER = 2;
private static final String KEY_UP_ARROW = "up_arrow";
private AddfeedBinding viewBinding;
private MainActivity activity;
private boolean displayUpArrow;
+ private final ActivityResultLauncher<String> chooseOpmlImportPathLauncher =
+ registerForActivityResult(new GetContent(), this::chooseOpmlImportPathResult);
+ private final ActivityResultLauncher<Uri> addLocalFolderLauncher =
+ registerForActivityResult(new AddLocalFolder(), this::addLocalFolderResult);
+
@Override
@Nullable
public View onCreateView(@NonNull LayoutInflater inflater,
@@ -91,10 +98,7 @@ public class AddFeedFragment extends Fragment {
viewBinding.opmlImportButton.setOnClickListener(v -> {
try {
- Intent intentGetContentAction = new Intent(Intent.ACTION_GET_CONTENT);
- intentGetContentAction.addCategory(Intent.CATEGORY_OPENABLE);
- intentGetContentAction.setType("*/*");
- startActivityForResult(intentGetContentAction, REQUEST_CODE_CHOOSE_OPML_IMPORT_PATH);
+ chooseOpmlImportPathLauncher.launch("*/*");
} catch (ActivityNotFoundException e) {
e.printStackTrace();
((MainActivity) getActivity())
@@ -107,9 +111,7 @@ public class AddFeedFragment extends Fragment {
return;
}
try {
- Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
- intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
- startActivityForResult(intent, REQUEST_CODE_ADD_LOCAL_FOLDER);
+ addLocalFolderLauncher.launch(null);
} catch (ActivityNotFoundException e) {
e.printStackTrace();
((MainActivity) getActivity())
@@ -157,6 +159,10 @@ public class AddFeedFragment extends Fragment {
}
private void performSearch() {
+ viewBinding.combinedFeedSearchEditText.clearFocus();
+ InputMethodManager in = (InputMethodManager)
+ getActivity().getSystemService(Context.INPUT_METHOD_SERVICE);
+ in.hideSoftInputFromWindow(viewBinding.combinedFeedSearchEditText.getWindowToken(), 0);
String query = viewBinding.combinedFeedSearchEditText.getText().toString();
if (query.matches("http[s]?://.*")) {
addUrl(query);
@@ -171,22 +177,23 @@ public class AddFeedFragment extends Fragment {
setRetainInstance(true);
}
- @Override
- public void onActivityResult(int requestCode, int resultCode, Intent data) {
- if (resultCode != Activity.RESULT_OK || data == null) {
+ private void chooseOpmlImportPathResult(final Uri uri) {
+ if (uri == null) {
return;
}
- Uri uri = data.getData();
-
- if (requestCode == REQUEST_CODE_CHOOSE_OPML_IMPORT_PATH) {
- Intent intent = new Intent(getContext(), OpmlImportActivity.class);
- intent.setData(uri);
- startActivity(intent);
- } else if (requestCode == REQUEST_CODE_ADD_LOCAL_FOLDER) {
- Observable.fromCallable(() -> addLocalFolder(uri))
- .subscribeOn(Schedulers.io())
- .observeOn(AndroidSchedulers.mainThread())
- .subscribe(
+ final Intent intent = new Intent(getContext(), OpmlImportActivity.class);
+ intent.setData(uri);
+ startActivity(intent);
+ }
+
+ private void addLocalFolderResult(final Uri uri) {
+ if (uri == null) {
+ return;
+ }
+ Observable.fromCallable(() -> addLocalFolder(uri))
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(
feed -> {
Fragment fragment = FeedItemlistFragment.newInstance(feed.getId());
((MainActivity) getActivity()).loadChildFragment(fragment);
@@ -195,7 +202,6 @@ public class AddFeedFragment extends Fragment {
((MainActivity) getActivity())
.showSnackbarAbovePlayer(error.getLocalizedMessage(), Snackbar.LENGTH_LONG);
});
- }
}
private Feed addLocalFolder(Uri uri) throws DownloadRequestException {
@@ -219,4 +225,14 @@ public class AddFeedFragment extends Fragment {
DBTasks.forceRefreshFeed(getContext(), fromDatabase, true);
return fromDatabase;
}
+
+ private static class AddLocalFolder extends ActivityResultContracts.OpenDocumentTree {
+ @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
+ @NonNull
+ @Override
+ public Intent createIntent(@NonNull final Context context, @Nullable final Uri input) {
+ return super.createIntent(context, input)
+ .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ }
+ }
}
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/AudioPlayerFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/AudioPlayerFragment.java
index 168133c7a..95e2eb1aa 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/AudioPlayerFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/AudioPlayerFragment.java
@@ -25,6 +25,14 @@ import androidx.viewpager2.widget.ViewPager2;
import com.google.android.material.bottomsheet.BottomSheetBehavior;
import com.google.android.material.snackbar.Snackbar;
+import de.danoeh.antennapod.core.service.playback.PlaybackService;
+import de.danoeh.antennapod.core.util.playback.PlaybackController;
+import de.danoeh.antennapod.event.playback.BufferUpdateEvent;
+import de.danoeh.antennapod.event.playback.PlaybackServiceEvent;
+import de.danoeh.antennapod.event.PlayerErrorEvent;
+import de.danoeh.antennapod.event.playback.SleepTimerUpdatedEvent;
+import de.danoeh.antennapod.event.playback.SpeedChangedEvent;
+import de.danoeh.antennapod.playback.cast.CastEnabledActivity;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
@@ -34,25 +42,20 @@ import java.text.NumberFormat;
import java.util.List;
import de.danoeh.antennapod.R;
-import de.danoeh.antennapod.activity.CastEnabledActivity;
import de.danoeh.antennapod.activity.MainActivity;
-import de.danoeh.antennapod.core.event.FavoritesEvent;
-import de.danoeh.antennapod.core.event.PlaybackPositionEvent;
-import de.danoeh.antennapod.core.event.ServiceEvent;
+import de.danoeh.antennapod.event.FavoritesEvent;
+import de.danoeh.antennapod.event.playback.PlaybackPositionEvent;
import de.danoeh.antennapod.model.feed.Chapter;
-import de.danoeh.antennapod.core.event.UnreadItemsUpdateEvent;
+import de.danoeh.antennapod.event.UnreadItemsUpdateEvent;
import de.danoeh.antennapod.model.feed.FeedItem;
import de.danoeh.antennapod.model.feed.FeedMedia;
import de.danoeh.antennapod.core.feed.util.PlaybackSpeedUtils;
import de.danoeh.antennapod.core.preferences.UserPreferences;
-import de.danoeh.antennapod.core.service.playback.PlaybackService;
import de.danoeh.antennapod.core.util.ChapterUtils;
import de.danoeh.antennapod.core.util.Converter;
import de.danoeh.antennapod.core.util.IntentUtils;
import de.danoeh.antennapod.core.util.TimeSpeedConverter;
-import de.danoeh.antennapod.core.util.playback.MediaPlayerError;
import de.danoeh.antennapod.model.playback.Playable;
-import de.danoeh.antennapod.core.util.playback.PlaybackController;
import de.danoeh.antennapod.dialog.PlaybackControlsDialog;
import de.danoeh.antennapod.dialog.SkipPreferenceDialog;
import de.danoeh.antennapod.dialog.SleepTimerDialog;
@@ -224,8 +227,8 @@ public class AudioPlayerFragment extends Fragment implements
}
@Subscribe(threadMode = ThreadMode.MAIN)
- public void onPlaybackServiceChanged(ServiceEvent event) {
- if (event.action == ServiceEvent.Action.SERVICE_SHUT_DOWN) {
+ public void onPlaybackServiceChanged(PlaybackServiceEvent event) {
+ if (event.action == PlaybackServiceEvent.Action.SERVICE_SHUT_DOWN) {
((MainActivity) getActivity()).getBottomSheet().setState(BottomSheetBehavior.STATE_COLLAPSED);
}
}
@@ -243,14 +246,11 @@ public class AudioPlayerFragment extends Fragment implements
});
}
- protected void updatePlaybackSpeedButton(Playable media) {
- if (butPlaybackSpeed == null || controller == null) {
- return;
- }
- float speed = PlaybackSpeedUtils.getCurrentPlaybackSpeed(media);
- String speedStr = new DecimalFormat("0.00").format(speed);
+ @Subscribe(threadMode = ThreadMode.MAIN)
+ public void updatePlaybackSpeedButton(SpeedChangedEvent event) {
+ String speedStr = new DecimalFormat("0.00").format(event.getNewSpeed());
txtvPlaybackSpeed.setText(speedStr);
- butPlaybackSpeed.setSpeed(speed);
+ butPlaybackSpeed.setSpeed(event.getNewSpeed());
}
private void loadMediaInfo(boolean includingChapters) {
@@ -282,47 +282,6 @@ public class AudioPlayerFragment extends Fragment implements
private PlaybackController newPlaybackController() {
return new PlaybackController(getActivity()) {
@Override
- public void onBufferStart() {
- progressIndicator.setVisibility(View.VISIBLE);
- }
-
- @Override
- public void onBufferEnd() {
- progressIndicator.setVisibility(View.GONE);
- }
-
- @Override
- public void onBufferUpdate(float progress) {
- if (isStreaming()) {
- sbPosition.setSecondaryProgress((int) (progress * sbPosition.getMax()));
- } else {
- sbPosition.setSecondaryProgress(0);
- }
- }
-
- @Override
- public void handleError(int code) {
- final AlertDialog.Builder errorDialog = new AlertDialog.Builder(getContext());
- errorDialog.setTitle(R.string.error_label);
- errorDialog.setMessage(MediaPlayerError.getErrorString(getContext(), code));
- errorDialog.setPositiveButton(android.R.string.ok, (dialog, which) ->
- ((MainActivity) getActivity()).getBottomSheet().setState(BottomSheetBehavior.STATE_COLLAPSED));
- if (!UserPreferences.useExoplayer()) {
- errorDialog.setNeutralButton(R.string.media_player_switch_to_exoplayer, (dialog, which) -> {
- UserPreferences.enableExoplayer();
- ((MainActivity) getActivity()).showSnackbarAbovePlayer(
- R.string.media_player_switched_to_exoplayer, Snackbar.LENGTH_LONG);
- });
- }
- errorDialog.create().show();
- }
-
- @Override
- public void onSleepTimerUpdate() {
- AudioPlayerFragment.this.loadMediaInfo(false);
- }
-
- @Override
protected void updatePlayButtonShowsPlay(boolean showPlay) {
butPlay.setIsShowPlay(showPlay);
}
@@ -336,25 +295,28 @@ public class AudioPlayerFragment extends Fragment implements
public void onPlaybackEnd() {
((MainActivity) getActivity()).getBottomSheet().setState(BottomSheetBehavior.STATE_COLLAPSED);
}
-
- @Override
- public void onPlaybackSpeedChange() {
- updatePlaybackSpeedButton(getMedia());
- }
};
}
private void updateUi(Playable media) {
- if (controller == null) {
+ if (controller == null || media == null) {
return;
}
duration = controller.getDuration();
- updatePosition(new PlaybackPositionEvent(controller.getPosition(), duration));
- updatePlaybackSpeedButton(media);
+ updatePosition(new PlaybackPositionEvent(media.getPosition(), media.getDuration()));
+ updatePlaybackSpeedButton(new SpeedChangedEvent(PlaybackSpeedUtils.getCurrentPlaybackSpeed(media)));
setChapterDividers(media);
setupOptionsMenu(media);
}
+ @Subscribe(threadMode = ThreadMode.MAIN)
+ @SuppressWarnings("unused")
+ public void sleepTimerUpdate(SleepTimerUpdatedEvent event) {
+ if (event.isCancelled() || event.wasJustEnabled()) {
+ AudioPlayerFragment.this.loadMediaInfo(false);
+ }
+ }
+
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -385,6 +347,20 @@ public class AudioPlayerFragment extends Fragment implements
}
@Subscribe(threadMode = ThreadMode.MAIN)
+ @SuppressWarnings("unused")
+ public void bufferUpdate(BufferUpdateEvent event) {
+ if (event.hasStarted()) {
+ progressIndicator.setVisibility(View.VISIBLE);
+ } else if (event.hasEnded()) {
+ progressIndicator.setVisibility(View.GONE);
+ } else if (controller != null && controller.isStreaming()) {
+ sbPosition.setSecondaryProgress((int) (event.getProgress() * sbPosition.getMax()));
+ } else {
+ sbPosition.setSecondaryProgress(0);
+ }
+ }
+
+ @Subscribe(threadMode = ThreadMode.MAIN)
public void updatePosition(PlaybackPositionEvent event) {
if (controller == null || txtvPosition == null || txtvLength == null || sbPosition == null) {
return;
@@ -419,6 +395,23 @@ public class AudioPlayerFragment extends Fragment implements
AudioPlayerFragment.this.loadMediaInfo(false);
}
+ @Subscribe(threadMode = ThreadMode.MAIN)
+ public void mediaPlayerError(PlayerErrorEvent event) {
+ final AlertDialog.Builder errorDialog = new AlertDialog.Builder(getContext());
+ errorDialog.setTitle(R.string.error_label);
+ errorDialog.setMessage(event.getMessage());
+ errorDialog.setPositiveButton(android.R.string.ok, (dialog, which) ->
+ ((MainActivity) getActivity()).getBottomSheet().setState(BottomSheetBehavior.STATE_COLLAPSED));
+ if (!UserPreferences.useExoplayer()) {
+ errorDialog.setNeutralButton(R.string.media_player_switch_to_exoplayer, (dialog, which) -> {
+ UserPreferences.enableExoplayer();
+ ((MainActivity) getActivity()).showSnackbarAbovePlayer(
+ R.string.media_player_switched_to_exoplayer, Snackbar.LENGTH_LONG);
+ });
+ }
+ errorDialog.create().show();
+ }
+
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
if (controller == null || txtvLength == null) {
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 de14f220e..04ad6e2bd 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/ChaptersFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/ChaptersFragment.java
@@ -17,14 +17,14 @@ import androidx.recyclerview.widget.DividerItemDecoration;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
+import de.danoeh.antennapod.playback.base.PlayerStatus;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.adapter.ChaptersListAdapter;
-import de.danoeh.antennapod.core.event.PlaybackPositionEvent;
-import de.danoeh.antennapod.core.service.playback.PlayerStatus;
+import de.danoeh.antennapod.event.playback.PlaybackPositionEvent;
import de.danoeh.antennapod.core.util.ChapterUtils;
import de.danoeh.antennapod.core.util.playback.PlaybackController;
import de.danoeh.antennapod.model.feed.Chapter;
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 6c8baef29..933147378 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/CompletedDownloadsFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/CompletedDownloadsFragment.java
@@ -23,10 +23,10 @@ import de.danoeh.antennapod.adapter.EpisodeItemListAdapter;
import de.danoeh.antennapod.adapter.actionbutton.DeleteActionButton;
import de.danoeh.antennapod.core.event.DownloadEvent;
import de.danoeh.antennapod.core.event.DownloadLogEvent;
-import de.danoeh.antennapod.core.event.FeedItemEvent;
-import de.danoeh.antennapod.core.event.PlaybackPositionEvent;
-import de.danoeh.antennapod.core.event.PlayerStatusEvent;
-import de.danoeh.antennapod.core.event.UnreadItemsUpdateEvent;
+import de.danoeh.antennapod.event.FeedItemEvent;
+import de.danoeh.antennapod.event.playback.PlaybackPositionEvent;
+import de.danoeh.antennapod.event.PlayerStatusEvent;
+import de.danoeh.antennapod.event.UnreadItemsUpdateEvent;
import de.danoeh.antennapod.core.menuhandler.MenuItemUtils;
import de.danoeh.antennapod.fragment.actions.EpisodeMultiSelectActionHandler;
import de.danoeh.antennapod.model.feed.FeedItem;
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/CoverFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/CoverFragment.java
index 8c2203f72..2d448faa8 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/CoverFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/CoverFragment.java
@@ -45,7 +45,7 @@ import org.greenrobot.eventbus.ThreadMode;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MainActivity;
-import de.danoeh.antennapod.core.event.PlaybackPositionEvent;
+import de.danoeh.antennapod.event.playback.PlaybackPositionEvent;
import de.danoeh.antennapod.model.feed.FeedMedia;
import de.danoeh.antennapod.core.feed.util.ImageResourceUtils;
import de.danoeh.antennapod.core.glide.ApGlideSettings;
@@ -110,9 +110,8 @@ public class CoverFragment extends Fragment {
butNextChapter.setColorFilter(colorFilter);
butPrevChapter.setColorFilter(colorFilter);
descriptionIcon.setColorFilter(colorFilter);
- ChaptersFragment chaptersFragment = new ChaptersFragment();
chapterControl.setOnClickListener(v ->
- chaptersFragment.show(getChildFragmentManager(), ChaptersFragment.TAG));
+ new ChaptersFragment().show(getChildFragmentManager(), ChaptersFragment.TAG));
butPrevChapter.setOnClickListener(v -> seekToPrevChapter());
butNextChapter.setOnClickListener(v -> seekToNextChapter());
@@ -156,8 +155,13 @@ public class CoverFragment extends Fragment {
+ "・"
+ "\u00A0"
+ StringUtils.replace(StringUtils.stripToEmpty(pubDateStr), " ", "\u00A0"));
- Intent openFeed = MainActivity.getIntentToOpenFeed(requireContext(), ((FeedMedia) media).getItem().getFeedId());
- txtvPodcastTitle.setOnClickListener(v -> startActivity(openFeed));
+ if (media instanceof FeedMedia) {
+ Intent openFeed = MainActivity.getIntentToOpenFeed(requireContext(),
+ ((FeedMedia) media).getItem().getFeedId());
+ txtvPodcastTitle.setOnClickListener(v -> startActivity(openFeed));
+ } else {
+ txtvPodcastTitle.setOnClickListener(null);
+ }
txtvPodcastTitle.setOnLongClickListener(v -> copyText(media.getFeedTitle()));
txtvEpisodeTitle.setText(media.getEpisodeTitle());
txtvEpisodeTitle.setOnLongClickListener(v -> copyText(media.getEpisodeTitle()));
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 034b111e1..230a0ce0d 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/DiscoveryFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/DiscoveryFragment.java
@@ -23,7 +23,7 @@ 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.core.event.DiscoveryDefaultUpdateEvent;
+import de.danoeh.antennapod.event.DiscoveryDefaultUpdateEvent;
import de.danoeh.antennapod.discovery.ItunesTopListLoader;
import de.danoeh.antennapod.discovery.PodcastSearchResult;
import io.reactivex.disposables.Disposable;
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 ddbf6c078..5602dcb78 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/DownloadLogFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/DownloadLogFragment.java
@@ -113,7 +113,7 @@ public class DownloadLogFragment extends ListFragment {
if (downloadRequest.getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA) {
FeedMedia media = DBReader.getFeedMedia(downloadRequest.getFeedfileId());
FeedItem feedItem = media.getItem();
- feedItem.setAutoDownload(false);
+ feedItem.disableAutoDownload();
DBWriter.setFeedItem(feedItem);
}
} else if (item instanceof DownloadStatus) {
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 7ea76bb8d..7eb0847eb 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/EpisodesListFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/EpisodesListFragment.java
@@ -2,6 +2,7 @@ package de.danoeh.antennapod.fragment;
import android.content.DialogInterface;
import android.os.Bundle;
+import android.view.KeyEvent;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.RecyclerView;
@@ -21,10 +22,10 @@ import android.widget.TextView;
import android.widget.Toast;
import de.danoeh.antennapod.adapter.EpisodeItemListAdapter;
-import de.danoeh.antennapod.core.event.FeedListUpdateEvent;
-import de.danoeh.antennapod.core.event.PlaybackPositionEvent;
-import de.danoeh.antennapod.core.event.PlayerStatusEvent;
-import de.danoeh.antennapod.core.event.UnreadItemsUpdateEvent;
+import de.danoeh.antennapod.event.FeedListUpdateEvent;
+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.view.EpisodeItemListRecyclerView;
import de.danoeh.antennapod.view.viewholder.EpisodeItemViewHolder;
@@ -40,7 +41,7 @@ import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.core.dialog.ConfirmationDialog;
import de.danoeh.antennapod.core.event.DownloadEvent;
import de.danoeh.antennapod.core.event.DownloaderUpdate;
-import de.danoeh.antennapod.core.event.FeedItemEvent;
+import de.danoeh.antennapod.event.FeedItemEvent;
import de.danoeh.antennapod.model.feed.FeedItem;
import de.danoeh.antennapod.core.service.download.DownloadService;
import de.danoeh.antennapod.core.storage.DBWriter;
@@ -321,6 +322,23 @@ public abstract class EpisodesListFragment extends Fragment {
}
}
+ @Subscribe(threadMode = ThreadMode.MAIN)
+ public void onKeyUp(KeyEvent event) {
+ if (!isAdded() || !isVisible() || !isMenuVisible()) {
+ return;
+ }
+ switch (event.getKeyCode()) {
+ case KeyEvent.KEYCODE_T:
+ recyclerView.smoothScrollToPosition(0);
+ break;
+ case KeyEvent.KEYCODE_B:
+ recyclerView.smoothScrollToPosition(listAdapter.getItemCount() - 1);
+ break;
+ default:
+ break;
+ }
+ }
+
protected boolean shouldUpdatedItemRemainInList(FeedItem item) {
return true;
}
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/ExternalPlayerFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/ExternalPlayerFragment.java
index 8e070738c..1e24d62f7 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/ExternalPlayerFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/ExternalPlayerFragment.java
@@ -9,21 +9,23 @@ import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.TextView;
+
+import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import com.bumptech.glide.Glide;
import com.bumptech.glide.request.RequestOptions;
import com.google.android.material.bottomsheet.BottomSheetBehavior;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MainActivity;
-import de.danoeh.antennapod.core.event.PlaybackPositionEvent;
-import de.danoeh.antennapod.core.event.ServiceEvent;
+import de.danoeh.antennapod.event.playback.PlaybackPositionEvent;
+import de.danoeh.antennapod.event.playback.PlaybackServiceEvent;
import de.danoeh.antennapod.model.playback.MediaType;
import de.danoeh.antennapod.core.feed.util.ImageResourceUtils;
import de.danoeh.antennapod.core.glide.ApGlideSettings;
import de.danoeh.antennapod.core.service.playback.PlaybackService;
-import de.danoeh.antennapod.core.service.playback.PlayerStatus;
import de.danoeh.antennapod.model.playback.Playable;
import de.danoeh.antennapod.core.util.playback.PlaybackController;
+import de.danoeh.antennapod.playback.base.PlayerStatus;
import de.danoeh.antennapod.view.PlayButton;
import io.reactivex.Maybe;
import io.reactivex.android.schedulers.AndroidSchedulers;
@@ -77,8 +79,8 @@ public class ExternalPlayerFragment extends Fragment {
}
@Override
- public void onActivityCreated(Bundle savedInstanceState) {
- super.onActivityCreated(savedInstanceState);
+ public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
butPlay.setOnClickListener(v -> {
if (controller == null) {
return;
@@ -97,12 +99,6 @@ public class ExternalPlayerFragment extends Fragment {
private PlaybackController setupPlaybackController() {
return new PlaybackController(getActivity()) {
-
- @Override
- public void onPositionObserverUpdate() {
- ExternalPlayerFragment.this.onPositionObserverUpdate();
- }
-
@Override
protected void updatePlayButtonShowsPlay(boolean showPlay) {
butPlay.setIsShowPlay(showPlay);
@@ -140,13 +136,20 @@ public class ExternalPlayerFragment extends Fragment {
}
@Subscribe(threadMode = ThreadMode.MAIN)
- public void onEventMainThread(PlaybackPositionEvent event) {
- onPositionObserverUpdate();
+ public void onPositionObserverUpdate(PlaybackPositionEvent event) {
+ if (controller == null) {
+ return;
+ } else if (controller.getPosition() == PlaybackService.INVALID_TIME
+ || controller.getDuration() == PlaybackService.INVALID_TIME) {
+ return;
+ }
+ progressBar.setProgress((int)
+ ((double) controller.getPosition() / controller.getDuration() * 100));
}
@Subscribe(threadMode = ThreadMode.MAIN)
- public void onPlaybackServiceChanged(ServiceEvent event) {
- if (event.action == ServiceEvent.Action.SERVICE_SHUT_DOWN) {
+ public void onPlaybackServiceChanged(PlaybackServiceEvent event) {
+ if (event.action == PlaybackServiceEvent.Action.SERVICE_SHUT_DOWN) {
((MainActivity) getActivity()).setPlayerVisible(false);
}
}
@@ -193,7 +196,7 @@ public class ExternalPlayerFragment extends Fragment {
((MainActivity) getActivity()).setPlayerVisible(true);
txtvTitle.setText(media.getEpisodeTitle());
feedName.setText(media.getFeedTitle());
- onPositionObserverUpdate();
+ onPositionObserverUpdate(new PlaybackPositionEvent(media.getPosition(), media.getDuration()));
RequestOptions options = new RequestOptions()
.placeholder(R.color.light_gray)
@@ -218,15 +221,4 @@ public class ExternalPlayerFragment extends Fragment {
((MainActivity) getActivity()).getBottomSheet().setLocked(false);
}
}
-
- private void onPositionObserverUpdate() {
- if (controller == null) {
- return;
- } else if (controller.getPosition() == PlaybackService.INVALID_TIME
- || controller.getDuration() == PlaybackService.INVALID_TIME) {
- return;
- }
- progressBar.setProgress((int)
- ((double) controller.getPosition() / controller.getDuration() * 100));
- }
}
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/FavoriteEpisodesFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/FavoriteEpisodesFragment.java
index 986c417fd..d7bfd404d 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/FavoriteEpisodesFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/FavoriteEpisodesFragment.java
@@ -18,7 +18,7 @@ import org.greenrobot.eventbus.Subscribe;
import java.util.List;
import de.danoeh.antennapod.R;
-import de.danoeh.antennapod.core.event.FavoritesEvent;
+import de.danoeh.antennapod.event.FavoritesEvent;
import de.danoeh.antennapod.model.feed.FeedItem;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.storage.DBWriter;
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 da7e7e633..947b8aa6e 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/FeedInfoFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/FeedInfoFragment.java
@@ -1,6 +1,5 @@
package de.danoeh.antennapod.fragment;
-import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.ClipData;
import android.content.Context;
@@ -10,62 +9,56 @@ import android.graphics.LightingColorFilter;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.appcompat.app.AlertDialog;
-import androidx.appcompat.widget.AppCompatDrawableManager;
-import androidx.appcompat.widget.Toolbar;
-import androidx.documentfile.provider.DocumentFile;
-import androidx.fragment.app.Fragment;
import android.text.TextUtils;
-import android.text.format.Formatter;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
-import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
+import androidx.activity.result.ActivityResultLauncher;
+import androidx.activity.result.contract.ActivityResultContracts;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.appcompat.app.AlertDialog;
+import androidx.appcompat.content.res.AppCompatResources;
+import androidx.appcompat.widget.Toolbar;
+import androidx.documentfile.provider.DocumentFile;
+import androidx.fragment.app.Fragment;
import com.bumptech.glide.Glide;
import com.bumptech.glide.request.RequestOptions;
import com.google.android.material.appbar.AppBarLayout;
import com.google.android.material.appbar.CollapsingToolbarLayout;
import com.google.android.material.snackbar.Snackbar;
import com.joanzapata.iconify.Iconify;
-
-import org.apache.commons.lang3.StringUtils;
-
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.core.dialog.DownloadRequestErrorDialogCreator;
-import de.danoeh.antennapod.model.feed.Feed;
-import de.danoeh.antennapod.model.feed.FeedFunding;
import de.danoeh.antennapod.core.glide.ApGlideSettings;
import de.danoeh.antennapod.core.glide.FastBlurTransformation;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.storage.DBTasks;
import de.danoeh.antennapod.core.storage.DownloadRequestException;
-import de.danoeh.antennapod.core.storage.StatisticsItem;
-import de.danoeh.antennapod.core.util.Converter;
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.view.ToolbarIconTintManager;
import io.reactivex.Completable;
import io.reactivex.Maybe;
import io.reactivex.MaybeOnSubscribe;
-import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
+import org.apache.commons.lang3.StringUtils;
import java.util.ArrayList;
import java.util.HashSet;
-import java.util.List;
-import java.util.Locale;
/**
* Displays information about a feed.
@@ -74,28 +67,22 @@ public class FeedInfoFragment extends Fragment implements Toolbar.OnMenuItemClic
private static final String EXTRA_FEED_ID = "de.danoeh.antennapod.extra.feedId";
private static final String TAG = "FeedInfoActivity";
- private static final int REQUEST_CODE_ADD_LOCAL_FOLDER = 2;
+ private final ActivityResultLauncher<Uri> addLocalFolderLauncher =
+ registerForActivityResult(new AddLocalFolder(), this::addLocalFolderResult);
private Feed feed;
private Disposable disposable;
- private Disposable disposableStatistics;
private ImageView imgvCover;
private TextView txtvTitle;
private TextView txtvDescription;
- private TextView lblStatistics;
- private TextView txtvPodcastTime;
- private TextView txtvPodcastSpace;
- private TextView txtvPodcastEpisodeCount;
private TextView txtvFundingUrl;
private TextView lblSupport;
- private Button btnvOpenStatistics;
private TextView txtvUrl;
private TextView txtvAuthorHeader;
private ImageView imgvBackground;
private View infoContainer;
private View header;
private Toolbar toolbar;
- private ToolbarIconTintManager iconTintManager;
public static FeedInfoFragment newInstance(Feed feed) {
FeedInfoFragment fragment = new FeedInfoFragment();
@@ -133,13 +120,13 @@ public class FeedInfoFragment extends Fragment implements Toolbar.OnMenuItemClic
AppBarLayout appBar = root.findViewById(R.id.appBar);
CollapsingToolbarLayout collapsingToolbar = root.findViewById(R.id.collapsing_toolbar);
- iconTintManager = new ToolbarIconTintManager(getContext(), toolbar, collapsingToolbar) {
+ ToolbarIconTintManager iconTintManager = new ToolbarIconTintManager(getContext(), toolbar, collapsingToolbar) {
@Override
protected void doTint(Context themedContext) {
toolbar.getMenu().findItem(R.id.visit_website_item)
- .setIcon(AppCompatDrawableManager.get().getDrawable(themedContext, R.drawable.ic_web));
+ .setIcon(AppCompatResources.getDrawable(themedContext, R.drawable.ic_web));
toolbar.getMenu().findItem(R.id.share_parent)
- .setIcon(AppCompatDrawableManager.get().getDrawable(themedContext, R.drawable.ic_share));
+ .setIcon(AppCompatResources.getDrawable(themedContext, R.drawable.ic_share));
}
};
iconTintManager.updateTint();
@@ -157,23 +144,20 @@ public class FeedInfoFragment extends Fragment implements Toolbar.OnMenuItemClic
imgvBackground.setColorFilter(new LightingColorFilter(0xff828282, 0x000000));
txtvDescription = root.findViewById(R.id.txtvDescription);
- lblStatistics = root.findViewById(R.id.lblStatistics);
- txtvPodcastSpace = root.findViewById(R.id.txtvPodcastSpaceUsed);
- txtvPodcastEpisodeCount = root.findViewById(R.id.txtvPodcastEpisodeCount);
- txtvPodcastTime = root.findViewById(R.id.txtvPodcastTime);
- btnvOpenStatistics = root.findViewById(R.id.btnvOpenStatistics);
txtvUrl = root.findViewById(R.id.txtvUrl);
lblSupport = root.findViewById(R.id.lblSupport);
txtvFundingUrl = root.findViewById(R.id.txtvFundingUrl);
txtvUrl.setOnClickListener(copyUrlToClipboard);
- btnvOpenStatistics.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View view) {
- StatisticsFragment fragment = new StatisticsFragment();
- ((MainActivity) getActivity()).loadChildFragment(fragment, TransitionEffect.SLIDE);
- }
+ long feedId = getArguments().getLong(EXTRA_FEED_ID);
+ getParentFragmentManager().beginTransaction().replace(R.id.statisticsFragmentContainer,
+ FeedStatisticsFragment.newInstance(feedId, false), "feed_statistics_fragment")
+ .commitAllowingStateLoss();
+
+ root.findViewById(R.id.btnvOpenStatistics).setOnClickListener(view -> {
+ StatisticsFragment fragment = new StatisticsFragment();
+ ((MainActivity) getActivity()).loadChildFragment(fragment, TransitionEffect.SLIDE);
});
return root;
@@ -195,7 +179,6 @@ public class FeedInfoFragment extends Fragment implements Toolbar.OnMenuItemClic
.subscribe(result -> {
feed = result;
showFeed();
- loadStatistics();
}, error -> Log.d(TAG, Log.getStackTraceString(error)), () -> { });
}
@@ -270,53 +253,12 @@ public class FeedInfoFragment extends Fragment implements Toolbar.OnMenuItemClic
refreshToolbarState();
}
- private void loadStatistics() {
- if (disposableStatistics != null) {
- disposableStatistics.dispose();
- }
-
- disposableStatistics =
- Observable.fromCallable(() -> {
- List<StatisticsItem> statisticsData = DBReader.getStatistics();
-
- for (StatisticsItem statisticsItem : statisticsData) {
- if (statisticsItem.feed.getId() == feed.getId()) {
- return statisticsItem;
- }
- }
-
- return null;
- })
- .subscribeOn(Schedulers.io())
- .observeOn(AndroidSchedulers.mainThread())
- .subscribe(result -> {
- txtvPodcastTime.setText(Converter.shortLocalizedDuration(
- getContext(), result.timePlayed));
- txtvPodcastSpace.setText(Formatter.formatShortFileSize(
- getContext(), result.totalDownloadSize));
- txtvPodcastEpisodeCount.setText(String.format(Locale.getDefault(),
- "%d%s", result.episodesDownloadCount,
- getString(R.string.episodes_suffix)));
- }, error -> {
- Log.d(TAG, Log.getStackTraceString(error));
- lblStatistics.setVisibility(View.GONE);
- txtvPodcastSpace.setVisibility(View.GONE);
- txtvPodcastTime.setVisibility(View.GONE);
- txtvPodcastEpisodeCount.setVisibility(View.GONE);
- btnvOpenStatistics.setVisibility(View.GONE);
- });
- }
-
@Override
public void onDestroy() {
super.onDestroy();
if (disposable != null) {
disposable.dispose();
}
-
- if (disposableStatistics != null) {
- disposableStatistics.dispose();
- }
}
private void refreshToolbarState() {
@@ -351,9 +293,7 @@ public class FeedInfoFragment extends Fragment implements Toolbar.OnMenuItemClic
alert.setMessage(R.string.reconnect_local_folder_warning);
alert.setPositiveButton(android.R.string.ok, (dialog, which) -> {
try {
- Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
- intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
- startActivityForResult(intent, REQUEST_CODE_ADD_LOCAL_FOLDER);
+ addLocalFolderLauncher.launch(null);
} catch (ActivityNotFoundException e) {
Log.e(TAG, "No activity found. Should never happen...");
}
@@ -366,16 +306,11 @@ public class FeedInfoFragment extends Fragment implements Toolbar.OnMenuItemClic
return handled;
}
- @Override
- public void onActivityResult(int requestCode, int resultCode, Intent data) {
- if (resultCode != Activity.RESULT_OK || data == null) {
+ private void addLocalFolderResult(final Uri uri) {
+ if (uri == null) {
return;
}
- Uri uri = data.getData();
-
- if (requestCode == REQUEST_CODE_ADD_LOCAL_FOLDER) {
- reconnectLocalFolder(uri);
- }
+ reconnectLocalFolder(uri);
}
private void reconnectLocalFolder(Uri uri) {
@@ -401,4 +336,14 @@ public class FeedInfoFragment extends Fragment implements Toolbar.OnMenuItemClic
error -> ((MainActivity) getActivity())
.showSnackbarAbovePlayer(error.getLocalizedMessage(), Snackbar.LENGTH_LONG));
}
+
+ private static class AddLocalFolder extends ActivityResultContracts.OpenDocumentTree {
+ @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
+ @NonNull
+ @Override
+ public Intent createIntent(@NonNull final Context context, @Nullable final Uri input) {
+ return super.createIntent(context, input)
+ .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ }
+ }
}
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/FeedItemlistFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/FeedItemlistFragment.java
index 0ee60866d..5df8e2ccf 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/FeedItemlistFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/FeedItemlistFragment.java
@@ -22,9 +22,8 @@ import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import androidx.appcompat.widget.AppCompatDrawableManager;
+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;
@@ -52,13 +51,13 @@ import de.danoeh.antennapod.adapter.EpisodeItemListAdapter;
import de.danoeh.antennapod.core.dialog.DownloadRequestErrorDialogCreator;
import de.danoeh.antennapod.core.event.DownloadEvent;
import de.danoeh.antennapod.core.event.DownloaderUpdate;
-import de.danoeh.antennapod.core.event.FavoritesEvent;
-import de.danoeh.antennapod.core.event.FeedItemEvent;
-import de.danoeh.antennapod.core.event.FeedListUpdateEvent;
-import de.danoeh.antennapod.core.event.PlaybackPositionEvent;
-import de.danoeh.antennapod.core.event.PlayerStatusEvent;
-import de.danoeh.antennapod.core.event.QueueEvent;
-import de.danoeh.antennapod.core.event.UnreadItemsUpdateEvent;
+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;
@@ -73,7 +72,7 @@ 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.dialog.RemoveFeedDialog;
-import de.danoeh.antennapod.dialog.RenameFeedDialog;
+import de.danoeh.antennapod.dialog.RenameItemDialog;
import de.danoeh.antennapod.fragment.swipeactions.SwipeActions;
import de.danoeh.antennapod.fragment.actions.EpisodeMultiSelectActionHandler;
import de.danoeh.antennapod.menuhandler.FeedItemMenuHandler;
@@ -88,6 +87,8 @@ 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;
/**
* Displays a list of FeedItems.
@@ -187,13 +188,13 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem
@Override
protected void doTint(Context themedContext) {
toolbar.getMenu().findItem(R.id.sort_items)
- .setIcon(AppCompatDrawableManager.get().getDrawable(themedContext, R.drawable.ic_sort));
+ .setIcon(AppCompatResources.getDrawable(themedContext, R.drawable.ic_sort));
toolbar.getMenu().findItem(R.id.filter_items)
- .setIcon(AppCompatDrawableManager.get().getDrawable(themedContext, R.drawable.ic_filter));
+ .setIcon(AppCompatResources.getDrawable(themedContext, R.drawable.ic_filter));
toolbar.getMenu().findItem(R.id.refresh_item)
- .setIcon(AppCompatDrawableManager.get().getDrawable(themedContext, R.drawable.ic_refresh));
+ .setIcon(AppCompatResources.getDrawable(themedContext, R.drawable.ic_refresh));
toolbar.getMenu().findItem(R.id.action_search)
- .setIcon(AppCompatDrawableManager.get().getDrawable(themedContext, R.drawable.ic_search));
+ .setIcon(AppCompatResources.getDrawable(themedContext, R.drawable.ic_search));
}
};
iconTintManager.updateTint();
@@ -330,11 +331,11 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem
}
final int itemId = item.getItemId();
if (itemId == R.id.rename_item) {
- new RenameFeedDialog(getActivity(), feed).show();
+ new RenameItemDialog(getActivity(), feed).show();
return true;
} else if (itemId == R.id.remove_item) {
- RemoveFeedDialog.show(getContext(), feed, () ->
- ((MainActivity) getActivity()).loadFragment(EpisodesFragment.TAG, null));
+ ((MainActivity) getActivity()).loadFragment(EpisodesFragment.TAG, null);
+ RemoveFeedDialog.show(getContext(), feed);
return true;
} else if (itemId == R.id.action_search) {
((MainActivity) getActivity()).loadChildFragment(SearchFragment.newInstance(feed.getId(), feed.getTitle()));
@@ -644,6 +645,23 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem
return feed;
}
+ @Subscribe(threadMode = ThreadMode.MAIN)
+ public void onKeyUp(KeyEvent event) {
+ if (!isAdded() || !isVisible() || !isMenuVisible()) {
+ return;
+ }
+ switch (event.getKeyCode()) {
+ case KeyEvent.KEYCODE_T:
+ recyclerView.smoothScrollToPosition(0);
+ break;
+ case KeyEvent.KEYCODE_B:
+ recyclerView.smoothScrollToPosition(adapter.getItemCount() - 1);
+ break;
+ default:
+ break;
+ }
+ }
+
private static class FeedItemListAdapter extends EpisodeItemListAdapter {
public FeedItemListAdapter(MainActivity mainActivity) {
super(mainActivity);
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/FeedSettingsFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/FeedSettingsFragment.java
index dbc7f2ae3..0c2103d25 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/FeedSettingsFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/FeedSettingsFragment.java
@@ -7,16 +7,19 @@ import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.Toolbar;
import androidx.fragment.app.Fragment;
import androidx.preference.ListPreference;
+import androidx.preference.Preference;
import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.SwitchPreferenceCompat;
import androidx.recyclerview.widget.RecyclerView;
import de.danoeh.antennapod.R;
-import de.danoeh.antennapod.core.event.settings.SkipIntroEndingChangedEvent;
-import de.danoeh.antennapod.core.event.settings.SpeedPresetChangedEvent;
-import de.danoeh.antennapod.core.event.settings.VolumeAdaptionChangedEvent;
+import de.danoeh.antennapod.event.settings.SkipIntroEndingChangedEvent;
+import de.danoeh.antennapod.event.settings.SpeedPresetChangedEvent;
+import de.danoeh.antennapod.event.settings.VolumeAdaptionChangedEvent;
+import de.danoeh.antennapod.databinding.PlaybackSpeedFeedSettingDialogBinding;
import de.danoeh.antennapod.model.feed.Feed;
import de.danoeh.antennapod.model.feed.FeedFilter;
import de.danoeh.antennapod.model.feed.FeedPreferences;
@@ -35,12 +38,9 @@ import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
import org.greenrobot.eventbus.EventBus;
-import java.text.DecimalFormat;
-import java.text.DecimalFormatSymbols;
+import java.util.Collections;
import java.util.Locale;
-import static de.danoeh.antennapod.model.feed.FeedPreferences.SPEED_USE_GLOBAL;
-
public class FeedSettingsFragment extends Fragment {
private static final String TAG = "FeedSettingsFragment";
private static final String EXTRA_FEED_ID = "de.danoeh.antennapod.extra.feedId";
@@ -104,8 +104,6 @@ public class FeedSettingsFragment extends Fragment {
private static final String PREF_FEED_PLAYBACK_SPEED = "feedPlaybackSpeed";
private static final String PREF_AUTO_SKIP = "feedAutoSkip";
private static final String PREF_TAGS = "tags";
- private static final DecimalFormat SPEED_FORMAT =
- new DecimalFormat("0.00", DecimalFormatSymbols.getInstance(Locale.US));
private Feed feed;
private Disposable disposable;
@@ -164,7 +162,6 @@ public class FeedSettingsFragment extends Fragment {
updateAutoDeleteSummary();
updateVolumeReductionValue();
updateAutoDownloadEnabled();
- updatePlaybackSpeedPreference();
if (feed.isLocalFeed()) {
findPreference(PREF_AUTHENTICATION).setVisible(false);
@@ -205,27 +202,34 @@ public class FeedSettingsFragment extends Fragment {
}
private void setupPlaybackSpeedPreference() {
- ListPreference feedPlaybackSpeedPreference = findPreference(PREF_FEED_PLAYBACK_SPEED);
-
- final String[] speeds = getResources().getStringArray(R.array.playback_speed_values);
- String[] values = new String[speeds.length + 1];
- values[0] = SPEED_FORMAT.format(SPEED_USE_GLOBAL);
-
- String[] entries = new String[speeds.length + 1];
- entries[0] = getString(R.string.feed_auto_download_global);
-
- System.arraycopy(speeds, 0, values, 1, speeds.length);
- System.arraycopy(speeds, 0, entries, 1, speeds.length);
-
- feedPlaybackSpeedPreference.setEntryValues(values);
- feedPlaybackSpeedPreference.setEntries(entries);
- feedPlaybackSpeedPreference.setOnPreferenceChangeListener((preference, newValue) -> {
- feedPreferences.setFeedPlaybackSpeed(Float.parseFloat((String) newValue));
- DBWriter.setFeedPreferences(feedPreferences);
- updatePlaybackSpeedPreference();
- EventBus.getDefault().post(
- new SpeedPresetChangedEvent(feedPreferences.getFeedPlaybackSpeed(), feed.getId()));
- return false;
+ Preference feedPlaybackSpeedPreference = findPreference(PREF_FEED_PLAYBACK_SPEED);
+ feedPlaybackSpeedPreference.setOnPreferenceClickListener(preference -> {
+ PlaybackSpeedFeedSettingDialogBinding viewBinding =
+ PlaybackSpeedFeedSettingDialogBinding.inflate(getLayoutInflater());
+ viewBinding.seekBar.setProgressChangedListener(speed ->
+ viewBinding.currentSpeedLabel.setText(String.format(Locale.getDefault(), "%.2fx", speed)));
+ float speed = feedPreferences.getFeedPlaybackSpeed();
+ viewBinding.useGlobalCheckbox.setOnCheckedChangeListener((buttonView, isChecked) -> {
+ viewBinding.seekBar.setEnabled(!isChecked);
+ viewBinding.seekBar.setAlpha(isChecked ? 0.4f : 1f);
+ viewBinding.currentSpeedLabel.setAlpha(isChecked ? 0.4f : 1f);
+ });
+ viewBinding.useGlobalCheckbox.setChecked(speed == FeedPreferences.SPEED_USE_GLOBAL);
+ viewBinding.seekBar.updateSpeed(speed == FeedPreferences.SPEED_USE_GLOBAL ? 1 : speed);
+ new AlertDialog.Builder(getContext())
+ .setTitle(R.string.playback_speed)
+ .setView(viewBinding.getRoot())
+ .setPositiveButton(android.R.string.ok, (dialog, which) -> {
+ float newSpeed = viewBinding.useGlobalCheckbox.isChecked()
+ ? FeedPreferences.SPEED_USE_GLOBAL : viewBinding.seekBar.getCurrentSpeed();
+ feedPreferences.setFeedPlaybackSpeed(newSpeed);
+ DBWriter.setFeedPreferences(feedPreferences);
+ EventBus.getDefault().post(
+ new SpeedPresetChangedEvent(feedPreferences.getFeedPlaybackSpeed(), feed.getId()));
+ })
+ .setNegativeButton(R.string.cancel_label, null)
+ .show();
+ return true;
});
}
@@ -277,13 +281,6 @@ public class FeedSettingsFragment extends Fragment {
});
}
- private void updatePlaybackSpeedPreference() {
- ListPreference feedPlaybackSpeedPreference = findPreference(PREF_FEED_PLAYBACK_SPEED);
-
- float speedValue = feedPreferences.getFeedPlaybackSpeed();
- feedPlaybackSpeedPreference.setValue(SPEED_FORMAT.format(speedValue));
- }
-
private void updateAutoDeleteSummary() {
ListPreference autoDeletePreference = findPreference(PREF_AUTO_DELETE);
@@ -395,7 +392,8 @@ public class FeedSettingsFragment extends Fragment {
private void setupTags() {
findPreference(PREF_TAGS).setOnPreferenceClickListener(preference -> {
- TagSettingsDialog.newInstance(feedPreferences).show(getChildFragmentManager(), TagSettingsDialog.TAG);
+ TagSettingsDialog.newInstance(Collections.singletonList(feedPreferences))
+ .show(getChildFragmentManager(), TagSettingsDialog.TAG);
return true;
});
}
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/FeedStatisticsDialogFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/FeedStatisticsDialogFragment.java
new file mode 100644
index 000000000..33710b2c4
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/FeedStatisticsDialogFragment.java
@@ -0,0 +1,42 @@
+package de.danoeh.antennapod.fragment;
+
+import android.app.Dialog;
+import android.os.Bundle;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AlertDialog;
+import androidx.fragment.app.DialogFragment;
+import de.danoeh.antennapod.R;
+
+public class FeedStatisticsDialogFragment extends DialogFragment {
+ private static final String EXTRA_FEED_ID = "de.danoeh.antennapod.extra.feedId";
+ private static final String EXTRA_FEED_TITLE = "de.danoeh.antennapod.extra.feedTitle";
+
+ public static FeedStatisticsDialogFragment newInstance(long feedId, String feedTitle) {
+ FeedStatisticsDialogFragment fragment = new FeedStatisticsDialogFragment();
+ Bundle arguments = new Bundle();
+ arguments.putLong(EXTRA_FEED_ID, feedId);
+ arguments.putString(EXTRA_FEED_TITLE, feedTitle);
+ fragment.setArguments(arguments);
+ return fragment;
+ }
+
+ @NonNull
+ @Override
+ public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
+ AlertDialog.Builder dialog = new AlertDialog.Builder(getContext());
+ dialog.setPositiveButton(android.R.string.ok, null);
+ dialog.setTitle(getArguments().getString(EXTRA_FEED_TITLE));
+ dialog.setView(R.layout.feed_statistics_dialog);
+ return dialog.create();
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ long feedId = getArguments().getLong(EXTRA_FEED_ID);
+ getChildFragmentManager().beginTransaction().replace(R.id.statisticsContainer,
+ FeedStatisticsFragment.newInstance(feedId, true), "feed_statistics_fragment")
+ .commitAllowingStateLoss();
+ }
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/FeedStatisticsFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/FeedStatisticsFragment.java
new file mode 100644
index 000000000..e85c2a386
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/FeedStatisticsFragment.java
@@ -0,0 +1,93 @@
+package de.danoeh.antennapod.fragment;
+
+import android.os.Bundle;
+import android.text.format.Formatter;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+import de.danoeh.antennapod.core.storage.DBReader;
+import de.danoeh.antennapod.core.storage.StatisticsItem;
+import de.danoeh.antennapod.core.util.Converter;
+import de.danoeh.antennapod.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.Locale;
+
+public class FeedStatisticsFragment extends Fragment {
+ private static final String EXTRA_FEED_ID = "de.danoeh.antennapod.extra.feedId";
+ private static final String EXTRA_DETAILED = "de.danoeh.antennapod.extra.detailed";
+
+ private long feedId;
+ private Disposable disposable;
+ private FeedStatisticsBinding viewBinding;
+
+ public static FeedStatisticsFragment newInstance(long feedId, boolean detailed) {
+ FeedStatisticsFragment fragment = new FeedStatisticsFragment();
+ Bundle arguments = new Bundle();
+ arguments.putLong(EXTRA_FEED_ID, feedId);
+ arguments.putBoolean(EXTRA_DETAILED, detailed);
+ fragment.setArguments(arguments);
+ return fragment;
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
+ @Nullable Bundle savedInstanceState) {
+ feedId = getArguments().getLong(EXTRA_FEED_ID);
+ viewBinding = FeedStatisticsBinding.inflate(inflater);
+
+ if (!getArguments().getBoolean(EXTRA_DETAILED)) {
+ for (int i = 0; i < viewBinding.getRoot().getChildCount(); i++) {
+ View child = viewBinding.getRoot().getChildAt(i);
+ if ("detailed".equals(child.getTag())) {
+ child.setVisibility(View.GONE);
+ }
+ }
+ }
+
+ loadStatistics();
+ return viewBinding.getRoot();
+ }
+
+ private void loadStatistics() {
+ disposable =
+ Observable.fromCallable(() -> {
+ List<StatisticsItem> statisticsData = DBReader.getStatistics();
+ for (StatisticsItem statisticsItem : statisticsData) {
+ if (statisticsItem.feed.getId() == feedId) {
+ return statisticsItem;
+ }
+ }
+ return null;
+ })
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(this::showStats, Throwable::printStackTrace);
+ }
+
+ private void showStats(StatisticsItem s) {
+ 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));
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ if (disposable != null) {
+ disposable.dispose();
+ }
+ }
+}
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 31c6da8cd..7361c8527 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/ItemFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/ItemFragment.java
@@ -14,11 +14,7 @@ import android.widget.Button;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.TextView;
-import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import androidx.core.text.TextUtilsCompat;
-import androidx.core.util.ObjectsCompat;
-import androidx.core.view.ViewCompat;
import androidx.fragment.app.Fragment;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.resource.bitmap.FitCenter;
@@ -42,9 +38,9 @@ import de.danoeh.antennapod.adapter.actionbutton.StreamActionButton;
import de.danoeh.antennapod.adapter.actionbutton.VisitWebsiteActionButton;
import de.danoeh.antennapod.core.event.DownloadEvent;
import de.danoeh.antennapod.core.event.DownloaderUpdate;
-import de.danoeh.antennapod.core.event.FeedItemEvent;
-import de.danoeh.antennapod.core.event.PlayerStatusEvent;
-import de.danoeh.antennapod.core.event.UnreadItemsUpdateEvent;
+import de.danoeh.antennapod.event.FeedItemEvent;
+import de.danoeh.antennapod.event.PlayerStatusEvent;
+import de.danoeh.antennapod.event.UnreadItemsUpdateEvent;
import de.danoeh.antennapod.model.feed.FeedItem;
import de.danoeh.antennapod.model.feed.FeedMedia;
import de.danoeh.antennapod.core.feed.util.ImageResourceUtils;
@@ -72,6 +68,7 @@ import org.greenrobot.eventbus.ThreadMode;
import java.util.List;
import java.util.Locale;
+import java.util.Objects;
/**
* Displays information about a FeedItem and actions.
@@ -149,7 +146,7 @@ public class ItemFragment extends Fragment {
webvDescription = layout.findViewById(R.id.webvDescription);
webvDescription.setTimecodeSelectedListener(time -> {
if (controller != null && item.getMedia() != null && controller.getMedia() != null
- && ObjectsCompat.equals(item.getMedia().getIdentifier(), controller.getMedia().getIdentifier())) {
+ && Objects.equals(item.getMedia().getIdentifier(), controller.getMedia().getIdentifier())) {
controller.seekTo(time);
} else {
((MainActivity) getActivity()).showSnackbarAbovePlayer(R.string.play_this_to_seek_position,
@@ -190,8 +187,8 @@ public class ItemFragment extends Fragment {
}
private void showOnDemandConfigBalloon(boolean offerStreaming) {
- boolean isLocaleRtl = TextUtilsCompat.getLayoutDirectionFromLocale(Locale.getDefault())
- == ViewCompat.LAYOUT_DIRECTION_RTL;
+ boolean isLocaleRtl = TextUtils.getLayoutDirectionFromLocale(Locale.getDefault())
+ == View.LAYOUT_DIRECTION_RTL;
Balloon balloon = new Balloon.Builder(getContext())
.setArrowOrientation(ArrowOrientation.TOP)
.setArrowPosition(0.25f + ((isLocaleRtl ^ offerStreaming) ? 0f : 0.5f))
@@ -224,17 +221,6 @@ public class ItemFragment extends Fragment {
}
@Override
- public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
- super.onViewCreated(view, savedInstanceState);
- load();
- }
-
- @Override
- public void onActivityCreated(@Nullable Bundle savedInstanceState) {
- super.onActivityCreated(savedInstanceState);
- }
-
- @Override
public void onStart() {
super.onStart();
EventBus.getDefault().register(this);
@@ -245,6 +231,7 @@ public class ItemFragment extends Fragment {
}
};
controller.init();
+ load();
}
@Override
@@ -398,7 +385,7 @@ public class ItemFragment extends Fragment {
long mediaId = item.getMedia().getId();
if (ArrayUtils.contains(update.mediaIds, mediaId)) {
if (itemsLoaded && getActivity() != null) {
- updateAppearance();
+ updateButtons();
}
}
}
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/ItemPagerFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/ItemPagerFragment.java
index d42300ca7..cae49c63e 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/ItemPagerFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/ItemPagerFragment.java
@@ -9,7 +9,6 @@ import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.Toolbar;
-import androidx.core.view.ViewCompat;
import androidx.fragment.app.Fragment;
import androidx.viewpager2.adapter.FragmentStateAdapter;
import androidx.viewpager2.widget.ViewPager2;
@@ -20,7 +19,7 @@ import org.greenrobot.eventbus.ThreadMode;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MainActivity;
-import de.danoeh.antennapod.core.event.FeedItemEvent;
+import de.danoeh.antennapod.event.FeedItemEvent;
import de.danoeh.antennapod.model.feed.FeedItem;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.menuhandler.FeedItemMenuHandler;
@@ -78,7 +77,7 @@ public class ItemPagerFragment extends Fragment implements Toolbar.OnMenuItemCli
// > When using FragmentStatePagerAdapter the host ViewPager must have a valid ID set.
// When opening multiple ItemPagerFragments by clicking "item" -> "visit podcast" -> "item" -> etc,
// the ID is no longer unique and FragmentStatePagerAdapter does not display any pages.
- int newId = ViewCompat.generateViewId();
+ int newId = View.generateViewId();
if (savedInstanceState != null && savedInstanceState.getInt(KEY_PAGER_ID, 0) != 0) {
// Restore state by using the same ID as before. ID collisions are prevented in MainActivity.
newId = savedInstanceState.getInt(KEY_PAGER_ID, 0);
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/NavDrawerFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/NavDrawerFragment.java
index 826a7e0ab..a5cabeb29 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/NavDrawerFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/NavDrawerFragment.java
@@ -28,9 +28,9 @@ import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.activity.PreferenceActivity;
import de.danoeh.antennapod.adapter.NavListAdapter;
import de.danoeh.antennapod.core.dialog.ConfirmationDialog;
-import de.danoeh.antennapod.core.event.FeedListUpdateEvent;
-import de.danoeh.antennapod.core.event.QueueEvent;
-import de.danoeh.antennapod.core.event.UnreadItemsUpdateEvent;
+import de.danoeh.antennapod.event.FeedListUpdateEvent;
+import de.danoeh.antennapod.event.QueueEvent;
+import de.danoeh.antennapod.event.UnreadItemsUpdateEvent;
import de.danoeh.antennapod.dialog.TagSettingsDialog;
import de.danoeh.antennapod.model.feed.Feed;
import de.danoeh.antennapod.core.preferences.UserPreferences;
@@ -39,7 +39,7 @@ import de.danoeh.antennapod.core.storage.DBWriter;
import de.danoeh.antennapod.core.storage.NavDrawerData;
import de.danoeh.antennapod.dialog.RemoveFeedDialog;
import de.danoeh.antennapod.dialog.SubscriptionsFilterDialog;
-import de.danoeh.antennapod.dialog.RenameFeedDialog;
+import de.danoeh.antennapod.dialog.RenameItemDialog;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
@@ -50,6 +50,7 @@ import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@@ -123,24 +124,28 @@ public class NavDrawerFragment extends Fragment implements SharedPreferences.OnS
@Override
public void onCreateContextMenu(@NonNull ContextMenu menu, @NonNull View v, ContextMenu.ContextMenuInfo menuInfo) {
super.onCreateContextMenu(menu, v, menuInfo);
- if (contextPressedItem.type != NavDrawerData.DrawerItem.Type.FEED) {
- return; // Should actually never happen because the context menu is not set up for other items
- }
-
MenuInflater inflater = getActivity().getMenuInflater();
- inflater.inflate(R.menu.nav_feed_context, menu);
- menu.setHeaderTitle(((NavDrawerData.FeedDrawerItem) contextPressedItem).feed.getTitle());
- // episodes are not loaded, so we cannot check if the podcast has new or unplayed ones!
+ menu.setHeaderTitle(contextPressedItem.getTitle());
+ if (contextPressedItem.type == NavDrawerData.DrawerItem.Type.FEED) {
+ inflater.inflate(R.menu.nav_feed_context, menu);
+ // episodes are not loaded, so we cannot check if the podcast has new or unplayed ones!
+ } else {
+ inflater.inflate(R.menu.nav_folder_context, menu);
+ }
}
@Override
public boolean onContextItemSelected(@NonNull MenuItem item) {
NavDrawerData.DrawerItem pressedItem = contextPressedItem;
contextPressedItem = null;
- if (pressedItem != null && pressedItem.type == NavDrawerData.DrawerItem.Type.FEED) {
+ if (pressedItem == null) {
+ return false;
+ }
+ if (pressedItem.type == NavDrawerData.DrawerItem.Type.FEED) {
return onFeedContextMenuClicked(((NavDrawerData.FeedDrawerItem) pressedItem).feed, item);
+ } else {
+ return onTagContextMenuClicked(pressedItem, item);
}
- return false;
}
private boolean onFeedContextMenuClicked(Feed feed, MenuItem item) {
@@ -157,16 +162,25 @@ public class NavDrawerFragment extends Fragment implements SharedPreferences.OnS
};
removeAllNewFlagsConfirmationDialog.createNewDialog().show();
return true;
- } else if (itemId == R.id.add_to_folder) {
- TagSettingsDialog.newInstance(feed.getPreferences()).show(getChildFragmentManager(), TagSettingsDialog.TAG);
+ } else if (itemId == R.id.edit_tags) {
+ TagSettingsDialog.newInstance(Collections.singletonList(feed.getPreferences()))
+ .show(getChildFragmentManager(), TagSettingsDialog.TAG);
return true;
} else if (itemId == R.id.rename_item) {
- new RenameFeedDialog(getActivity(), feed).show();
+ new RenameItemDialog(getActivity(), feed).show();
return true;
} else if (itemId == R.id.remove_item) {
- RemoveFeedDialog.show(getContext(), feed, () -> {
- ((MainActivity) getActivity()).loadFragment(EpisodesFragment.TAG, null);
- });
+ ((MainActivity) getActivity()).loadFragment(EpisodesFragment.TAG, null);
+ RemoveFeedDialog.show(getContext(), feed);
+ return true;
+ }
+ return super.onContextItemSelected(item);
+ }
+
+ private boolean onTagContextMenuClicked(NavDrawerData.DrawerItem drawerItem, MenuItem item) {
+ final int itemId = item.getItemId();
+ if (itemId == R.id.rename_folder_item) {
+ new RenameItemDialog(getActivity(), drawerItem).show();
return true;
}
return super.onContextItemSelected(item);
@@ -318,7 +332,7 @@ public class NavDrawerFragment extends Fragment implements SharedPreferences.OnS
((MainActivity) getActivity()).getBottomSheet()
.setState(BottomSheetBehavior.STATE_COLLAPSED);
} else {
- NavDrawerData.FolderDrawerItem folder = ((NavDrawerData.FolderDrawerItem) clickedItem);
+ NavDrawerData.TagDrawerItem folder = ((NavDrawerData.TagDrawerItem) clickedItem);
if (openFolders.contains(folder.name)) {
openFolders.remove(folder.name);
} else {
@@ -388,11 +402,11 @@ public class NavDrawerFragment extends Fragment implements SharedPreferences.OnS
for (NavDrawerData.DrawerItem item : items) {
item.setLayer(layer);
flatItems.add(item);
- if (item.type == NavDrawerData.DrawerItem.Type.FOLDER) {
- NavDrawerData.FolderDrawerItem folder = ((NavDrawerData.FolderDrawerItem) item);
+ if (item.type == NavDrawerData.DrawerItem.Type.TAG) {
+ NavDrawerData.TagDrawerItem folder = ((NavDrawerData.TagDrawerItem) item);
folder.isOpen = openFolders.contains(folder.name);
if (folder.isOpen) {
- flatItems.addAll(makeFlatDrawerData(((NavDrawerData.FolderDrawerItem) item).children, layer + 1));
+ flatItems.addAll(makeFlatDrawerData(((NavDrawerData.TagDrawerItem) item).children, layer + 1));
}
}
}
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 992b6930c..f3080f655 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/OnlineSearchFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/OnlineSearchFragment.java
@@ -1,15 +1,20 @@
package de.danoeh.antennapod.fragment;
+import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
+
+import android.widget.AbsListView;
import androidx.appcompat.widget.Toolbar;
import androidx.fragment.app.Fragment;
import androidx.appcompat.widget.SearchView;
+
import android.util.Log;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
+import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.GridView;
import android.widget.ProgressBar;
@@ -110,6 +115,21 @@ public class OnlineSearchFragment extends Fragment {
TextView txtvPoweredBy = root.findViewById(R.id.search_powered_by);
txtvPoweredBy.setText(getString(R.string.search_powered_by, searchProvider.getName()));
setupToolbar(root.findViewById(R.id.toolbar));
+
+ gridView.setOnScrollListener(new AbsListView.OnScrollListener() {
+ @Override
+ public void onScrollStateChanged(AbsListView view, int scrollState) {
+ if (scrollState == SCROLL_STATE_TOUCH_SCROLL) {
+ InputMethodManager imm = (InputMethodManager)
+ getActivity().getSystemService(Context.INPUT_METHOD_SERVICE);
+ imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
+ }
+ }
+
+ @Override
+ public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
+ }
+ });
return root;
}
@@ -142,6 +162,11 @@ public class OnlineSearchFragment extends Fragment {
return false;
}
});
+ sv.setOnQueryTextFocusChangeListener((view, hasFocus) -> {
+ if (hasFocus) {
+ showInputMethod(view.findFocus());
+ }
+ });
searchItem.setOnActionExpandListener(new MenuItem.OnActionExpandListener() {
@Override
public boolean onMenuItemActionExpand(MenuItem item) {
@@ -192,4 +217,11 @@ public class OnlineSearchFragment extends Fragment {
txtvEmpty.setVisibility(View.GONE);
progressBar.setVisibility(View.VISIBLE);
}
+
+ private void showInputMethod(View view) {
+ InputMethodManager imm = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE);
+ if (imm != null) {
+ imm.showSoftInput(view, 0);
+ }
+ }
}
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/PlaybackHistoryFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/PlaybackHistoryFragment.java
index 5e3d36c03..54c98c0ce 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/PlaybackHistoryFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/PlaybackHistoryFragment.java
@@ -2,6 +2,7 @@ package de.danoeh.antennapod.fragment;
import android.os.Bundle;
import android.util.Log;
+import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
@@ -16,11 +17,11 @@ 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.core.event.FeedItemEvent;
-import de.danoeh.antennapod.core.event.PlaybackHistoryEvent;
-import de.danoeh.antennapod.core.event.PlaybackPositionEvent;
-import de.danoeh.antennapod.core.event.PlayerStatusEvent;
-import de.danoeh.antennapod.core.event.UnreadItemsUpdateEvent;
+import de.danoeh.antennapod.event.FeedItemEvent;
+import de.danoeh.antennapod.event.playback.PlaybackHistoryEvent;
+import de.danoeh.antennapod.event.playback.PlaybackPositionEvent;
+import de.danoeh.antennapod.event.PlayerStatusEvent;
+import de.danoeh.antennapod.event.UnreadItemsUpdateEvent;
import de.danoeh.antennapod.model.feed.FeedItem;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.storage.DBWriter;
@@ -181,6 +182,23 @@ public class PlaybackHistoryFragment extends Fragment implements Toolbar.OnMenuI
}
@Subscribe(threadMode = ThreadMode.MAIN)
+ public void onKeyUp(KeyEvent event) {
+ if (!isAdded() || !isVisible() || !isMenuVisible()) {
+ return;
+ }
+ switch (event.getKeyCode()) {
+ case KeyEvent.KEYCODE_T:
+ recyclerView.smoothScrollToPosition(0);
+ break;
+ case KeyEvent.KEYCODE_B:
+ recyclerView.smoothScrollToPosition(adapter.getItemCount() - 1);
+ break;
+ default:
+ break;
+ }
+ }
+
+ @Subscribe(threadMode = ThreadMode.MAIN)
public void onHistoryUpdated(PlaybackHistoryEvent event) {
loadItems();
refreshToolbarState();
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java
index 1b7d236c6..c145642b9 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java
@@ -7,6 +7,7 @@ import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
+import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
@@ -33,11 +34,11 @@ import de.danoeh.antennapod.adapter.QueueRecyclerAdapter;
import de.danoeh.antennapod.core.dialog.ConfirmationDialog;
import de.danoeh.antennapod.core.event.DownloadEvent;
import de.danoeh.antennapod.core.event.DownloaderUpdate;
-import de.danoeh.antennapod.core.event.FeedItemEvent;
-import de.danoeh.antennapod.core.event.PlaybackPositionEvent;
-import de.danoeh.antennapod.core.event.PlayerStatusEvent;
-import de.danoeh.antennapod.core.event.QueueEvent;
-import de.danoeh.antennapod.core.event.UnreadItemsUpdateEvent;
+import de.danoeh.antennapod.event.FeedItemEvent;
+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.menuhandler.MenuItemUtils;
import de.danoeh.antennapod.fragment.actions.EpisodeMultiSelectActionHandler;
import de.danoeh.antennapod.fragment.swipeactions.SwipeActions;
@@ -234,6 +235,23 @@ public class QueueFragment extends Fragment implements Toolbar.OnMenuItemClickLi
}
}
+ @Subscribe(threadMode = ThreadMode.MAIN)
+ public void onKeyUp(KeyEvent event) {
+ if (!isAdded() || !isVisible() || !isMenuVisible()) {
+ return;
+ }
+ switch (event.getKeyCode()) {
+ case KeyEvent.KEYCODE_T:
+ recyclerView.smoothScrollToPosition(0);
+ break;
+ case KeyEvent.KEYCODE_B:
+ recyclerView.smoothScrollToPosition(recyclerAdapter.getItemCount() - 1);
+ break;
+ default:
+ break;
+ }
+ }
+
@Override
public void onDestroyView() {
super.onDestroyView();
@@ -247,8 +265,9 @@ public class QueueFragment extends Fragment implements Toolbar.OnMenuItemClickLi
() -> DownloadService.isRunning && DownloadRequester.getInstance().isDownloadingFeeds();
private void refreshToolbarState() {
- toolbar.getMenu().findItem(R.id.queue_lock).setChecked(UserPreferences.isQueueLocked());
boolean keepSorted = UserPreferences.isQueueKeepSorted();
+ toolbar.getMenu().findItem(R.id.queue_lock).setChecked(UserPreferences.isQueueLocked());
+ toolbar.getMenu().findItem(R.id.queue_lock).setVisible(!keepSorted);
toolbar.getMenu().findItem(R.id.queue_sort_random).setVisible(!keepSorted);
toolbar.getMenu().findItem(R.id.queue_keep_sorted).setChecked(keepSorted);
isUpdatingFeeds = MenuItemUtils.updateRefreshMenuItem(toolbar.getMenu(),
@@ -635,11 +654,6 @@ public class QueueFragment extends Fragment implements Toolbar.OnMenuItemClickLi
}
@Override
- public boolean isItemViewSwipeEnabled() {
- return !UserPreferences.isQueueLocked();
- }
-
- @Override
public void clearView(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
super.clearView(recyclerView, viewHolder);
// Check if drag finished
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 14f355b52..8bfcfd1ed 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/QuickFeedDiscoveryFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/QuickFeedDiscoveryFragment.java
@@ -25,7 +25,7 @@ import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.activity.OnlineFeedViewActivity;
import de.danoeh.antennapod.adapter.FeedDiscoverAdapter;
-import de.danoeh.antennapod.core.event.DiscoveryDefaultUpdateEvent;
+import de.danoeh.antennapod.event.DiscoveryDefaultUpdateEvent;
import de.danoeh.antennapod.discovery.ItunesTopListLoader;
import de.danoeh.antennapod.discovery.PodcastSearchResult;
import io.reactivex.disposables.Disposable;
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 f8326d9c1..e43b6f314 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/SearchFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/SearchFragment.java
@@ -1,5 +1,7 @@
package de.danoeh.antennapod.fragment;
+
+import android.content.Context;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
@@ -9,6 +11,7 @@ import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
+import android.view.inputmethod.InputMethodManager;
import android.widget.ProgressBar;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -26,10 +29,10 @@ import de.danoeh.antennapod.adapter.EpisodeItemListAdapter;
import de.danoeh.antennapod.adapter.FeedSearchResultAdapter;
import de.danoeh.antennapod.core.event.DownloadEvent;
import de.danoeh.antennapod.core.event.DownloaderUpdate;
-import de.danoeh.antennapod.core.event.FeedItemEvent;
-import de.danoeh.antennapod.core.event.PlaybackPositionEvent;
-import de.danoeh.antennapod.core.event.PlayerStatusEvent;
-import de.danoeh.antennapod.core.event.UnreadItemsUpdateEvent;
+import de.danoeh.antennapod.event.FeedItemEvent;
+import de.danoeh.antennapod.event.playback.PlaybackPositionEvent;
+import de.danoeh.antennapod.event.PlayerStatusEvent;
+import de.danoeh.antennapod.event.UnreadItemsUpdateEvent;
import de.danoeh.antennapod.model.feed.Feed;
import de.danoeh.antennapod.model.feed.FeedItem;
import de.danoeh.antennapod.core.storage.FeedSearcher;
@@ -70,7 +73,6 @@ public class SearchFragment extends Fragment {
private SearchView searchView;
private Handler automaticSearchDebouncer;
private long lastQueryChange = 0;
-
/**
* Create a new SearchFragment that searches all feeds.
*/
@@ -153,6 +155,22 @@ public class SearchFragment extends Fragment {
if (getArguments().getString(ARG_QUERY, null) != null) {
search();
}
+ searchView.setOnQueryTextFocusChangeListener((view, hasFocus) -> {
+ if (hasFocus) {
+ showInputMethod(view.findFocus());
+ }
+ });
+ recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
+ @Override
+ public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
+ super.onScrollStateChanged(recyclerView, newState);
+ if (newState == RecyclerView.SCROLL_STATE_DRAGGING) {
+ InputMethodManager imm = (InputMethodManager)
+ getActivity().getSystemService(Context.INPUT_METHOD_SERVICE);
+ imm.hideSoftInputFromWindow(recyclerView.getWindowToken(), 0);
+ }
+ }
+ });
return layout;
}
@@ -320,4 +338,11 @@ public class SearchFragment extends Fragment {
List<Feed> feeds = FeedSearcher.searchFeeds(getContext(), query);
return new Pair<>(items, feeds);
}
+
+ private void showInputMethod(View view) {
+ InputMethodManager imm = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE);
+ if (imm != null) {
+ imm.showSoftInput(view, 0);
+ }
+ }
}
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 ea6c2ca0d..200f4dcd6 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/SubscriptionFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/SubscriptionFragment.java
@@ -33,6 +33,7 @@ import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.Callable;
@@ -42,8 +43,8 @@ import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.adapter.SubscriptionsRecyclerAdapter;
import de.danoeh.antennapod.core.dialog.ConfirmationDialog;
import de.danoeh.antennapod.core.event.DownloadEvent;
-import de.danoeh.antennapod.core.event.FeedListUpdateEvent;
-import de.danoeh.antennapod.core.event.UnreadItemsUpdateEvent;
+import de.danoeh.antennapod.event.FeedListUpdateEvent;
+import de.danoeh.antennapod.event.UnreadItemsUpdateEvent;
import de.danoeh.antennapod.core.menuhandler.MenuItemUtils;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.service.download.DownloadService;
@@ -54,7 +55,7 @@ import de.danoeh.antennapod.core.storage.NavDrawerData;
import de.danoeh.antennapod.core.util.download.AutoUpdateManager;
import de.danoeh.antennapod.dialog.FeedSortDialog;
import de.danoeh.antennapod.dialog.RemoveFeedDialog;
-import de.danoeh.antennapod.dialog.RenameFeedDialog;
+import de.danoeh.antennapod.dialog.RenameItemDialog;
import de.danoeh.antennapod.dialog.SubscriptionsFilterDialog;
import de.danoeh.antennapod.fragment.actions.FeedMultiSelectActionHandler;
import de.danoeh.antennapod.model.feed.Feed;
@@ -250,8 +251,8 @@ public class SubscriptionFragment extends Fragment
}
@Override
- public void onActivityCreated(Bundle savedInstanceState) {
- super.onActivityCreated(savedInstanceState);
+ public void onViewCreated(@NonNull View v, Bundle savedInstanceState) {
+ super.onViewCreated(v, savedInstanceState);
subscriptionAdapter = new SubscriptionsRecyclerAdapter((MainActivity) getActivity());
subscriptionAdapter.setOnSelectModeListener(this);
subscriptionRecycler.setAdapter(subscriptionAdapter);
@@ -293,9 +294,9 @@ public class SubscriptionFragment extends Fragment
NavDrawerData data = DBReader.getNavDrawerData();
List<NavDrawerData.DrawerItem> items = data.items;
for (NavDrawerData.DrawerItem item : items) {
- if (item.type == NavDrawerData.DrawerItem.Type.FOLDER
+ if (item.type == NavDrawerData.DrawerItem.Type.TAG
&& item.getTitle().equals(displayedFolder)) {
- return ((NavDrawerData.FolderDrawerItem) item).children;
+ return ((NavDrawerData.TagDrawerItem) item).children;
}
}
return items;
@@ -333,25 +334,32 @@ public class SubscriptionFragment extends Fragment
@Override
public boolean onContextItemSelected(MenuItem item) {
- Feed feed = subscriptionAdapter.getSelectedFeed();
- if (feed == null) {
+ NavDrawerData.DrawerItem drawerItem = subscriptionAdapter.getSelectedItem();
+ if (drawerItem == null) {
return false;
}
int itemId = item.getItemId();
+ if (drawerItem.type == NavDrawerData.DrawerItem.Type.TAG && itemId == R.id.rename_folder_item) {
+ new RenameItemDialog(getActivity(), drawerItem).show();
+ return true;
+ }
+
+ Feed feed = ((NavDrawerData.FeedDrawerItem) drawerItem).feed;
if (itemId == R.id.remove_all_new_flags_item) {
displayConfirmationDialog(
R.string.remove_all_new_flags_label,
R.string.remove_all_new_flags_confirmation_msg,
() -> DBWriter.removeFeedNewFlag(feed.getId()));
return true;
- } else if (itemId == R.id.add_to_folder) {
- TagSettingsDialog.newInstance(feed.getPreferences()).show(getChildFragmentManager(), TagSettingsDialog.TAG);
+ } else if (itemId == R.id.edit_tags) {
+ TagSettingsDialog.newInstance(Collections.singletonList(feed.getPreferences()))
+ .show(getChildFragmentManager(), TagSettingsDialog.TAG);
return true;
} else if (itemId == R.id.rename_item) {
- new RenameFeedDialog(getActivity(), feed).show();
+ new RenameItemDialog(getActivity(), feed).show();
return true;
} else if (itemId == R.id.remove_item) {
- RemoveFeedDialog.show(getContext(), feed, null);
+ RemoveFeedDialog.show(getContext(), feed);
return true;
} else if (itemId == R.id.multi_select) {
speedDialView.setVisibility(View.VISIBLE);
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/actions/EpisodeMultiSelectActionHandler.java b/app/src/main/java/de/danoeh/antennapod/fragment/actions/EpisodeMultiSelectActionHandler.java
index 028d2fff4..710ec6ce0 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/actions/EpisodeMultiSelectActionHandler.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/actions/EpisodeMultiSelectActionHandler.java
@@ -95,16 +95,13 @@ public class EpisodeMultiSelectActionHandler {
private void deleteChecked() {
int countHasMedia = 0;
- int countNoMedia = 0;
for (FeedItem feedItem : selectedItems) {
if (feedItem.hasMedia() && feedItem.getMedia().isDownloaded()) {
countHasMedia++;
DBWriter.deleteFeedMediaOfItem(activity, feedItem.getMedia().getId());
- } else {
- countNoMedia++;
}
}
- showMessageMore(R.plurals.deleted_multi_episode_batch_label, countNoMedia, countHasMedia);
+ showMessage(R.plurals.deleted_multi_episode_batch_label, countHasMedia);
}
private void showMessage(@PluralsRes int msgId, int numItems) {
@@ -112,14 +109,6 @@ public class EpisodeMultiSelectActionHandler {
.getQuantityString(msgId, numItems, numItems), Snackbar.LENGTH_LONG);
}
- private void showMessageMore(@PluralsRes int msgId, int countNoMedia, int countHasMedia) {
- activity.showSnackbarAbovePlayer(activity.getResources()
- .getQuantityString(msgId,
- (countHasMedia + countNoMedia),
- (countHasMedia + countNoMedia), countHasMedia),
- Snackbar.LENGTH_LONG);
- }
-
private long[] getSelectedIds() {
long[] checkedIds = new long[selectedItems.size()];
for (int i = 0; i < selectedItems.size(); ++i) {
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 f160b2241..e3dfe8ade 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
@@ -3,19 +3,23 @@ package de.danoeh.antennapod.fragment.actions;
import android.util.Log;
import androidx.annotation.PluralsRes;
+import androidx.appcompat.app.AlertDialog;
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;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.core.storage.DBWriter;
+import de.danoeh.antennapod.databinding.PlaybackSpeedFeedSettingDialogBinding;
import de.danoeh.antennapod.dialog.RemoveFeedDialog;
+import de.danoeh.antennapod.dialog.TagSettingsDialog;
import de.danoeh.antennapod.fragment.preferences.dialog.PreferenceListDialog;
import de.danoeh.antennapod.fragment.preferences.dialog.PreferenceSwitchDialog;
import de.danoeh.antennapod.model.feed.Feed;
@@ -33,7 +37,7 @@ public class FeedMultiSelectActionHandler {
public void handleAction(int id) {
if (id == R.id.remove_item) {
- RemoveFeedDialog.show(activity, selectedItems, null);
+ RemoveFeedDialog.show(activity, selectedItems);
} else if (id == R.id.keep_updated) {
keepUpdatedPrefHandler();
} else if (id == R.id.autodownload) {
@@ -42,6 +46,8 @@ public class FeedMultiSelectActionHandler {
autoDeleteEpisodesPrefHandler();
} else if (id == R.id.playback_speed) {
playbackSpeedPrefHandler();
+ } else if (id == R.id.edit_tags) {
+ editFeedPrefTags();
} else {
Log.e(TAG, "Unrecognized speed dial action item. Do nothing. id=" + id);
}
@@ -64,25 +70,26 @@ public class FeedMultiSelectActionHandler {
new DecimalFormat("0.00", DecimalFormatSymbols.getInstance(Locale.US));
private void playbackSpeedPrefHandler() {
- final String[] speeds = activity.getResources().getStringArray(R.array.playback_speed_values);
- String[] values = new String[speeds.length + 1];
- values[0] = SPEED_FORMAT.format(FeedPreferences.SPEED_USE_GLOBAL);
-
- String[] entries = new String[speeds.length + 1];
- entries[0] = activity.getString(R.string.feed_auto_download_global);
-
- System.arraycopy(speeds, 0, values, 1, speeds.length);
- System.arraycopy(speeds, 0, entries, 1, speeds.length);
-
- PreferenceListDialog preferenceListDialog = new PreferenceListDialog(activity,
- activity.getString(R.string.playback_speed));
- preferenceListDialog.openDialog(entries);
- preferenceListDialog.setOnPreferenceChangedListener(pos -> {
- saveFeedPreferences(feedPreferences -> {
- feedPreferences.setFeedPlaybackSpeed(Float.parseFloat((String) values[pos]));
- });
-
+ PlaybackSpeedFeedSettingDialogBinding viewBinding =
+ PlaybackSpeedFeedSettingDialogBinding.inflate(activity.getLayoutInflater());
+ viewBinding.seekBar.setProgressChangedListener(speed ->
+ viewBinding.currentSpeedLabel.setText(String.format(Locale.getDefault(), "%.2fx", speed)));
+ viewBinding.useGlobalCheckbox.setOnCheckedChangeListener((buttonView, isChecked) -> {
+ viewBinding.seekBar.setEnabled(!isChecked);
+ viewBinding.seekBar.setAlpha(isChecked ? 0.4f : 1f);
+ viewBinding.currentSpeedLabel.setAlpha(isChecked ? 0.4f : 1f);
});
+ viewBinding.seekBar.updateSpeed(1.0f);
+ new AlertDialog.Builder(activity)
+ .setTitle(R.string.playback_speed)
+ .setView(viewBinding.getRoot())
+ .setPositiveButton(android.R.string.ok, (dialog, which) -> {
+ float newSpeed = viewBinding.useGlobalCheckbox.isChecked()
+ ? FeedPreferences.SPEED_USE_GLOBAL : viewBinding.seekBar.getCurrentSpeed();
+ saveFeedPreferences(feedPreferences -> feedPreferences.setFeedPlaybackSpeed(newSpeed));
+ })
+ .setNegativeButton(R.string.cancel_label, null)
+ .show();
}
private void autoDeleteEpisodesPrefHandler() {
@@ -136,4 +143,13 @@ public class FeedMultiSelectActionHandler {
}
showMessage(R.plurals.updated_feeds_batch_label, selectedItems.size());
}
+
+ private void editFeedPrefTags() {
+ ArrayList<FeedPreferences> preferencesList = new ArrayList<>();
+ for (Feed feed : selectedItems) {
+ preferencesList.add(feed.getPreferences());
+ }
+ TagSettingsDialog.newInstance(preferencesList).show(activity.getSupportFragmentManager(),
+ TagSettingsDialog.TAG);
+ }
}
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/PodcastListFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/PodcastListFragment.java
index c813cbf7a..c2c5adc9a 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/PodcastListFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/PodcastListFragment.java
@@ -15,7 +15,7 @@ import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.activity.OnlineFeedViewActivity;
import de.danoeh.antennapod.adapter.gpodnet.PodcastListAdapter;
-import de.danoeh.antennapod.core.preferences.GpodnetPreferences;
+import de.danoeh.antennapod.core.sync.SynchronizationCredentials;
import de.danoeh.antennapod.core.service.download.AntennapodHttpClient;
import de.danoeh.antennapod.net.sync.gpoddernet.GpodnetService;
import de.danoeh.antennapod.net.sync.gpoddernet.GpodnetServiceException;
@@ -76,8 +76,8 @@ public abstract class PodcastListFragment extends Fragment {
disposable = Observable.fromCallable(
() -> {
GpodnetService service = new GpodnetService(AntennapodHttpClient.getHttpClient(),
- GpodnetPreferences.getHosturl(), GpodnetPreferences.getDeviceID(),
- GpodnetPreferences.getUsername(), GpodnetPreferences.getPassword());
+ SynchronizationCredentials.getHosturl(), SynchronizationCredentials.getDeviceID(),
+ SynchronizationCredentials.getUsername(), SynchronizationCredentials.getPassword());
return loadPodcastData(service);
})
.subscribeOn(Schedulers.io())
@@ -101,7 +101,7 @@ public abstract class PodcastListFragment extends Fragment {
}, error -> {
gridView.setVisibility(View.GONE);
progressBar.setVisibility(View.GONE);
- txtvError.setText(getString(R.string.error_msg_prefix) + error.getMessage());
+ txtvError.setText(error.getMessage());
txtvError.setVisibility(View.VISIBLE);
butRetry.setVisibility(View.VISIBLE);
Log.e(TAG, Log.getStackTraceString(error));
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/TagListFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/TagListFragment.java
index f961e30bb..abdfab941 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/TagListFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/TagListFragment.java
@@ -8,7 +8,7 @@ import androidx.annotation.NonNull;
import androidx.fragment.app.ListFragment;
import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.adapter.gpodnet.TagListAdapter;
-import de.danoeh.antennapod.core.preferences.GpodnetPreferences;
+import de.danoeh.antennapod.core.sync.SynchronizationCredentials;
import de.danoeh.antennapod.core.service.download.AntennapodHttpClient;
import de.danoeh.antennapod.net.sync.gpoddernet.GpodnetService;
import de.danoeh.antennapod.net.sync.gpoddernet.model.GpodnetTag;
@@ -51,8 +51,8 @@ public class TagListFragment extends ListFragment {
disposable = Observable.fromCallable(
() -> {
GpodnetService service = new GpodnetService(AntennapodHttpClient.getHttpClient(),
- GpodnetPreferences.getHosturl(), GpodnetPreferences.getDeviceID(),
- GpodnetPreferences.getUsername(), GpodnetPreferences.getPassword());
+ SynchronizationCredentials.getHosturl(), SynchronizationCredentials.getDeviceID(),
+ SynchronizationCredentials.getUsername(), SynchronizationCredentials.getPassword());
return service.getTopTags(COUNT);
})
.subscribeOn(Schedulers.io())
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/DownloadStatisticsFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/DownloadStatisticsFragment.java
index 3059d7ad2..ff94cc20c 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/DownloadStatisticsFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/DownloadStatisticsFragment.java
@@ -17,7 +17,6 @@ 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.core.util.comparator.CompareCompat;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
@@ -75,7 +74,7 @@ public class DownloadStatisticsFragment extends Fragment {
Observable.fromCallable(() -> {
List<StatisticsItem> statisticsData = DBReader.getStatistics();
Collections.sort(statisticsData, (item1, item2) ->
- CompareCompat.compareLong(item1.totalDownloadSize, item2.totalDownloadSize));
+ Long.compare(item2.totalDownloadSize, item1.totalDownloadSize));
return statisticsData;
})
.subscribeOn(Schedulers.io())
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/GpodderPreferencesFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/GpodderPreferencesFragment.java
deleted file mode 100644
index 4fb734e17..000000000
--- a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/GpodderPreferencesFragment.java
+++ /dev/null
@@ -1,128 +0,0 @@
-package de.danoeh.antennapod.fragment.preferences;
-
-import android.app.Activity;
-import android.os.Bundle;
-import androidx.core.text.HtmlCompat;
-import androidx.preference.PreferenceFragmentCompat;
-
-import android.text.Spanned;
-import android.text.format.DateUtils;
-import com.google.android.material.snackbar.Snackbar;
-import de.danoeh.antennapod.R;
-import de.danoeh.antennapod.activity.PreferenceActivity;
-import de.danoeh.antennapod.core.event.SyncServiceEvent;
-import de.danoeh.antennapod.core.preferences.GpodnetPreferences;
-import de.danoeh.antennapod.core.sync.SyncService;
-import de.danoeh.antennapod.dialog.AuthenticationDialog;
-import org.greenrobot.eventbus.EventBus;
-import org.greenrobot.eventbus.Subscribe;
-import org.greenrobot.eventbus.ThreadMode;
-
-public class GpodderPreferencesFragment extends PreferenceFragmentCompat {
- private static final String PREF_GPODNET_LOGIN = "pref_gpodnet_authenticate";
- private static final String PREF_GPODNET_SETLOGIN_INFORMATION = "pref_gpodnet_setlogin_information";
- private static final String PREF_GPODNET_SYNC = "pref_gpodnet_sync";
- private static final String PREF_GPODNET_FORCE_FULL_SYNC = "pref_gpodnet_force_full_sync";
- private static final String PREF_GPODNET_LOGOUT = "pref_gpodnet_logout";
-
- @Override
- public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
- addPreferencesFromResource(R.xml.preferences_gpodder);
- setupGpodderScreen();
- }
-
- @Override
- public void onStart() {
- super.onStart();
- ((PreferenceActivity) getActivity()).getSupportActionBar().setTitle(R.string.gpodnet_main_label);
- updateGpodnetPreferenceScreen();
- EventBus.getDefault().register(this);
- }
-
- @Override
- public void onStop() {
- super.onStop();
- EventBus.getDefault().unregister(this);
- ((PreferenceActivity) getActivity()).getSupportActionBar().setSubtitle("");
- }
-
- @Subscribe(threadMode = ThreadMode.MAIN, sticky = true)
- public void syncStatusChanged(SyncServiceEvent event) {
- updateGpodnetPreferenceScreen();
- if (!GpodnetPreferences.loggedIn()) {
- return;
- }
- if (event.getMessageResId() == R.string.sync_status_error
- || event.getMessageResId() == R.string.sync_status_success) {
- updateLastGpodnetSyncReport(SyncService.isLastSyncSuccessful(getContext()),
- SyncService.getLastSyncAttempt(getContext()));
- } else {
- ((PreferenceActivity) getActivity()).getSupportActionBar().setSubtitle(event.getMessageResId());
- }
- }
-
- private void setupGpodderScreen() {
- final Activity activity = getActivity();
-
- findPreference(PREF_GPODNET_LOGIN).setOnPreferenceClickListener(preference -> {
- new GpodderAuthenticationFragment().show(getChildFragmentManager(), GpodderAuthenticationFragment.TAG);
- return true;
- });
- findPreference(PREF_GPODNET_SETLOGIN_INFORMATION)
- .setOnPreferenceClickListener(preference -> {
- AuthenticationDialog dialog = new AuthenticationDialog(activity,
- R.string.pref_gpodnet_setlogin_information_title, false, GpodnetPreferences.getUsername(),
- null) {
-
- @Override
- protected void onConfirmed(String username, String password) {
- GpodnetPreferences.setPassword(password);
- }
- };
- dialog.show();
- return true;
- });
- findPreference(PREF_GPODNET_SYNC).setOnPreferenceClickListener(preference -> {
- SyncService.syncImmediately(getActivity().getApplicationContext());
- return true;
- });
- findPreference(PREF_GPODNET_FORCE_FULL_SYNC).setOnPreferenceClickListener(preference -> {
- SyncService.fullSync(getContext());
- return true;
- });
- findPreference(PREF_GPODNET_LOGOUT).setOnPreferenceClickListener(preference -> {
- GpodnetPreferences.logout();
- Snackbar.make(getView(), R.string.pref_gpodnet_logout_toast, Snackbar.LENGTH_LONG).show();
- updateGpodnetPreferenceScreen();
- return true;
- });
- }
-
- private void updateGpodnetPreferenceScreen() {
- final boolean loggedIn = GpodnetPreferences.loggedIn();
- findPreference(PREF_GPODNET_LOGIN).setEnabled(!loggedIn);
- findPreference(PREF_GPODNET_SETLOGIN_INFORMATION).setEnabled(loggedIn);
- findPreference(PREF_GPODNET_SYNC).setEnabled(loggedIn);
- findPreference(PREF_GPODNET_FORCE_FULL_SYNC).setEnabled(loggedIn);
- findPreference(PREF_GPODNET_LOGOUT).setEnabled(loggedIn);
- if (loggedIn) {
- String format = getActivity().getString(R.string.pref_gpodnet_login_status);
- String summary = String.format(format, GpodnetPreferences.getUsername(),
- GpodnetPreferences.getDeviceID());
- Spanned formattedSummary = HtmlCompat.fromHtml(summary, HtmlCompat.FROM_HTML_MODE_LEGACY);
- findPreference(PREF_GPODNET_LOGOUT).setSummary(formattedSummary);
- updateLastGpodnetSyncReport(SyncService.isLastSyncSuccessful(getContext()),
- SyncService.getLastSyncAttempt(getContext()));
- } else {
- findPreference(PREF_GPODNET_LOGOUT).setSummary(null);
- }
- }
-
- private void updateLastGpodnetSyncReport(boolean successful, long lastTime) {
- String status = String.format("%1$s (%2$s)", getString(successful
- ? R.string.gpodnetsync_pref_report_successful : R.string.gpodnetsync_pref_report_failed),
- DateUtils.getRelativeDateTimeString(getContext(),
- lastTime, DateUtils.MINUTE_IN_MILLIS, DateUtils.WEEK_IN_MILLIS, DateUtils.FORMAT_SHOW_TIME));
- ((PreferenceActivity) getActivity()).getSupportActionBar().setSubtitle(status);
- }
-}
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 f6aa45e93..5156de432 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
@@ -8,10 +8,15 @@ import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.net.Uri;
-import android.os.Build;
import android.os.Bundle;
-import android.os.Environment;
import android.util.Log;
+
+import androidx.activity.result.ActivityResult;
+import androidx.activity.result.ActivityResultLauncher;
+import androidx.activity.result.contract.ActivityResultContracts;
+import androidx.activity.result.contract.ActivityResultContracts.GetContent;
+import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult;
+import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.core.content.FileProvider;
import androidx.preference.PreferenceFragmentCompat;
@@ -35,7 +40,6 @@ import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
import java.io.File;
-import java.io.FileOutputStream;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
@@ -54,13 +58,19 @@ public class ImportExportPreferencesFragment extends PreferenceFragmentCompat {
private static final String DEFAULT_HTML_OUTPUT_NAME = "antennapod-feeds-%s.html";
private static final String CONTENT_TYPE_HTML = "text/html";
private static final String DEFAULT_FAVORITES_OUTPUT_NAME = "antennapod-favorites-%s.html";
- private static final int REQUEST_CODE_CHOOSE_OPML_EXPORT_PATH = 1;
- private static final int REQUEST_CODE_CHOOSE_OPML_IMPORT_PATH = 2;
- private static final int REQUEST_CODE_CHOOSE_HTML_EXPORT_PATH = 3;
- private static final int REQUEST_CODE_RESTORE_DATABASE = 4;
- private static final int REQUEST_CODE_BACKUP_DATABASE = 5;
- private static final int REQUEST_CODE_CHOOSE_FAVORITES_EXPORT_PATH = 6;
private static final String DATABASE_EXPORT_FILENAME = "AntennaPodBackup-%s.db";
+ private final ActivityResultLauncher<Intent> chooseOpmlExportPathLauncher =
+ registerForActivityResult(new StartActivityForResult(), this::chooseOpmlExportPathResult);
+ private final ActivityResultLauncher<Intent> chooseHtmlExportPathLauncher =
+ registerForActivityResult(new StartActivityForResult(), this::chooseHtmlExportPathResult);
+ private final ActivityResultLauncher<Intent> chooseFavoritesExportPathLauncher =
+ registerForActivityResult(new StartActivityForResult(), this::chooseFavoritesExportPathResult);
+ private final ActivityResultLauncher<Intent> restoreDatabaseLauncher =
+ registerForActivityResult(new StartActivityForResult(), this::restoreDatabaseResult);
+ private final ActivityResultLauncher<String> backupDatabaseLauncher =
+ registerForActivityResult(new BackupDatabase(), this::backupDatabaseResult);
+ private final ActivityResultLauncher<String> chooseOpmlImportPathLauncher =
+ registerForActivityResult(new GetContent(), this::chooseOpmlImportPathResult);
private Disposable disposable;
private ProgressDialog progressDialog;
@@ -95,23 +105,20 @@ public class ImportExportPreferencesFragment extends PreferenceFragmentCompat {
findPreference(PREF_OPML_EXPORT).setOnPreferenceClickListener(
preference -> {
openExportPathPicker(CONTENT_TYPE_OPML, dateStampFilename(DEFAULT_OPML_OUTPUT_NAME),
- REQUEST_CODE_CHOOSE_OPML_EXPORT_PATH, new OpmlWriter());
+ chooseOpmlExportPathLauncher, new OpmlWriter());
return true;
}
);
findPreference(PREF_HTML_EXPORT).setOnPreferenceClickListener(
preference -> {
openExportPathPicker(CONTENT_TYPE_HTML, dateStampFilename(DEFAULT_HTML_OUTPUT_NAME),
- REQUEST_CODE_CHOOSE_HTML_EXPORT_PATH, new HtmlWriter());
+ chooseHtmlExportPathLauncher, new HtmlWriter());
return true;
});
findPreference(PREF_OPML_IMPORT).setOnPreferenceClickListener(
preference -> {
try {
- Intent intentGetContentAction = new Intent(Intent.ACTION_GET_CONTENT);
- intentGetContentAction.addCategory(Intent.CATEGORY_OPENABLE);
- intentGetContentAction.setType("*/*");
- startActivityForResult(intentGetContentAction, REQUEST_CODE_CHOOSE_OPML_IMPORT_PATH);
+ chooseOpmlImportPathLauncher.launch("*/*");
} catch (ActivityNotFoundException e) {
Log.e(TAG, "No activity found. Should never happen...");
}
@@ -130,7 +137,7 @@ public class ImportExportPreferencesFragment extends PreferenceFragmentCompat {
findPreference(PREF_FAVORITE_EXPORT).setOnPreferenceClickListener(
preference -> {
openExportPathPicker(CONTENT_TYPE_HTML, dateStampFilename(DEFAULT_FAVORITES_OUTPUT_NAME),
- REQUEST_CODE_CHOOSE_FAVORITES_EXPORT_PATH, new FavoritesWriter());
+ chooseFavoritesExportPathLauncher, new FavoritesWriter());
return true;
});
}
@@ -159,26 +166,7 @@ public class ImportExportPreferencesFragment extends PreferenceFragmentCompat {
}
private void exportDatabase() {
- if (Build.VERSION.SDK_INT >= 19) {
- Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT)
- .addCategory(Intent.CATEGORY_OPENABLE)
- .setType("application/x-sqlite3")
- .putExtra(Intent.EXTRA_TITLE, dateStampFilename(DATABASE_EXPORT_FILENAME));
-
- startActivityForResult(intent, REQUEST_CODE_BACKUP_DATABASE);
- } else {
- File sd = Environment.getExternalStorageDirectory();
- File backupDB = new File(sd, dateStampFilename(DATABASE_EXPORT_FILENAME));
- progressDialog.show();
- disposable = Completable.fromAction(() ->
- DatabaseExporter.exportToStream(new FileOutputStream(backupDB), getContext()))
- .subscribeOn(Schedulers.io())
- .observeOn(AndroidSchedulers.mainThread())
- .subscribe(() -> {
- Snackbar.make(getView(), R.string.export_success_title, Snackbar.LENGTH_LONG).show();
- progressDialog.dismiss();
- }, this::showExportErrorDialog);
- }
+ backupDatabaseLauncher.launch(dateStampFilename(DATABASE_EXPORT_FILENAME));
}
private void importDatabase() {
@@ -190,18 +178,10 @@ public class ImportExportPreferencesFragment extends PreferenceFragmentCompat {
// add a button
builder.setNegativeButton(R.string.no, null);
builder.setPositiveButton(R.string.confirm_label, (dialog, which) -> {
- if (Build.VERSION.SDK_INT >= 19) {
- Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
- intent.setType("*/*");
- startActivityForResult(intent, REQUEST_CODE_RESTORE_DATABASE);
- } else {
- Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
- intent.setType("*/*");
- startActivityForResult(Intent.createChooser(intent,
- getString(R.string.import_select_file)), REQUEST_CODE_RESTORE_DATABASE);
- }
- }
- );
+ Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
+ intent.setType("*/*");
+ restoreDatabaseLauncher.launch(intent);
+ });
// create and show the alert dialog
builder.show();
@@ -227,15 +207,14 @@ public class ImportExportPreferencesFragment extends PreferenceFragmentCompat {
sendIntent.putExtra(Intent.EXTRA_STREAM, streamUri);
sendIntent.setType("text/plain");
sendIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
- if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) {
- 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);
- }
+ 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(Intent.createChooser(sendIntent, getString(R.string.send_label)));
+ getContext().startActivity(chooserIntent);
});
alert.create().show();
}
@@ -249,64 +228,97 @@ public class ImportExportPreferencesFragment extends PreferenceFragmentCompat {
alert.show();
}
- @Override
- public void onActivityResult(int requestCode, int resultCode, Intent data) {
- if (resultCode != Activity.RESULT_OK || data == null) {
+ private void chooseOpmlExportPathResult(final ActivityResult result) {
+ if (result.getResultCode() != Activity.RESULT_OK || result.getData() == null) {
return;
}
- Uri uri = data.getData();
+ final Uri uri = result.getData().getData();
+ exportWithWriter(new OpmlWriter(), uri);
+ }
- if (requestCode == REQUEST_CODE_CHOOSE_OPML_EXPORT_PATH) {
- exportWithWriter(new OpmlWriter(), uri);
- } else if (requestCode == REQUEST_CODE_CHOOSE_HTML_EXPORT_PATH) {
- exportWithWriter(new HtmlWriter(), uri);
- } else if (requestCode == REQUEST_CODE_CHOOSE_FAVORITES_EXPORT_PATH) {
- exportWithWriter(new FavoritesWriter(), uri);
- } else if (requestCode == REQUEST_CODE_RESTORE_DATABASE) {
- progressDialog.show();
- disposable = Completable.fromAction(() -> DatabaseExporter.importBackup(uri, getContext()))
- .subscribeOn(Schedulers.io())
- .observeOn(AndroidSchedulers.mainThread())
- .subscribe(() -> {
- showDatabaseImportSuccessDialog();
- UserPreferences.unsetUsageCountingDate();
- progressDialog.dismiss();
- }, this::showExportErrorDialog);
- } else if (requestCode == REQUEST_CODE_BACKUP_DATABASE) {
- progressDialog.show();
- disposable = Completable.fromAction(() -> DatabaseExporter.exportToDocument(uri, getContext()))
- .subscribeOn(Schedulers.io())
- .observeOn(AndroidSchedulers.mainThread())
- .subscribe(() -> {
- Snackbar.make(getView(), R.string.export_success_title, Snackbar.LENGTH_LONG).show();
- progressDialog.dismiss();
- }, this::showExportErrorDialog);
- } else if (requestCode == REQUEST_CODE_CHOOSE_OPML_IMPORT_PATH) {
- Intent intent = new Intent(getContext(), OpmlImportActivity.class);
- intent.setData(uri);
- startActivity(intent);
+ private void chooseHtmlExportPathResult(final ActivityResult result) {
+ if (result.getResultCode() != Activity.RESULT_OK || result.getData() == null) {
+ return;
}
+ final Uri uri = result.getData().getData();
+ exportWithWriter(new HtmlWriter(), uri);
}
- private void openExportPathPicker(String contentType, String title, int requestCode, ExportWriter writer) {
- if (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR2) {
- Intent intentPickAction = new Intent(Intent.ACTION_CREATE_DOCUMENT)
- .addCategory(Intent.CATEGORY_OPENABLE)
- .setType(contentType)
- .putExtra(Intent.EXTRA_TITLE, title);
+ private void chooseFavoritesExportPathResult(final ActivityResult result) {
+ if (result.getResultCode() != Activity.RESULT_OK || result.getData() == null) {
+ return;
+ }
+ final Uri uri = result.getData().getData();
+ exportWithWriter(new FavoritesWriter(), uri);
+ }
- // Creates an implicit intent to launch a file manager which lets
- // the user choose a specific directory to export to.
- try {
- startActivityForResult(intentPickAction, requestCode);
- return;
- } catch (ActivityNotFoundException e) {
- Log.e(TAG, "No activity found. Should never happen...");
- }
+ private void restoreDatabaseResult(final ActivityResult result) {
+ if (result.getResultCode() != Activity.RESULT_OK || result.getData() == null) {
+ return;
+ }
+ final Uri uri = result.getData().getData();
+ progressDialog.show();
+ disposable = Completable.fromAction(() -> DatabaseExporter.importBackup(uri, getContext()))
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(() -> {
+ showDatabaseImportSuccessDialog();
+ UserPreferences.unsetUsageCountingDate();
+ progressDialog.dismiss();
+ }, this::showExportErrorDialog);
+ }
+
+ private void backupDatabaseResult(final Uri uri) {
+ if (uri == null) {
+ return;
+ }
+ progressDialog.show();
+ disposable = Completable.fromAction(() -> DatabaseExporter.exportToDocument(uri, getContext()))
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(() -> {
+ Snackbar.make(getView(), R.string.export_success_title, Snackbar.LENGTH_LONG).show();
+ progressDialog.dismiss();
+ }, this::showExportErrorDialog);
+ }
+
+ private void chooseOpmlImportPathResult(final Uri uri) {
+ if (uri == null) {
+ return;
+ }
+ final Intent intent = new Intent(getContext(), OpmlImportActivity.class);
+ intent.setData(uri);
+ startActivity(intent);
+ }
+
+ private void openExportPathPicker(String contentType, String title,
+ final ActivityResultLauncher<Intent> result, ExportWriter writer) {
+ Intent intentPickAction = new Intent(Intent.ACTION_CREATE_DOCUMENT)
+ .addCategory(Intent.CATEGORY_OPENABLE)
+ .setType(contentType)
+ .putExtra(Intent.EXTRA_TITLE, title);
+
+ // Creates an implicit intent to launch a file manager which lets
+ // the user choose a specific directory to export to.
+ try {
+ result.launch(intentPickAction);
+ return;
+ } catch (ActivityNotFoundException e) {
+ Log.e(TAG, "No activity found. Should never happen...");
}
// If we are using a SDK lower than API 21 or the implicit intent failed
// fallback to the legacy export process
exportWithWriter(writer, null);
}
+
+ private static class BackupDatabase extends ActivityResultContracts.CreateDocument {
+ @NonNull
+ @Override
+ public Intent createIntent(@NonNull final Context context, @NonNull final String input) {
+ return super.createIntent(context, input)
+ .addCategory(Intent.CATEGORY_OPENABLE)
+ .setType("application/x-sqlite3");
+ }
+ }
}
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 cc09acbca..891d3737b 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
@@ -1,6 +1,8 @@
package de.danoeh.antennapod.fragment.preferences;
import android.content.Intent;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffColorFilter;
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
@@ -17,12 +19,11 @@ import de.danoeh.antennapod.core.util.IntentUtils;
import de.danoeh.antennapod.fragment.preferences.about.AboutFragment;
public class MainPreferencesFragment extends PreferenceFragmentCompat {
- private static final String TAG = "MainPreferencesFragment";
private static final String PREF_SCREEN_USER_INTERFACE = "prefScreenInterface";
private static final String PREF_SCREEN_PLAYBACK = "prefScreenPlayback";
private static final String PREF_SCREEN_NETWORK = "prefScreenNetwork";
- private static final String PREF_SCREEN_GPODDER = "prefScreenGpodder";
+ private static final String PREF_SCREEN_SYNCHRONIZATION = "prefScreenSynchronization";
private static final String PREF_SCREEN_STORAGE = "prefScreenStorage";
private static final String PREF_DOCUMENTATION = "prefDocumentation";
private static final String PREF_VIEW_FORUM = "prefViewForum";
@@ -43,15 +44,26 @@ public class MainPreferencesFragment extends PreferenceFragmentCompat {
// and afterwards remove the following lines. Please keep in mind that AntennaPod is licensed under the GPL.
// This means that your application needs to be open-source under the GPL, too.
// It must also include a prominent copyright notice.
- String packageName = getContext().getPackageName();
- if (!"de.danoeh.antennapod".equals(packageName) && !"de.danoeh.antennapod.debug".equals(packageName)) {
+ int packageHash = getContext().getPackageName().hashCode();
+ if (packageHash != 1790437538 && packageHash != -1190467065) {
findPreference(PREF_CATEGORY_PROJECT).setVisible(false);
Preference copyrightNotice = new Preference(getContext());
+ copyrightNotice.setIcon(R.drawable.ic_info_white);
+ copyrightNotice.getIcon().mutate()
+ .setColorFilter(new PorterDuffColorFilter(0xffcc0000, PorterDuff.Mode.MULTIPLY));
copyrightNotice.setSummary("This application is based on AntennaPod."
+ " The AntennaPod team does NOT provide support for this unofficial version."
+ " If you can read this message, the developers of this modification"
+ " violate the GNU General Public License (GPL).");
findPreference(PREF_CATEGORY_PROJECT).getParent().addPreference(copyrightNotice);
+ } else if (packageHash == -1190467065) {
+ Preference debugNotice = new Preference(getContext());
+ debugNotice.setIcon(R.drawable.ic_info_white);
+ debugNotice.getIcon().mutate()
+ .setColorFilter(new PorterDuffColorFilter(0xffcc0000, PorterDuff.Mode.MULTIPLY));
+ debugNotice.setOrder(-1);
+ debugNotice.setSummary("This is a development version of AntennaPod and not meant for daily use");
+ findPreference(PREF_CATEGORY_PROJECT).getParent().addPreference(debugNotice);
}
}
@@ -74,8 +86,8 @@ public class MainPreferencesFragment extends PreferenceFragmentCompat {
((PreferenceActivity) getActivity()).openScreen(R.xml.preferences_network);
return true;
});
- findPreference(PREF_SCREEN_GPODDER).setOnPreferenceClickListener(preference -> {
- ((PreferenceActivity) getActivity()).openScreen(R.xml.preferences_gpodder);
+ findPreference(PREF_SCREEN_SYNCHRONIZATION).setOnPreferenceClickListener(preference -> {
+ ((PreferenceActivity) getActivity()).openScreen(R.xml.preferences_synchronization);
return true;
});
findPreference(PREF_SCREEN_STORAGE).setOnPreferenceClickListener(preference -> {
@@ -142,8 +154,8 @@ public class MainPreferencesFragment extends PreferenceFragmentCompat {
.addBreadcrumb(PreferenceActivity.getTitleOfPage(R.xml.preferences_network))
.addBreadcrumb(R.string.automation)
.addBreadcrumb(PreferenceActivity.getTitleOfPage(R.xml.preferences_autodownload));
- config.index(R.xml.preferences_gpodder)
- .addBreadcrumb(PreferenceActivity.getTitleOfPage(R.xml.preferences_gpodder));
+ config.index(R.xml.preferences_synchronization)
+ .addBreadcrumb(PreferenceActivity.getTitleOfPage(R.xml.preferences_synchronization));
config.index(R.xml.preferences_notifications)
.addBreadcrumb(PreferenceActivity.getTitleOfPage(R.xml.preferences_notifications));
config.index(R.xml.feed_settings)
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/NotificationPreferencesFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/NotificationPreferencesFragment.java
index 94e151f7a..ba17cedb2 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/NotificationPreferencesFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/NotificationPreferencesFragment.java
@@ -4,11 +4,10 @@ import android.os.Bundle;
import androidx.preference.PreferenceFragmentCompat;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.PreferenceActivity;
-import de.danoeh.antennapod.core.preferences.GpodnetPreferences;
+import de.danoeh.antennapod.core.sync.SynchronizationSettings;
public class NotificationPreferencesFragment extends PreferenceFragmentCompat {
- private static final String TAG = "NotificationPrefFragment";
private static final String PREF_GPODNET_NOTIFICATIONS = "pref_gpodnet_notifications";
@Override
@@ -24,7 +23,6 @@ public class NotificationPreferencesFragment extends PreferenceFragmentCompat {
}
private void setUpScreen() {
- final boolean loggedIn = GpodnetPreferences.loggedIn();
- findPreference(PREF_GPODNET_NOTIFICATIONS).setEnabled(loggedIn);
+ findPreference(PREF_GPODNET_NOTIFICATIONS).setEnabled(SynchronizationSettings.isProviderConnected());
}
}
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 1fa1fed58..7fa2ed4d1 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
@@ -10,13 +10,12 @@ import androidx.preference.Preference;
import androidx.preference.PreferenceFragmentCompat;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.PreferenceActivity;
-import de.danoeh.antennapod.core.event.UnreadItemsUpdateEvent;
+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 de.danoeh.antennapod.preferences.PreferenceControllerFlavorHelper;
import java.util.Map;
import org.greenrobot.eventbus.EventBus;
@@ -31,7 +30,6 @@ public class PlaybackPreferencesFragment extends PreferenceFragmentCompat {
addPreferencesFromResource(R.xml.preferences_playback);
setupPlaybackScreen();
- PreferenceControllerFlavorHelper.setupFlavoredUI(this);
buildSmartMarkAsPlayedPreference();
}
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
index 208ede8cc..ba6164212 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/PlaybackStatisticsFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/PlaybackStatisticsFragment.java
@@ -28,7 +28,6 @@ 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 de.danoeh.antennapod.core.util.comparator.CompareCompat;
import io.reactivex.Completable;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
@@ -68,7 +67,7 @@ public class PlaybackStatisticsFragment extends Fragment {
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(getContext());
+ listAdapter = new PlaybackStatisticsListAdapter(this);
listAdapter.setCountAll(countAll);
feedStatisticsList.setLayoutManager(new LinearLayoutManager(getContext()));
feedStatisticsList.setAdapter(listAdapter);
@@ -188,10 +187,10 @@ public class PlaybackStatisticsFragment extends Fragment {
List<StatisticsItem> statisticsData = DBReader.getStatistics();
if (countAll) {
Collections.sort(statisticsData, (item1, item2) ->
- CompareCompat.compareLong(item1.timePlayedCountAll, item2.timePlayedCountAll));
+ Long.compare(item2.timePlayedCountAll, item1.timePlayedCountAll));
} else {
Collections.sort(statisticsData, (item1, item2) ->
- CompareCompat.compareLong(item1.timePlayed, item2.timePlayed));
+ Long.compare(item2.timePlayed, item1.timePlayed));
}
return statisticsData;
}
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/UserInterfacePreferencesFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/UserInterfacePreferencesFragment.java
index 04b9677e2..ff974179e 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/UserInterfacePreferencesFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/UserInterfacePreferencesFragment.java
@@ -10,8 +10,8 @@ import androidx.preference.PreferenceFragmentCompat;
import android.widget.ListView;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.PreferenceActivity;
-import de.danoeh.antennapod.core.event.PlayerStatusEvent;
-import de.danoeh.antennapod.core.event.UnreadItemsUpdateEvent;
+import de.danoeh.antennapod.event.PlayerStatusEvent;
+import de.danoeh.antennapod.event.UnreadItemsUpdateEvent;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.dialog.SubscriptionsFilterDialog;
import de.danoeh.antennapod.dialog.FeedSortDialog;
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/GpodderAuthenticationFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/synchronization/GpodderAuthenticationFragment.java
index c0bf3e0ea..9dfe6840c 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/GpodderAuthenticationFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/synchronization/GpodderAuthenticationFragment.java
@@ -1,4 +1,4 @@
-package de.danoeh.antennapod.fragment.preferences;
+package de.danoeh.antennapod.fragment.preferences.synchronization;
import android.app.Dialog;
import android.content.Context;
@@ -15,30 +15,35 @@ import android.widget.ProgressBar;
import android.widget.RadioGroup;
import android.widget.TextView;
import android.widget.ViewFlipper;
+
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.DialogFragment;
+
import com.google.android.material.button.MaterialButton;
import com.google.android.material.textfield.TextInputLayout;
+
+import java.util.List;
+import java.util.Locale;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
import de.danoeh.antennapod.R;
-import de.danoeh.antennapod.core.preferences.GpodnetPreferences;
+import de.danoeh.antennapod.core.sync.SynchronizationCredentials;
import de.danoeh.antennapod.core.service.download.AntennapodHttpClient;
import de.danoeh.antennapod.core.sync.SyncService;
-import de.danoeh.antennapod.net.sync.gpoddernet.GpodnetService;
-import de.danoeh.antennapod.net.sync.gpoddernet.model.GpodnetDevice;
+import de.danoeh.antennapod.core.sync.SynchronizationProviderViewData;
+import de.danoeh.antennapod.core.sync.SynchronizationSettings;
import de.danoeh.antennapod.core.util.FileNameGenerator;
import de.danoeh.antennapod.core.util.IntentUtils;
+import de.danoeh.antennapod.net.sync.gpoddernet.GpodnetService;
+import de.danoeh.antennapod.net.sync.gpoddernet.model.GpodnetDevice;
import io.reactivex.Completable;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
-import java.util.List;
-import java.util.Locale;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
/**
* Guides the user through the authentication process.
*/
@@ -83,23 +88,24 @@ public class GpodderAuthenticationFragment extends DialogFragment {
final RadioGroup serverRadioGroup = view.findViewById(R.id.serverRadioGroup);
final EditText serverUrlText = view.findViewById(R.id.serverUrlText);
- if (!GpodnetService.DEFAULT_BASE_HOST.equals(GpodnetPreferences.getHosturl())) {
- serverUrlText.setText(GpodnetPreferences.getHosturl());
+ if (!GpodnetService.DEFAULT_BASE_HOST.equals(SynchronizationCredentials.getHosturl())) {
+ serverUrlText.setText(SynchronizationCredentials.getHosturl());
}
final TextInputLayout serverUrlTextInput = view.findViewById(R.id.serverUrlTextInput);
serverRadioGroup.setOnCheckedChangeListener((group, checkedId) -> {
serverUrlTextInput.setVisibility(checkedId == R.id.customServerRadio ? View.VISIBLE : View.GONE);
});
selectHost.setOnClickListener(v -> {
+ SynchronizationCredentials.clear(getContext());
if (serverRadioGroup.getCheckedRadioButtonId() == R.id.customServerRadio) {
- GpodnetPreferences.setHosturl(serverUrlText.getText().toString());
+ SynchronizationCredentials.setHosturl(serverUrlText.getText().toString());
} else {
- GpodnetPreferences.setHosturl(GpodnetService.DEFAULT_BASE_HOST);
+ SynchronizationCredentials.setHosturl(GpodnetService.DEFAULT_BASE_HOST);
}
service = new GpodnetService(AntennapodHttpClient.getHttpClient(),
- GpodnetPreferences.getHosturl(), GpodnetPreferences.getDeviceID(),
- GpodnetPreferences.getUsername(), GpodnetPreferences.getPassword());
- getDialog().setTitle(GpodnetPreferences.getHosturl());
+ SynchronizationCredentials.getHosturl(), SynchronizationCredentials.getDeviceID(),
+ SynchronizationCredentials.getUsername(), SynchronizationCredentials.getPassword());
+ getDialog().setTitle(SynchronizationCredentials.getHosturl());
advance();
});
}
@@ -116,7 +122,7 @@ public class GpodderAuthenticationFragment extends DialogFragment {
createAccount.setPaintFlags(createAccount.getPaintFlags() | Paint.UNDERLINE_TEXT_FLAG);
createAccount.setOnClickListener(v -> IntentUtils.openInBrowser(getContext(), "https://gpodder.net/register/"));
- if (GpodnetPreferences.getHosturl().startsWith("http://")) {
+ if (SynchronizationCredentials.getHosturl().startsWith("http://")) {
createAccountWarning.setVisibility(View.VISIBLE);
}
password.setOnEditorActionListener((v, actionID, event) ->
@@ -265,15 +271,8 @@ public class GpodderAuthenticationFragment extends DialogFragment {
});
}
- private void writeLoginCredentials() {
- GpodnetPreferences.setUsername(username);
- GpodnetPreferences.setPassword(password);
- GpodnetPreferences.setDeviceID(selectedDevice.getId());
- }
-
private void advance() {
if (currentStep < STEP_FINISH) {
-
View view = viewFlipper.getChildAt(currentStep + 1);
if (currentStep == STEP_DEFAULT) {
setupHostView(view);
@@ -289,7 +288,10 @@ public class GpodderAuthenticationFragment extends DialogFragment {
if (selectedDevice == null) {
throw new IllegalStateException("Device must not be null here");
} else {
- writeLoginCredentials();
+ SynchronizationSettings.setSelectedSyncProvider(SynchronizationProviderViewData.GPODDER_NET);
+ SynchronizationCredentials.setUsername(username);
+ SynchronizationCredentials.setPassword(password);
+ SynchronizationCredentials.setDeviceID(selectedDevice.getId());
setupFinishView(view);
}
}
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/synchronization/NextcloudAuthenticationFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/synchronization/NextcloudAuthenticationFragment.java
new file mode 100644
index 000000000..2e9260c1d
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/synchronization/NextcloudAuthenticationFragment.java
@@ -0,0 +1,92 @@
+package de.danoeh.antennapod.fragment.preferences.synchronization;
+
+import android.app.Dialog;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.view.View;
+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.core.service.download.AntennapodHttpClient;
+import de.danoeh.antennapod.core.sync.SyncService;
+import de.danoeh.antennapod.core.sync.SynchronizationCredentials;
+import de.danoeh.antennapod.core.sync.SynchronizationProviderViewData;
+import de.danoeh.antennapod.core.sync.SynchronizationSettings;
+import de.danoeh.antennapod.databinding.NextcloudAuthDialogBinding;
+import de.danoeh.antennapod.net.sync.nextcloud.NextcloudLoginFlow;
+
+/**
+ * Guides the user through the authentication process.
+ */
+public class NextcloudAuthenticationFragment extends DialogFragment
+ implements NextcloudLoginFlow.AuthenticationCallback {
+ public static final String TAG = "NextcloudAuthenticationFragment";
+ private NextcloudAuthDialogBinding viewBinding;
+ private NextcloudLoginFlow nextcloudLoginFlow;
+ private boolean shouldDismiss = false;
+
+ @NonNull
+ @Override
+ public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
+ AlertDialog.Builder dialog = new AlertDialog.Builder(getContext());
+ dialog.setTitle(R.string.gpodnetauth_login_butLabel);
+ dialog.setNegativeButton(R.string.cancel_label, null);
+ dialog.setCancelable(false);
+ this.setCancelable(false);
+
+ viewBinding = NextcloudAuthDialogBinding.inflate(getLayoutInflater());
+ dialog.setView(viewBinding.getRoot());
+
+ viewBinding.loginButton.setOnClickListener(v -> {
+ viewBinding.errorText.setVisibility(View.GONE);
+ viewBinding.loginButton.setVisibility(View.GONE);
+ viewBinding.loginProgressContainer.setVisibility(View.VISIBLE);
+ nextcloudLoginFlow = new NextcloudLoginFlow(AntennapodHttpClient.getHttpClient(),
+ viewBinding.serverUrlText.getText().toString(), getContext(), this);
+ nextcloudLoginFlow.start();
+ });
+
+ return dialog.create();
+ }
+
+ @Override
+ public void onDismiss(@NonNull DialogInterface dialog) {
+ super.onDismiss(dialog);
+ if (nextcloudLoginFlow != null) {
+ nextcloudLoginFlow.cancel();
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ if (shouldDismiss) {
+ dismiss();
+ }
+ }
+
+ @Override
+ public void onNextcloudAuthenticated(String server, String username, String password) {
+ SynchronizationSettings.setSelectedSyncProvider(SynchronizationProviderViewData.NEXTCLOUD_GPODDER);
+ SynchronizationCredentials.clear(getContext());
+ SynchronizationCredentials.setPassword(password);
+ SynchronizationCredentials.setHosturl(server);
+ SynchronizationCredentials.setUsername(username);
+ SyncService.fullSync(getContext());
+ if (isVisible()) {
+ dismiss();
+ } else {
+ shouldDismiss = true;
+ }
+ }
+
+ @Override
+ public void onNextcloudAuthError(String errorMessage) {
+ viewBinding.loginProgressContainer.setVisibility(View.GONE);
+ viewBinding.errorText.setVisibility(View.VISIBLE);
+ viewBinding.errorText.setText(errorMessage);
+ viewBinding.loginButton.setVisibility(View.VISIBLE);
+ }
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/synchronization/SynchronizationPreferencesFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/synchronization/SynchronizationPreferencesFragment.java
new file mode 100644
index 000000000..8cb7f45db
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/synchronization/SynchronizationPreferencesFragment.java
@@ -0,0 +1,222 @@
+package de.danoeh.antennapod.fragment.preferences.synchronization;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.text.Spanned;
+import android.text.format.DateUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.ImageView;
+import android.widget.ListAdapter;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.AlertDialog;
+import androidx.core.text.HtmlCompat;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceFragmentCompat;
+
+import com.google.android.material.snackbar.Snackbar;
+
+import org.greenrobot.eventbus.EventBus;
+import org.greenrobot.eventbus.Subscribe;
+import org.greenrobot.eventbus.ThreadMode;
+
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.activity.PreferenceActivity;
+import de.danoeh.antennapod.event.SyncServiceEvent;
+import de.danoeh.antennapod.core.sync.SynchronizationCredentials;
+import de.danoeh.antennapod.core.sync.SyncService;
+import de.danoeh.antennapod.core.sync.SynchronizationProviderViewData;
+import de.danoeh.antennapod.core.sync.SynchronizationSettings;
+import de.danoeh.antennapod.dialog.AuthenticationDialog;
+
+public class SynchronizationPreferencesFragment extends PreferenceFragmentCompat {
+ private static final String PREFERENCE_SYNCHRONIZATION_DESCRIPTION = "preference_synchronization_description";
+ private static final String PREFERENCE_GPODNET_SETLOGIN_INFORMATION = "pref_gpodnet_setlogin_information";
+ private static final String PREFERENCE_SYNC = "pref_synchronization_sync";
+ private static final String PREFERENCE_FORCE_FULL_SYNC = "pref_synchronization_force_full_sync";
+ private static final String PREFERENCE_LOGOUT = "pref_synchronization_logout";
+
+ @Override
+ public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
+ addPreferencesFromResource(R.xml.preferences_synchronization);
+ setupScreen();
+ updateScreen();
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ ((PreferenceActivity) getActivity()).getSupportActionBar().setTitle(R.string.synchronization_pref);
+ updateScreen();
+ EventBus.getDefault().register(this);
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ EventBus.getDefault().unregister(this);
+ ((PreferenceActivity) getActivity()).getSupportActionBar().setSubtitle("");
+ }
+
+ @Subscribe(threadMode = ThreadMode.MAIN, sticky = true)
+ public void syncStatusChanged(SyncServiceEvent event) {
+ if (!SynchronizationSettings.isProviderConnected()) {
+ return;
+ }
+ updateScreen();
+ if (event.getMessageResId() == R.string.sync_status_error
+ || event.getMessageResId() == R.string.sync_status_success) {
+ updateLastSyncReport(SynchronizationSettings.isLastSyncSuccessful(),
+ SynchronizationSettings.getLastSyncAttempt());
+ } else {
+ ((PreferenceActivity) getActivity()).getSupportActionBar().setSubtitle(event.getMessageResId());
+ }
+ }
+
+ private void setupScreen() {
+ final Activity activity = getActivity();
+ findPreference(PREFERENCE_GPODNET_SETLOGIN_INFORMATION)
+ .setOnPreferenceClickListener(preference -> {
+ AuthenticationDialog dialog = new AuthenticationDialog(activity,
+ R.string.pref_gpodnet_setlogin_information_title,
+ false, SynchronizationCredentials.getUsername(), null) {
+ @Override
+ protected void onConfirmed(String username, String password) {
+ SynchronizationCredentials.setPassword(password);
+ }
+ };
+ dialog.show();
+ return true;
+ });
+ findPreference(PREFERENCE_SYNC).setOnPreferenceClickListener(preference -> {
+ SyncService.syncImmediately(getActivity().getApplicationContext());
+ return true;
+ });
+ findPreference(PREFERENCE_FORCE_FULL_SYNC).setOnPreferenceClickListener(preference -> {
+ SyncService.fullSync(getContext());
+ return true;
+ });
+ findPreference(PREFERENCE_LOGOUT).setOnPreferenceClickListener(preference -> {
+ SynchronizationCredentials.clear(getContext());
+ Snackbar.make(getView(), R.string.pref_synchronization_logout_toast, Snackbar.LENGTH_LONG).show();
+ SynchronizationSettings.setSelectedSyncProvider(null);
+ updateScreen();
+ return true;
+ });
+ }
+
+ private void updateScreen() {
+ final boolean loggedIn = SynchronizationSettings.isProviderConnected();
+ Preference preferenceHeader = findPreference(PREFERENCE_SYNCHRONIZATION_DESCRIPTION);
+ if (loggedIn) {
+ SynchronizationProviderViewData selectedProvider =
+ SynchronizationProviderViewData.fromIdentifier(getSelectedSyncProviderKey());
+ preferenceHeader.setTitle("");
+ preferenceHeader.setSummary(selectedProvider.getSummaryResource());
+ preferenceHeader.setIcon(selectedProvider.getIconResource());
+ preferenceHeader.setOnPreferenceClickListener(null);
+ } else {
+ preferenceHeader.setTitle(R.string.synchronization_choose_title);
+ preferenceHeader.setSummary(R.string.synchronization_summary_unchoosen);
+ preferenceHeader.setIcon(R.drawable.ic_cloud);
+ preferenceHeader.setOnPreferenceClickListener((preference) -> {
+ chooseProviderAndLogin();
+ return true;
+ });
+ }
+
+ Preference gpodnetSetLoginPreference = findPreference(PREFERENCE_GPODNET_SETLOGIN_INFORMATION);
+ gpodnetSetLoginPreference.setVisible(isProviderSelected(SynchronizationProviderViewData.GPODDER_NET));
+ gpodnetSetLoginPreference.setEnabled(loggedIn);
+ findPreference(PREFERENCE_SYNC).setEnabled(loggedIn);
+ findPreference(PREFERENCE_FORCE_FULL_SYNC).setEnabled(loggedIn);
+ findPreference(PREFERENCE_LOGOUT).setEnabled(loggedIn);
+ if (loggedIn) {
+ String summary = getString(R.string.synchronization_login_status,
+ SynchronizationCredentials.getUsername(), SynchronizationCredentials.getHosturl());
+ Spanned formattedSummary = HtmlCompat.fromHtml(summary, HtmlCompat.FROM_HTML_MODE_LEGACY);
+ findPreference(PREFERENCE_LOGOUT).setSummary(formattedSummary);
+ updateLastSyncReport(SynchronizationSettings.isLastSyncSuccessful(),
+ SynchronizationSettings.getLastSyncAttempt());
+ } else {
+ findPreference(PREFERENCE_LOGOUT).setSummary(null);
+ ((PreferenceActivity) getActivity()).getSupportActionBar().setSubtitle(null);
+ }
+ }
+
+ private void chooseProviderAndLogin() {
+ AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
+ builder.setTitle(R.string.dialog_choose_sync_service_title);
+
+ SynchronizationProviderViewData[] providers = SynchronizationProviderViewData.values();
+ ListAdapter adapter = new ArrayAdapter<SynchronizationProviderViewData>(
+ getContext(), R.layout.alertdialog_sync_provider_chooser, providers) {
+
+ ViewHolder holder;
+
+ class ViewHolder {
+ ImageView icon;
+ TextView title;
+ }
+
+ public View getView(int position, View convertView, ViewGroup parent) {
+ final LayoutInflater inflater = LayoutInflater.from(getContext());
+ if (convertView == null) {
+ convertView = inflater.inflate(
+ R.layout.alertdialog_sync_provider_chooser, null);
+
+ holder = new ViewHolder();
+ holder.icon = (ImageView) convertView.findViewById(R.id.icon);
+ holder.title = (TextView) convertView.findViewById(R.id.title);
+ convertView.setTag(holder);
+ } else {
+ holder = (ViewHolder) convertView.getTag();
+ }
+ SynchronizationProviderViewData synchronizationProviderViewData = getItem(position);
+ holder.title.setText(synchronizationProviderViewData.getSummaryResource());
+ holder.icon.setImageResource(synchronizationProviderViewData.getIconResource());
+ return convertView;
+ }
+ };
+
+ builder.setAdapter(adapter, (dialog, which) -> {
+ switch (providers[which]) {
+ case GPODDER_NET:
+ new GpodderAuthenticationFragment()
+ .show(getChildFragmentManager(), GpodderAuthenticationFragment.TAG);
+ break;
+ case NEXTCLOUD_GPODDER:
+ new NextcloudAuthenticationFragment()
+ .show(getChildFragmentManager(), NextcloudAuthenticationFragment.TAG);
+ break;
+ default:
+ break;
+ }
+ updateScreen();
+ });
+
+ AlertDialog dialog = builder.create();
+ dialog.show();
+ }
+
+ private boolean isProviderSelected(@NonNull SynchronizationProviderViewData provider) {
+ String selectedSyncProviderKey = getSelectedSyncProviderKey();
+ return provider.getIdentifier().equals(selectedSyncProviderKey);
+ }
+
+ private String getSelectedSyncProviderKey() {
+ return SynchronizationSettings.getSelectedSyncProviderKey();
+ }
+
+ private void updateLastSyncReport(boolean successful, long lastTime) {
+ String status = String.format("%1$s (%2$s)", getString(successful
+ ? R.string.gpodnetsync_pref_report_successful : R.string.gpodnetsync_pref_report_failed),
+ DateUtils.getRelativeDateTimeString(getContext(),
+ lastTime, DateUtils.MINUTE_IN_MILLIS, DateUtils.WEEK_IN_MILLIS, DateUtils.FORMAT_SHOW_TIME));
+ ((PreferenceActivity) getActivity()).getSupportActionBar().setSubtitle(status);
+ }
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/SwipeActions.java b/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/SwipeActions.java
index 50c7c1ae5..adf133856 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/SwipeActions.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/SwipeActions.java
@@ -201,12 +201,12 @@ public class SwipeActions extends ItemTouchHelper.SimpleCallback implements Life
@Override
public float getSwipeEscapeVelocity(float defaultValue) {
- return swipeOutEnabled ? defaultValue : Float.MAX_VALUE;
+ return swipeOutEnabled ? defaultValue * 1.5f : Float.MAX_VALUE;
}
@Override
public float getSwipeVelocityThreshold(float defaultValue) {
- return swipeOutEnabled ? defaultValue : 0;
+ return swipeOutEnabled ? defaultValue * 0.6f : 0;
}
@Override
diff --git a/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedItemMenuHandler.java b/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedItemMenuHandler.java
index c272af7d5..23fdb86de 100644
--- a/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedItemMenuHandler.java
+++ b/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedItemMenuHandler.java
@@ -13,12 +13,12 @@ import com.google.android.material.snackbar.Snackbar;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MainActivity;
-import de.danoeh.antennapod.core.preferences.GpodnetPreferences;
import de.danoeh.antennapod.core.preferences.PlaybackPreferences;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.service.playback.PlaybackService;
import de.danoeh.antennapod.core.storage.DBWriter;
-import de.danoeh.antennapod.core.sync.SyncService;
+import de.danoeh.antennapod.core.sync.SynchronizationSettings;
+import de.danoeh.antennapod.core.sync.queue.SynchronizationQueueSink;
import de.danoeh.antennapod.core.util.FeedItemUtil;
import de.danoeh.antennapod.core.util.IntentUtils;
import de.danoeh.antennapod.core.util.ShareUtils;
@@ -151,7 +151,7 @@ public class FeedItemMenuHandler {
} else if (menuItemId == R.id.mark_read_item) {
selectedItem.setPlayed(true);
DBWriter.markItemPlayed(selectedItem, FeedItem.PLAYED, true);
- if (GpodnetPreferences.loggedIn()) {
+ if (SynchronizationSettings.isProviderConnected()) {
FeedMedia media = selectedItem.getMedia();
// not all items have media, Gpodder only cares about those that do
if (media != null) {
@@ -161,17 +161,17 @@ public class FeedItemMenuHandler {
.position(media.getDuration() / 1000)
.total(media.getDuration() / 1000)
.build();
- SyncService.enqueueEpisodeAction(context, actionPlay);
+ SynchronizationQueueSink.enqueueEpisodeActionIfSynchronizationIsActive(context, actionPlay);
}
}
} else if (menuItemId == R.id.mark_unread_item) {
selectedItem.setPlayed(false);
DBWriter.markItemPlayed(selectedItem, FeedItem.UNPLAYED, false);
- if (GpodnetPreferences.loggedIn() && selectedItem.getMedia() != null) {
+ if (selectedItem.getMedia() != null) {
EpisodeAction actionNew = new EpisodeAction.Builder(selectedItem, EpisodeAction.NEW)
.currentTimestamp()
.build();
- SyncService.enqueueEpisodeAction(context, actionNew);
+ SynchronizationQueueSink.enqueueEpisodeActionIfSynchronizationIsActive(context, actionNew);
}
} else if (menuItemId == R.id.add_to_queue_item) {
DBWriter.addQueueItem(context, selectedItem);
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 84c738632..af35bbac9 100644
--- a/app/src/main/java/de/danoeh/antennapod/preferences/PreferenceUpgrader.java
+++ b/app/src/main/java/de/danoeh/antennapod/preferences/PreferenceUpgrader.java
@@ -108,9 +108,12 @@ public class PreferenceUpgrader {
}
}
if (oldVersion < 2040000) {
- SharedPreferences prefs = context.getSharedPreferences(SwipeActions.PREF_NAME, Context.MODE_PRIVATE);
- prefs.edit().putString(SwipeActions.KEY_PREFIX_SWIPEACTIONS + QueueFragment.TAG,
+ SharedPreferences swipePrefs = context.getSharedPreferences(SwipeActions.PREF_NAME, Context.MODE_PRIVATE);
+ swipePrefs.edit().putString(SwipeActions.KEY_PREFIX_SWIPEACTIONS + QueueFragment.TAG,
SwipeAction.REMOVE_FROM_QUEUE + "," + SwipeAction.REMOVE_FROM_QUEUE).apply();
}
+ if (oldVersion < 2050000) {
+ prefs.edit().putBoolean(UserPreferences.PREF_PAUSE_PLAYBACK_FOR_FOCUS_LOSS, true).apply();
+ }
}
}
diff --git a/app/src/main/java/de/danoeh/antennapod/receiver/ConnectivityActionReceiver.java b/app/src/main/java/de/danoeh/antennapod/receiver/ConnectivityActionReceiver.java
index 1075117dd..020f4374b 100644
--- a/app/src/main/java/de/danoeh/antennapod/receiver/ConnectivityActionReceiver.java
+++ b/app/src/main/java/de/danoeh/antennapod/receiver/ConnectivityActionReceiver.java
@@ -4,37 +4,23 @@ import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.net.ConnectivityManager;
-import android.net.NetworkInfo;
import android.text.TextUtils;
import android.util.Log;
+
import de.danoeh.antennapod.core.ClientConfig;
-import de.danoeh.antennapod.core.storage.DBTasks;
-import de.danoeh.antennapod.core.storage.DownloadRequester;
import de.danoeh.antennapod.core.util.NetworkUtils;
public class ConnectivityActionReceiver extends BroadcastReceiver {
- private static final String TAG = "ConnectivityActionRecvr";
+ private static final String TAG = "ConnectivityActionRecvr";
- @Override
- public void onReceive(final Context context, Intent intent) {
- if (TextUtils.equals(intent.getAction(), ConnectivityManager.CONNECTIVITY_ACTION)) {
- Log.d(TAG, "Received intent");
+ @Override
+ public void onReceive(final Context context, Intent intent) {
+ if (TextUtils.equals(intent.getAction(), ConnectivityManager.CONNECTIVITY_ACTION)) {
+ Log.d(TAG, "Received intent");
ClientConfig.initialize(context);
- if (NetworkUtils.autodownloadNetworkAvailable()) {
- Log.d(TAG, "auto-dl network available, starting auto-download");
- DBTasks.autodownloadUndownloadedItems(context);
- } else { // if new network is Wi-Fi, finish ongoing downloads,
- // otherwise cancel all downloads
- ConnectivityManager cm = (ConnectivityManager) context
- .getSystemService(Context.CONNECTIVITY_SERVICE);
- NetworkInfo ni = cm.getActiveNetworkInfo();
- if (ni == null || ni.getType() != ConnectivityManager.TYPE_WIFI) {
- Log.i(TAG, "Device is no longer connected to Wi-Fi. Cancelling ongoing downloads");
- DownloadRequester.getInstance().cancelAllDownloads(context);
- }
- }
- }
- }
+ NetworkUtils.networkChangedDetected();
+ }
+ }
}
diff --git a/app/src/main/java/de/danoeh/antennapod/view/PlaybackSpeedSeekBar.java b/app/src/main/java/de/danoeh/antennapod/view/PlaybackSpeedSeekBar.java
index c75164a74..33f0d47b8 100644
--- a/app/src/main/java/de/danoeh/antennapod/view/PlaybackSpeedSeekBar.java
+++ b/app/src/main/java/de/danoeh/antennapod/view/PlaybackSpeedSeekBar.java
@@ -9,11 +9,9 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.util.Consumer;
import de.danoeh.antennapod.R;
-import de.danoeh.antennapod.core.util.playback.PlaybackController;
public class PlaybackSpeedSeekBar extends FrameLayout {
private SeekBar seekBar;
- private PlaybackController controller;
private Consumer<Float> progressChangedListener;
public PlaybackSpeedSeekBar(@NonNull Context context) {
@@ -40,15 +38,9 @@ public class PlaybackSpeedSeekBar extends FrameLayout {
seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
- if (controller != null) {
- float playbackSpeed = (progress + 10) / 20.0f;
- controller.setPlaybackSpeed(playbackSpeed);
-
- if (progressChangedListener != null) {
- progressChangedListener.accept(playbackSpeed);
- }
- } else if (fromUser) {
- seekBar.post(() -> updateSpeed());
+ float playbackSpeed = (progress + 10) / 20.0f;
+ if (progressChangedListener != null) {
+ progressChangedListener.accept(playbackSpeed);
}
}
@@ -62,21 +54,23 @@ public class PlaybackSpeedSeekBar extends FrameLayout {
});
}
- public void updateSpeed() {
- if (controller != null) {
- seekBar.setProgress(Math.round((20 * controller.getCurrentPlaybackSpeedMultiplier()) - 10));
- }
- }
-
- public void setController(PlaybackController controller) {
- this.controller = controller;
- updateSpeed();
- if (progressChangedListener != null && controller != null) {
- progressChangedListener.accept(controller.getCurrentPlaybackSpeedMultiplier());
- }
+ public void updateSpeed(float speedMultiplier) {
+ seekBar.setProgress(Math.round((20 * speedMultiplier) - 10));
}
public void setProgressChangedListener(Consumer<Float> progressChangedListener) {
this.progressChangedListener = progressChangedListener;
}
+
+ public float getCurrentSpeed() {
+ return (seekBar.getProgress() + 10) / 20.0f;
+ }
+
+ @Override
+ public void setEnabled(boolean enabled) {
+ super.setEnabled(enabled);
+ seekBar.setEnabled(enabled);
+ findViewById(R.id.butDecSpeed).setEnabled(enabled);
+ findViewById(R.id.butIncSpeed).setEnabled(enabled);
+ }
}
diff --git a/app/src/main/java/de/danoeh/antennapod/view/ToolbarIconTintManager.java b/app/src/main/java/de/danoeh/antennapod/view/ToolbarIconTintManager.java
index 37d8db03e..621b6ea95 100644
--- a/app/src/main/java/de/danoeh/antennapod/view/ToolbarIconTintManager.java
+++ b/app/src/main/java/de/danoeh/antennapod/view/ToolbarIconTintManager.java
@@ -6,7 +6,6 @@ import android.graphics.PorterDuffColorFilter;
import android.graphics.drawable.Drawable;
import android.view.ContextThemeWrapper;
import androidx.appcompat.widget.Toolbar;
-import androidx.core.view.ViewCompat;
import com.google.android.material.appbar.AppBarLayout;
import com.google.android.material.appbar.CollapsingToolbarLayout;
import de.danoeh.antennapod.R;
@@ -25,7 +24,7 @@ public abstract class ToolbarIconTintManager implements AppBarLayout.OnOffsetCha
@Override
public void onOffsetChanged(AppBarLayout appBarLayout, int offset) {
- boolean tint = (collapsingToolbar.getHeight() + offset) > (2 * ViewCompat.getMinimumHeight(collapsingToolbar));
+ boolean tint = (collapsingToolbar.getHeight() + offset) > (2 * collapsingToolbar.getMinimumHeight());
if (isTinted != tint) {
isTinted = tint;
updateTint();
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 cd3af5003..8d1810ecb 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
@@ -21,7 +21,7 @@ import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.adapter.CoverLoader;
import de.danoeh.antennapod.adapter.actionbutton.ItemActionButton;
-import de.danoeh.antennapod.core.event.PlaybackPositionEvent;
+import de.danoeh.antennapod.event.playback.PlaybackPositionEvent;
import de.danoeh.antennapod.core.util.DateFormatter;
import de.danoeh.antennapod.model.feed.FeedItem;
import de.danoeh.antennapod.model.feed.FeedMedia;
diff --git a/app/src/main/res/layout/alertdialog_sync_provider_chooser.xml b/app/src/main/res/layout/alertdialog_sync_provider_chooser.xml
new file mode 100644
index 000000000..9b4d62804
--- /dev/null
+++ b/app/src/main/res/layout/alertdialog_sync_provider_chooser.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:padding="16dp">
+
+ <ImageView
+ android:id="@+id/icon"
+ android:layout_width="48dp"
+ android:layout_height="48dp"
+ android:layout_marginRight="16dip"
+ android:layout_marginEnd="16dip"
+ android:layout_gravity="center_vertical" />
+
+ <TextView
+ android:id="@+id/title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text=""
+ android:layout_gravity="center" />
+
+</LinearLayout>
diff --git a/app/src/main/res/layout/audioplayer_fragment.xml b/app/src/main/res/layout/audioplayer_fragment.xml
index f801930f5..7efbd23c8 100644
--- a/app/src/main/res/layout/audioplayer_fragment.xml
+++ b/app/src/main/res/layout/audioplayer_fragment.xml
@@ -12,6 +12,7 @@
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"/>
diff --git a/app/src/main/res/layout/edit_tags_dialog.xml b/app/src/main/res/layout/edit_tags_dialog.xml
index 57e3c412f..9ac0b60d3 100644
--- a/app/src/main/res/layout/edit_tags_dialog.xml
+++ b/app/src/main/res/layout/edit_tags_dialog.xml
@@ -7,6 +7,16 @@
android:orientation="vertical"
android:padding="16dp">
+ <com.joanzapata.iconify.widget.IconTextView
+ android:id="@+id/commonTagsInfo"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ android:visibility="gone"
+ android:textSize="@dimen/text_size_micro"
+ android:paddingBottom="16dp"
+ android:text="@string/multi_feed_common_tags_info" />
+
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/tagsRecycler"
android:layout_width="match_parent"
diff --git a/app/src/main/res/layout/episode_filter_dialog.xml b/app/src/main/res/layout/episode_filter_dialog.xml
index 9661a8e72..e8672c2f3 100644
--- a/app/src/main/res/layout/episode_filter_dialog.xml
+++ b/app/src/main/res/layout/episode_filter_dialog.xml
@@ -40,4 +40,21 @@
android:minLines="1"
android:scrollbars="vertical" />
+ <CheckBox
+ android:id="@+id/checkbox_filter_duration"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/episode_filters_duration" />
+
+ <EditText
+ android:id="@+id/etxtEpisodeFilterDurationText"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:cursorVisible="true"
+ android:focusable="true"
+ android:focusableInTouchMode="true"
+ android:inputType="numberSigned"
+ android:lines="1" />
+
</LinearLayout>
diff --git a/app/src/main/res/layout/feed_statistics.xml b/app/src/main/res/layout/feed_statistics.xml
new file mode 100644
index 000000000..f8f5ac555
--- /dev/null
+++ b/app/src/main/res/layout/feed_statistics.xml
@@ -0,0 +1,113 @@
+<?xml version="1.0" encoding="utf-8"?>
+<TableLayout
+ 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">
+
+ <TableRow
+ android:tag="detailed">
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/statistics_episodes_started_total" />
+
+ <TextView
+ android:id="@+id/startedTotalLabel"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="8dp"
+ android:layout_marginStart="8dp"
+ tools:text="0 / 0" />
+
+ </TableRow>
+
+ <TableRow>
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/statistics_time_played" />
+
+ <TextView
+ android:id="@+id/timePlayedLabel"
+ 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_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
+ android:id="@+id/totalDurationLabel"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="8dp"
+ android:layout_marginStart="8dp"
+ tools:text="0 min" />
+
+ </TableRow>
+
+ <TableRow>
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/statistics_episodes_on_device" />
+
+ <TextView
+ android:id="@+id/onDeviceLabel"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="8dp"
+ android:layout_marginStart="8dp"
+ tools:text="0" />
+
+ </TableRow>
+
+ <TableRow>
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/statistics_space_used" />
+
+ <TextView
+ android:id="@+id/spaceUsedLabel"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="8dp"
+ android:layout_marginStart="8dp"
+ tools:text="0 MB" />
+
+ </TableRow>
+
+</TableLayout>
diff --git a/app/src/main/res/layout/feed_statistics_dialog.xml b/app/src/main/res/layout/feed_statistics_dialog.xml
new file mode 100644
index 000000000..fcd36fe7a
--- /dev/null
+++ b/app/src/main/res/layout/feed_statistics_dialog.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.fragment.app.FragmentContainerView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/statisticsContainer"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:padding="16dp" />
diff --git a/app/src/main/res/layout/feedinfo.xml b/app/src/main/res/layout/feedinfo.xml
index d753cbda1..b0a73cb97 100644
--- a/app/src/main/res/layout/feedinfo.xml
+++ b/app/src/main/res/layout/feedinfo.xml
@@ -1,210 +1,164 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
- 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">
+ 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">
<com.google.android.material.appbar.AppBarLayout
- android:id="@+id/appBar"
- android:layout_width="match_parent"
- android:layout_height="wrap_content">
+ android:id="@+id/appBar"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
<com.google.android.material.appbar.CollapsingToolbarLayout
- android:id="@+id/collapsing_toolbar"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:background="?android:attr/windowBackground"
- app:contentScrim="?android:attr/windowBackground"
- app:layout_scrollFlags="scroll|exitUntilCollapsed"
- app:scrimAnimationDuration="200">
+ android:id="@+id/collapsing_toolbar"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="?android:attr/windowBackground"
+ app:contentScrim="?android:attr/windowBackground"
+ app:layout_scrollFlags="scroll|exitUntilCollapsed"
+ app:scrimAnimationDuration="200">
<ImageView
- android:id="@+id/imgvBackground"
- style="@style/BigBlurryBackground"
- android:layout_width="match_parent"
- android:layout_height="232dp"
- android:background="@color/image_readability_tint"
- app:layout_collapseMode="parallax"
- app:layout_collapseParallaxMultiplier="0.6" />
+ android:id="@+id/imgvBackground"
+ android:layout_width="match_parent"
+ android:layout_height="232dp"
+ android:background="@color/image_readability_tint"
+ style="@style/BigBlurryBackground"
+ app:layout_collapseMode="parallax"
+ app:layout_collapseParallaxMultiplier="0.6" />
<include
- layout="@layout/feeditemlist_header"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_gravity="bottom"
- app:layout_collapseMode="parallax"
- app:layout_collapseParallaxMultiplier="0.6" />
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="bottom"
+ layout="@layout/feeditemlist_header"
+ app:layout_collapseMode="parallax"
+ app:layout_collapseParallaxMultiplier="0.6" />
<androidx.appcompat.widget.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:layout_collapseMode="pin"
- app:navigationIcon="?homeAsUpIndicator" />
+ 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:layout_collapseMode="pin"
+ app:navigationContentDescription="@string/toolbar_back_button_content_description"
+ app:navigationIcon="?homeAsUpIndicator" />
</com.google.android.material.appbar.CollapsingToolbarLayout>
+
</com.google.android.material.appbar.AppBarLayout>
<androidx.core.widget.NestedScrollView
- android:id="@+id/scrollView"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:clipToPadding="false"
- android:paddingLeft="16dp"
- android:paddingRight="16dp"
- android:paddingBottom="8dp"
- android:scrollbarStyle="outsideOverlay"
- app:layout_behavior="@string/appbar_scrolling_view_behavior">
+ android:id="@+id/scrollView"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:clipToPadding="false"
+ android:paddingLeft="16dp"
+ android:paddingRight="16dp"
+ android:paddingBottom="8dp"
+ android:scrollbarStyle="outsideOverlay"
+ app:layout_behavior="@string/appbar_scrolling_view_behavior">
<LinearLayout
- android:id="@+id/infoContainer"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:orientation="vertical"
- android:paddingHorizontal="@dimen/additional_horizontal_spacing">
+ android:id="@+id/infoContainer"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:paddingHorizontal="@dimen/additional_horizontal_spacing">
<TextView
- android:id="@+id/lblUrl"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:textSize="18sp"
- android:layout_marginTop="16dp"
- android:layout_marginBottom="4dp"
- android:text="@string/url_label"
- android:textColor="?android:attr/textColorPrimary"
- tools:background="@android:color/holo_red_light" />
+ android:id="@+id/lblUrl"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textSize="18sp"
+ android:layout_marginTop="16dp"
+ android:layout_marginBottom="4dp"
+ android:text="@string/url_label"
+ android:textColor="?android:attr/textColorPrimary"
+ tools:background="@android:color/holo_red_light" />
<TextView
- android:id="@+id/txtvUrl"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:background="?attr/selectableItemBackground"
- android:maxLines="4"
- android:paddingTop="4dp"
- android:paddingBottom="4dp"
- tools:background="@android:color/holo_green_dark"
- tools:text="http://www.example.com/feed" />
+ android:id="@+id/txtvUrl"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="?attr/selectableItemBackground"
+ android:maxLines="4"
+ android:paddingTop="4dp"
+ android:paddingBottom="4dp"
+ tools:background="@android:color/holo_green_dark"
+ tools:text="http://www.example.com/feed" />
<TextView
- android:id="@+id/lblSupport"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_marginTop="16dp"
- android:layout_marginBottom="4dp"
- android:text="@string/support_funding_label"
- android:textColor="?android:attr/textColorPrimary"
- android:textSize="18sp"
- tools:background="@android:color/holo_red_light" />
+ android:id="@+id/lblSupport"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:layout_marginBottom="4dp"
+ android:text="@string/support_funding_label"
+ android:textColor="?android:attr/textColorPrimary"
+ android:textSize="18sp"
+ tools:background="@android:color/holo_red_light" />
<TextView
- android:id="@+id/txtvFundingUrl"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:maxLines="8"
- android:paddingTop="4dp"
- android:paddingBottom="4dp"
- android:linksClickable="true"
- android:autoLink="web"
- tools:background="@android:color/holo_green_dark" />
+ android:id="@+id/txtvFundingUrl"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:maxLines="8"
+ android:paddingTop="4dp"
+ android:paddingBottom="4dp"
+ android:linksClickable="true"
+ android:autoLink="web"
+ tools:background="@android:color/holo_green_dark" />
<TextView
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:textSize="18sp"
- android:layout_marginTop="16dp"
- android:layout_marginBottom="4dp"
- android:text="@string/description_label"
- android:textColor="?android:attr/textColorPrimary"
- tools:background="@android:color/holo_red_light" />
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textSize="18sp"
+ android:layout_marginTop="16dp"
+ android:layout_marginBottom="4dp"
+ android:text="@string/description_label"
+ android:textColor="?android:attr/textColorPrimary"
+ tools:background="@android:color/holo_red_light" />
<TextView
- android:id="@+id/txtvDescription"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:text="@string/design_time_lorem_ipsum"
- android:textIsSelectable="true"
- tools:background="@android:color/holo_green_dark" />
+ android:id="@+id/txtvDescription"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/design_time_lorem_ipsum"
+ android:textIsSelectable="true"
+ tools:background="@android:color/holo_green_dark" />
<TextView
- android:id="@+id/lblStatistics"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginTop="16dp"
- android:textSize="18sp"
- android:layout_marginBottom="8dp"
- android:text="@string/statistics_label"
- android:textColor="?android:attr/textColorPrimary"
- tools:background="@android:color/holo_red_light" />
-
- <TableLayout
- android:layout_width="match_parent"
- android:layout_height="match_parent">
-
- <TableRow
- android:layout_width="match_parent"
- android:layout_height="match_parent">
-
- <TextView
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:text="@string/statistics_listened_for" />
-
- <TextView
- android:id="@+id/txtvPodcastTime"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginLeft="5dp" />
- </TableRow>
-
- <TableRow
- android:layout_width="match_parent"
- android:layout_height="match_parent">
-
- <TextView
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:text="@string/statistics_episodes_on_device" />
-
- <TextView
- android:id="@+id/txtvPodcastEpisodeCount"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_marginLeft="5dp" />
- </TableRow>
-
- <TableRow
- android:layout_width="match_parent"
- android:layout_height="match_parent">
-
- <TextView
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:text="@string/statistics_space_used" />
-
- <TextView
- android:id="@+id/txtvPodcastSpaceUsed"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_marginLeft="5dp" />
- </TableRow>
-
- </TableLayout>
+ android:id="@+id/lblStatistics"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:textSize="18sp"
+ android:layout_marginBottom="8dp"
+ android:text="@string/statistics_label"
+ android:textColor="?android:attr/textColorPrimary"
+ tools:background="@android:color/holo_red_light" />
+
+ <androidx.fragment.app.FragmentContainerView
+ android:id="@+id/statisticsFragmentContainer"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" />
<Button
- android:id="@+id/btnvOpenStatistics"
- style="@style/Widget.MaterialComponents.Button.TextButton"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:minWidth="0dp"
- android:minHeight="0dp"
- android:text="@string/statistics_view_all" />
+ android:id="@+id/btnvOpenStatistics"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:minWidth="0dp"
+ android:minHeight="0dp"
+ android:text="@string/statistics_view_all"
+ style="@style/Widget.MaterialComponents.Button.TextButton" />
</LinearLayout>
+
</androidx.core.widget.NestedScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
diff --git a/app/src/main/res/layout/feeditem_pager_fragment.xml b/app/src/main/res/layout/feeditem_pager_fragment.xml
index ac7316dd8..690ac3bc1 100644
--- a/app/src/main/res/layout/feeditem_pager_fragment.xml
+++ b/app/src/main/res/layout/feeditem_pager_fragment.xml
@@ -11,6 +11,7 @@
android:layout_height="wrap_content"
android:minHeight="?attr/actionBarSize"
android:theme="?attr/actionBarTheme"
+ app:navigationContentDescription="@string/toolbar_back_button_content_description"
app:navigationIcon="?homeAsUpIndicator"
android:id="@+id/toolbar"/>
diff --git a/app/src/main/res/layout/feedsettings.xml b/app/src/main/res/layout/feedsettings.xml
index acd1089bd..df6e666eb 100644
--- a/app/src/main/res/layout/feedsettings.xml
+++ b/app/src/main/res/layout/feedsettings.xml
@@ -11,6 +11,7 @@
android:minHeight="?attr/actionBarSize"
android:theme="?attr/actionBarTheme"
app:title="@string/feed_settings_label"
+ app:navigationContentDescription="@string/toolbar_back_button_content_description"
app:navigationIcon="?homeAsUpIndicator"
android:elevation="4dp"
android:id="@+id/toolbar"/>
diff --git a/app/src/main/res/layout/fragment_subscriptions.xml b/app/src/main/res/layout/fragment_subscriptions.xml
index 6dd112eed..5672a310f 100644
--- a/app/src/main/res/layout/fragment_subscriptions.xml
+++ b/app/src/main/res/layout/fragment_subscriptions.xml
@@ -2,6 +2,7 @@
<RelativeLayout
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"
android:orientation="vertical">
@@ -40,9 +41,11 @@
android:id="@+id/subscriptions_grid"
android:layout_width="match_parent"
android:layout_height="match_parent"
+ android:clipToPadding="false"
android:layout_gravity="center_horizontal"
android:paddingBottom="88dp"
- android:clipToPadding="false" />
+ tools:itemCount="2"
+ tools:listitem="@layout/subscription_item" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
diff --git a/app/src/main/res/layout/nextcloud_auth_dialog.xml b/app/src/main/res/layout/nextcloud_auth_dialog.xml
new file mode 100644
index 000000000..345eec88b
--- /dev/null
+++ b/app/src/main/res/layout/nextcloud_auth_dialog.xml
@@ -0,0 +1,63 @@
+<?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:padding="16dp"
+ android:orientation="vertical"
+ android:clipToPadding="false">
+
+ <com.google.android.material.textfield.TextInputLayout
+ android:id="@+id/serverUrlTextInput"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="16dp"
+ style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
+
+ <com.google.android.material.textfield.TextInputEditText
+ android:id="@+id/serverUrlText"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:hint="@string/gpodnetauth_host"
+ android:inputType="textNoSuggestions"
+ android:lines="1"
+ android:imeOptions="actionNext|flagNoFullscreen" />
+
+ </com.google.android.material.textfield.TextInputLayout>
+
+ <LinearLayout
+ android:id="@+id/loginProgressContainer"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:visibility="gone"
+ android:orientation="horizontal"
+ android:layout_gravity="center_vertical">
+
+ <ProgressBar
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="8dp"
+ android:layout_marginRight="8dp" />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/synchronization_nextcloud_authenticate_browser" />
+
+ </LinearLayout>
+
+ <TextView
+ android:id="@+id/errorText"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:visibility="gone"
+ android:textColor="@color/download_failed_red"
+ android:layout_marginBottom="16dp" />
+
+ <Button
+ android:id="@+id/loginButton"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/gpodnetauth_login_butLabel" />
+
+</LinearLayout>
diff --git a/app/src/main/res/layout/playback_speed_feed_setting_dialog.xml b/app/src/main/res/layout/playback_speed_feed_setting_dialog.xml
new file mode 100644
index 000000000..572096911
--- /dev/null
+++ b/app/src/main/res/layout/playback_speed_feed_setting_dialog.xml
@@ -0,0 +1,37 @@
+<?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/useGlobalCheckbox"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/feed_auto_download_global"
+ android:layout_marginBottom="8dp" />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:gravity="center_vertical">
+
+ <de.danoeh.antennapod.view.PlaybackSpeedSeekBar
+ android:id="@+id/seekBar"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1" />
+
+ <TextView
+ android:id="@+id/currentSpeedLabel"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="8dp"
+ android:layout_marginLeft="8dp" />
+
+ </LinearLayout>
+
+</LinearLayout>
diff --git a/app/src/main/res/layout/playback_speed_seek_bar.xml b/app/src/main/res/layout/playback_speed_seek_bar.xml
index 8c9b1725f..155a2261a 100644
--- a/app/src/main/res/layout/playback_speed_seek_bar.xml
+++ b/app/src/main/res/layout/playback_speed_seek_bar.xml
@@ -14,6 +14,7 @@
android:text="-"
android:clickable="true"
android:focusable="true"
+ android:scrollbars="none"
android:textStyle="bold"
android:textSize="24sp"
android:textColor="?attr/colorSecondary"
@@ -36,6 +37,7 @@
android:text="+"
android:clickable="true"
android:focusable="true"
+ android:scrollbars="none"
android:textStyle="bold"
android:textSize="24sp"
android:textColor="?attr/colorSecondary"
diff --git a/app/src/main/res/layout/quick_feed_discovery_item.xml b/app/src/main/res/layout/quick_feed_discovery_item.xml
index cb03b6677..c3a32f019 100644
--- a/app/src/main/res/layout/quick_feed_discovery_item.xml
+++ b/app/src/main/res/layout/quick_feed_discovery_item.xml
@@ -2,6 +2,7 @@
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:squareImageView="http://schemas.android.com/apk/de.danoeh.antennapod"
+ xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="4dp"
@@ -14,7 +15,8 @@
android:elevation="4dp"
android:outlineProvider="bounds"
android:foreground="?android:attr/selectableItemBackground"
- squareImageView:direction="width" />
+ squareImageView:direction="width"
+ tools:src="@android:drawable/sym_def_app_icon"/>
</LinearLayout>
diff --git a/app/src/main/res/layout/subscription_selection_activity.xml b/app/src/main/res/layout/subscription_selection_activity.xml
new file mode 100644
index 000000000..b54e7e4a4
--- /dev/null
+++ b/app/src/main/res/layout/subscription_selection_activity.xml
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/transparentBackground"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <androidx.cardview.widget.CardView
+ android:id="@+id/card"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_margin="32dp"
+ android:elevation="16dp"
+ app:cardCornerRadius="4dp">
+
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <androidx.appcompat.widget.Toolbar
+ android:id="@+id/toolbar"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="?attr/actionBarSize"
+ android:theme="?attr/actionBarTheme"
+ android:layout_alignParentTop="true" />
+
+ <View
+ android:id="@+id/divider"
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:layout_below="@id/toolbar"
+ android:background="?android:attr/listDivider" />
+
+ <ListView
+ android:id="@+id/list"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:clipToPadding="false"
+ android:layout_below="@id/divider"
+ android:paddingBottom="88dp" />
+
+ <Button
+ android:id="@+id/shortcutBtn"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_margin="16dp"
+ android:layout_alignParentBottom="true"
+ android:text="@string/add_shortcut" />
+
+ </RelativeLayout>
+
+ </androidx.cardview.widget.CardView>
+
+</LinearLayout>
diff --git a/app/src/main/res/layout/time_dialog.xml b/app/src/main/res/layout/time_dialog.xml
index 6b6ab3195..138a60b33 100644
--- a/app/src/main/res/layout/time_dialog.xml
+++ b/app/src/main/res/layout/time_dialog.xml
@@ -1,136 +1,144 @@
<?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"
- xmlns:tools="http://schemas.android.com/tools"
- android:orientation="vertical"
- android:gravity="center"
- android:padding="16dp">
+<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:gravity="center"
+ android:padding="16dp">
<LinearLayout
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:id="@+id/timeSetup"
- android:orientation="vertical">
+ android:id="@+id/timeSetup"
+ 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">
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
<EditText
- android:id="@+id/etxtTime"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_weight="1"
- android:layout_margin="8dp"
- android:ems="2"
- android:inputType="number"
- android:maxLength="3"/>
+ android:id="@+id/etxtTime"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_margin="8dp"
+ android:ems="2"
+ android:inputType="number"
+ android:maxLength="3" />
<Spinner
- android:id="@+id/spTimeUnit"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginBottom="8dp"
- android:layout_marginTop="8dp"/>
+ android:id="@+id/spTimeUnit"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="8dp"
+ android:layout_marginTop="8dp" />
+
</LinearLayout>
<Button
- android:text="@string/set_sleeptimer_label"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:id="@+id/setSleeptimerButton"/>
+ android:id="@+id/setSleeptimerButton"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/set_sleeptimer_label" />
+
</LinearLayout>
<LinearLayout
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:id="@+id/timeDisplay"
- android:orientation="vertical"
- android:visibility="gone">
+ android:id="@+id/timeDisplay"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:visibility="visible">
<TextView
- android:text="00:00:00"
- android:layout_gravity="center"
- android:gravity="center"
- android:textSize="32sp"
- android:textColor="?android:attr/textColorPrimary"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:id="@+id/time"/>
+ android:id="@+id/time"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:text="00:00:00"
+ android:layout_gravity="center"
+ android:gravity="center"
+ android:textSize="32sp"
+ android:textColor="?android:attr/textColorPrimary" />
<Button
- android:text="@string/disable_sleeptimer_label"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:id="@+id/disableSleeptimerButton"/>
+ android:id="@+id/disableSleeptimerButton"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/disable_sleeptimer_label" />
<LinearLayout
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:orientation="horizontal">
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:gravity="center_vertical">
<Button
- android:id="@+id/extendSleepFiveMinutesButton"
- style="?attr/materialButtonOutlinedStyle"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_marginEnd="4dp"
- android:layout_marginRight="4dp"
- android:layout_weight="1"
- android:padding="5dp"
- tools:text="+5 min" />
+ android:id="@+id/extendSleepFiveMinutesButton"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="4dp"
+ android:layout_marginRight="4dp"
+ android:paddingHorizontal="2dp"
+ android:paddingVertical="4dp"
+ android:layout_weight="1"
+ style="?attr/materialButtonOutlinedStyle"
+ tools:text="+5 min" />
<Button
- android:id="@+id/extendSleepTenMinutesButton"
- style="?attr/materialButtonOutlinedStyle"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_marginStart="4dp"
- android:layout_marginEnd="4dp"
- android:layout_weight="1"
- tools:text="+10 min" />
+ android:id="@+id/extendSleepTenMinutesButton"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="4dp"
+ android:layout_marginEnd="4dp"
+ android:paddingHorizontal="2dp"
+ android:paddingVertical="4dp"
+ android:layout_weight="1"
+ style="?attr/materialButtonOutlinedStyle"
+ tools:text="+10 min" />
<Button
- android:id="@+id/extendSleepTwentyMinutesButton"
- style="?attr/materialButtonOutlinedStyle"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_marginStart="4dp"
- android:layout_marginRight="4dp"
- android:layout_marginLeft="4dp"
- android:layout_weight="1"
- tools:text="+20 min" />
+ android:id="@+id/extendSleepTwentyMinutesButton"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="4dp"
+ android:layout_marginRight="4dp"
+ android:layout_marginLeft="4dp"
+ android:paddingHorizontal="2dp"
+ android:paddingVertical="4dp"
+ android:layout_weight="1"
+ style="?attr/materialButtonOutlinedStyle"
+ tools:text="+20 min" />
</LinearLayout>
</LinearLayout>
-
<LinearLayout
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:orientation="vertical"
- android:layout_marginTop="8dp">
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:layout_marginTop="8dp">
<CheckBox
- android:id="@+id/cbShakeToReset"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:text="@string/shake_to_reset_label"/>
+ android:id="@+id/cbShakeToReset"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/shake_to_reset_label" />
<CheckBox
- android:id="@+id/cbVibrate"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:text="@string/timer_vibration_label"/>
+ android:id="@+id/cbVibrate"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/timer_vibration_label" />
<CheckBox
- android:id="@+id/chAutoEnable"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:text="@string/auto_enable_label"/>
+ android:id="@+id/chAutoEnable"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/auto_enable_label" />
</LinearLayout>
diff --git a/app/src/main/res/menu/cast_enabled.xml b/app/src/main/res/menu/cast_enabled.xml
deleted file mode 100644
index d6e85c311..000000000
--- a/app/src/main/res/menu/cast_enabled.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<menu xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:custom="http://schemas.android.com/apk/res-auto">
-
- <item
- android:id="@+id/media_route_menu_item"
- android:title="@string/cast_media_route_menu_title"
- custom:actionProviderClass="de.danoeh.antennapod.core.cast.SwitchableMediaRouteActionProvider"
- custom:showAsAction="ifRoom"/>
-</menu> \ No newline at end of file
diff --git a/app/src/main/res/menu/nav_feed_action_speeddial.xml b/app/src/main/res/menu/nav_feed_action_speeddial.xml
index 2dfa002bb..d08aa645f 100644
--- a/app/src/main/res/menu/nav_feed_action_speeddial.xml
+++ b/app/src/main/res/menu/nav_feed_action_speeddial.xml
@@ -25,4 +25,9 @@
android:menuCategory="container"
android:title="@string/playback_speed"
android:icon="@drawable/ic_playback_speed"/>
+ <item
+ android:id="@+id/edit_tags"
+ android:menuCategory="container"
+ android:title="@string/edit_tags"
+ android:icon="@drawable/ic_tag"/>
</menu>
diff --git a/app/src/main/res/menu/nav_feed_context.xml b/app/src/main/res/menu/nav_feed_context.xml
index 17c15cbb0..3f5127f36 100644
--- a/app/src/main/res/menu/nav_feed_context.xml
+++ b/app/src/main/res/menu/nav_feed_context.xml
@@ -7,9 +7,9 @@
android:title="@string/remove_all_new_flags_label" />
<item
- android:id="@+id/add_to_folder"
+ android:id="@+id/edit_tags"
android:menuCategory="container"
- android:title="@string/add_to_folder" />
+ android:title="@string/edit_tags" />
<item
android:id="@+id/rename_item"
diff --git a/app/src/main/res/menu/nav_folder_context.xml b/app/src/main/res/menu/nav_folder_context.xml
new file mode 100644
index 000000000..eb6515bed
--- /dev/null
+++ b/app/src/main/res/menu/nav_folder_context.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+ <item
+ android:id="@+id/rename_folder_item"
+ android:menuCategory="container"
+ android:title="@string/rename_tag_label" />
+</menu> \ No newline at end of file
diff --git a/app/src/main/res/xml/feed_settings.xml b/app/src/main/res/xml/feed_settings.xml
index 457ff6e5b..007f084c9 100644
--- a/app/src/main/res/xml/feed_settings.xml
+++ b/app/src/main/res/xml/feed_settings.xml
@@ -1,72 +1,71 @@
<?xml version="1.0" encoding="utf-8"?>
-<PreferenceScreen
- xmlns:android="http://schemas.android.com/apk/res/android"
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
android:key="feedSettingsScreen">
<SwitchPreferenceCompat
- android:key="keepUpdated"
- android:icon="@drawable/ic_refresh"
- android:title="@string/keep_updated"
- android:summary="@string/keep_updated_summary"/>
+ android:icon="@drawable/ic_refresh"
+ android:key="keepUpdated"
+ android:summary="@string/keep_updated_summary"
+ android:title="@string/keep_updated" />
<SwitchPreferenceCompat
- android:key="episodeNotification"
- android:defaultValue="false"
- android:dependency="keepUpdated"
- android:icon="@drawable/ic_notifications"
- android:title="@string/episode_notification"
- android:summary="@string/episode_notification_summary"/>
+ android:defaultValue="false"
+ android:dependency="keepUpdated"
+ android:icon="@drawable/ic_notifications"
+ android:key="episodeNotification"
+ android:summary="@string/episode_notification_summary"
+ android:title="@string/episode_notification" />
<Preference
- android:key="authentication"
- android:icon="@drawable/ic_key"
- android:title="@string/authentication_label"
- android:summary="@string/authentication_descr"/>
+ android:icon="@drawable/ic_key"
+ android:key="authentication"
+ android:summary="@string/authentication_descr"
+ android:title="@string/authentication_label" />
<Preference
- android:key="tags"
- android:icon="@drawable/ic_folder"
- android:title="@string/feed_folders_label"
- android:summary="@string/feed_folders_summary"/>
+ android:icon="@drawable/ic_tag"
+ android:key="tags"
+ android:summary="@string/feed_tags_summary"
+ android:title="@string/feed_tags_label" />
- <ListPreference
- android:key="feedPlaybackSpeed"
- android:icon="@drawable/ic_playback_speed"
- android:title="@string/playback_speed"
- android:summary="@string/pref_feed_playback_speed_sum"/>
+ <Preference
+ android:icon="@drawable/ic_playback_speed"
+ android:key="feedPlaybackSpeed"
+ android:summary="@string/pref_feed_playback_speed_sum"
+ android:title="@string/playback_speed" />
<Preference
- android:key="feedAutoSkip"
- android:icon="@drawable/ic_skip_24dp"
- android:summary="@string/pref_feed_skip_sum"
- android:title="@string/pref_feed_skip" />
+ android:icon="@drawable/ic_skip_24dp"
+ android:key="feedAutoSkip"
+ android:summary="@string/pref_feed_skip_sum"
+ android:title="@string/pref_feed_skip" />
<ListPreference
- android:entries="@array/spnAutoDeleteItems"
- android:entryValues="@array/spnAutoDeleteValues"
- android:icon="@drawable/ic_delete"
- android:title="@string/auto_delete_label"
- android:summary="@string/feed_auto_download_global"
- android:key="autoDelete"/>
+ android:entries="@array/spnAutoDeleteItems"
+ android:entryValues="@array/spnAutoDeleteValues"
+ android:icon="@drawable/ic_delete"
+ android:key="autoDelete"
+ android:summary="@string/feed_auto_download_global"
+ android:title="@string/auto_delete_label" />
<ListPreference
- android:entries="@array/spnVolumeReductionItems"
- android:entryValues="@array/spnVolumeReductionValues"
- android:icon="@drawable/ic_volume_adaption"
- android:summary="@string/feed_volume_reduction_summary"
- android:title="@string/feed_volume_reduction"
- android:defaultValue="off"
- android:key="volumeReduction"/>
+ android:defaultValue="off"
+ android:entries="@array/spnVolumeReductionItems"
+ android:entryValues="@array/spnVolumeReductionValues"
+ android:icon="@drawable/ic_volume_adaption"
+ android:key="volumeReduction"
+ android:summary="@string/feed_volume_reduction_summary"
+ android:title="@string/feed_volume_reduction" />
<PreferenceCategory
- android:title="@string/auto_download_settings_label"
- android:key="autoDownloadCategory">
+ android:key="autoDownloadCategory"
+ android:title="@string/auto_download_settings_label">
<SwitchPreferenceCompat
- android:key="autoDownload"
- android:title="@string/auto_download_label"/>
+ android:key="autoDownload"
+ android:title="@string/auto_download_label" />
<Preference
- android:key="episodeFilter"
- android:title="@string/episode_filters_label"
- android:summary="@string/episode_filters_description"/>
+ android:key="episodeFilter"
+ android:summary="@string/episode_filters_description"
+ android:title="@string/episode_filters_label" />
</PreferenceCategory>
</PreferenceScreen>
diff --git a/app/src/main/res/xml/network_security_config.xml b/app/src/main/res/xml/network_security_config.xml
new file mode 100644
index 000000000..d4c3fc996
--- /dev/null
+++ b/app/src/main/res/xml/network_security_config.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<network-security-config xmlns:tools="http://schemas.android.com/tools">
+ <base-config cleartextTrafficPermitted="true" tools:ignore="InsecureBaseConfiguration">
+ <trust-anchors>
+ <certificates src="user" tools:ignore="AcceptsUserCertificates"/>
+ <certificates src="system" />
+ </trust-anchors>
+ </base-config>
+</network-security-config>
diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml
index d528945c7..7c5012899 100644
--- a/app/src/main/res/xml/preferences.xml
+++ b/app/src/main/res/xml/preferences.xml
@@ -28,7 +28,7 @@
android:icon="@drawable/ic_network" />
<Preference
- android:key="prefScreenGpodder"
+ android:key="prefScreenSynchronization"
android:title="@string/synchronization_pref"
android:summary="@string/synchronization_sum"
android:icon="@drawable/ic_cloud" />
diff --git a/app/src/main/res/xml/preferences_gpodder.xml b/app/src/main/res/xml/preferences_gpodder.xml
deleted file mode 100644
index a210b8e11..000000000
--- a/app/src/main/res/xml/preferences_gpodder.xml
+++ /dev/null
@@ -1,28 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<PreferenceScreen
- xmlns:android="http://schemas.android.com/apk/res/android">
- <Preference
- android:key="pref_gpodnet_description"
- android:icon="@drawable/gpodder_icon"
- android:summary="@string/gpodnet_description"/>
- <Preference
- android:key="pref_gpodnet_authenticate"
- android:title="@string/pref_gpodnet_authenticate_title"
- android:summary="@string/pref_gpodnet_authenticate_sum"/>
- <Preference
- android:key="pref_gpodnet_setlogin_information"
- android:title="@string/pref_gpodnet_setlogin_information_title"
- android:summary="@string/pref_gpodnet_setlogin_information_sum"/>
- <Preference
- android:key="pref_gpodnet_sync"
- android:title="@string/pref_gpodnet_sync_changes_title"
- android:summary="@string/pref_gpodnet_sync_changes_sum"/>
- <Preference
- android:key="pref_gpodnet_force_full_sync"
- android:title="@string/pref_gpodnet_full_sync_title"
- android:summary="@string/pref_gpodnet_full_sync_sum"/>
- <Preference
- android:key="pref_gpodnet_logout"
- android:title="@string/pref_gpodnet_logout_title"/>
-
-</PreferenceScreen>
diff --git a/app/src/main/res/xml/preferences_playback.xml b/app/src/main/res/xml/preferences_playback.xml
index 2be8492eb..add9e8d4c 100644
--- a/app/src/main/res/xml/preferences_playback.xml
+++ b/app/src/main/res/xml/preferences_playback.xml
@@ -23,7 +23,7 @@
android:summary="@string/pref_unpauseOnBluetoothReconnect_sum"
android:title="@string/pref_unpauseOnBluetoothReconnect_title"/>
<SwitchPreferenceCompat
- android:defaultValue="false"
+ android:defaultValue="true"
android:enabled="true"
android:key="prefPauseForFocusLoss"
android:summary="@string/pref_pausePlaybackForFocusLoss_sum"
@@ -127,11 +127,5 @@
android:title="@string/media_player"
android:summary="@string/pref_media_player_message"
android:entryValues="@array/media_player_values"/>
- <SwitchPreferenceCompat
- android:defaultValue="false"
- android:enabled="true"
- android:key="prefCast"
- android:summary="@string/pref_cast_message"
- android:title="@string/pref_cast_title"/>
</PreferenceCategory>
</PreferenceScreen>
diff --git a/app/src/main/res/xml/preferences_synchronization.xml b/app/src/main/res/xml/preferences_synchronization.xml
new file mode 100644
index 000000000..fbd4ccc79
--- /dev/null
+++ b/app/src/main/res/xml/preferences_synchronization.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<PreferenceScreen
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+
+ <Preference
+ android:key="preference_synchronization_description"
+ android:icon="@drawable/ic_notification_sync"
+ android:summary="@string/synchronization_summary_unchoosen"/>
+
+ <Preference
+ android:key="pref_gpodnet_setlogin_information"
+ android:title="@string/pref_gpodnet_setlogin_information_title"
+ android:summary="@string/pref_gpodnet_setlogin_information_sum"
+ app:isPreferenceVisible="false"/>
+
+ <Preference
+ android:key="pref_synchronization_sync"
+ android:title="@string/synchronization_sync_changes_title"
+ android:summary="@string/synchronization_sync_summary"/>
+
+ <Preference
+ android:key="pref_synchronization_force_full_sync"
+ android:title="@string/synchronization_full_sync_title"
+ android:summary="@string/synchronization_force_sync_summary"/>
+
+ <Preference
+ android:key="pref_synchronization_logout"
+ android:title="@string/synchronization_logout"/>
+
+</PreferenceScreen>
diff --git a/app/src/main/res/xml/preferences_user_interface.xml b/app/src/main/res/xml/preferences_user_interface.xml
index 0b2707a18..59e7092a1 100644
--- a/app/src/main/res/xml/preferences_user_interface.xml
+++ b/app/src/main/res/xml/preferences_user_interface.xml
@@ -44,6 +44,11 @@
android:title="@string/pref_filter_feed_title"
android:key="prefSubscriptionsFilter"
android:summary="@string/pref_filter_feed_sum" />
+ <SwitchPreferenceCompat
+ android:title="@string/pref_show_subscription_title"
+ android:key="prefSubscriptionTitle"
+ android:summary="@string/pref_show_subscription_title_summary"
+ android:defaultValue="false" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/external_elements">
<SwitchPreferenceCompat
diff --git a/app/src/main/res/xml/provider_paths.xml b/app/src/main/res/xml/provider_paths.xml
index a16b679e3..045996714 100644
--- a/app/src/main/res/xml/provider_paths.xml
+++ b/app/src/main/res/xml/provider_paths.xml
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-path name="external_storage" path="."/>
- <root-path name="external_files" path="/storage/" />
+ <files-path name="name" path="." />
</paths>
diff --git a/app/src/play/java/de/danoeh/antennapod/activity/CastEnabledActivity.java b/app/src/play/java/de/danoeh/antennapod/activity/CastEnabledActivity.java
deleted file mode 100644
index 753feb3e7..000000000
--- a/app/src/play/java/de/danoeh/antennapod/activity/CastEnabledActivity.java
+++ /dev/null
@@ -1,157 +0,0 @@
-package de.danoeh.antennapod.activity;
-
-import android.content.SharedPreferences;
-import android.media.AudioManager;
-import android.os.Bundle;
-import androidx.preference.PreferenceManager;
-import androidx.appcompat.app.AppCompatActivity;
-import android.util.Log;
-import android.view.Menu;
-import android.view.MenuItem;
-
-import com.google.android.gms.cast.ApplicationMetadata;
-
-import de.danoeh.antennapod.R;
-import de.danoeh.antennapod.core.cast.CastButtonVisibilityManager;
-import de.danoeh.antennapod.core.cast.CastConsumer;
-import de.danoeh.antennapod.core.cast.CastManager;
-import de.danoeh.antennapod.core.cast.DefaultCastConsumer;
-import de.danoeh.antennapod.core.cast.SwitchableMediaRouteActionProvider;
-import de.danoeh.antennapod.core.preferences.UserPreferences;
-import de.danoeh.antennapod.core.service.playback.PlaybackService;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Activity that allows for showing the MediaRouter button whenever there's a cast device in the
- * network.
- */
-public abstract class CastEnabledActivity extends AppCompatActivity
- implements SharedPreferences.OnSharedPreferenceChangeListener {
- public static final String TAG = "CastEnabledActivity";
-
- private CastConsumer castConsumer;
- private CastManager castManager;
- private final List<CastButtonVisibilityManager> castButtons = new ArrayList<>();
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- if (!CastManager.isInitialized()) {
- return;
- }
-
- PreferenceManager.getDefaultSharedPreferences(getApplicationContext())
- .registerOnSharedPreferenceChangeListener(this);
-
- castConsumer = new DefaultCastConsumer() {
- @Override
- public void onApplicationConnected(ApplicationMetadata appMetadata, String sessionId, boolean wasLaunched) {
- onCastConnectionChanged(true);
- }
-
- @Override
- public void onDisconnected() {
- onCastConnectionChanged(false);
- }
- };
- castManager = CastManager.getInstance();
- castManager.addCastConsumer(castConsumer);
- CastButtonVisibilityManager castButtonVisibilityManager = new CastButtonVisibilityManager(castManager);
- castButtonVisibilityManager.setPrefEnabled(UserPreferences.isCastEnabled());
- onCastConnectionChanged(castManager.isConnected());
- castButtons.add(castButtonVisibilityManager);
- }
-
- @Override
- protected void onDestroy() {
- if (!CastManager.isInitialized()) {
- super.onDestroy();
- return;
- }
- PreferenceManager.getDefaultSharedPreferences(getApplicationContext())
- .unregisterOnSharedPreferenceChangeListener(this);
- castManager.removeCastConsumer(castConsumer);
- super.onDestroy();
- }
-
- @Override
- protected void onResume() {
- super.onResume();
- if (!CastManager.isInitialized()) {
- return;
- }
- for (CastButtonVisibilityManager castButton : castButtons) {
- castButton.setResumed(true);
- }
- }
-
- @Override
- protected void onPause() {
- super.onPause();
- if (!CastManager.isInitialized()) {
- return;
- }
- for (CastButtonVisibilityManager castButton : castButtons) {
- castButton.setResumed(false);
- }
- }
-
-
- @Override
- public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
- if (UserPreferences.PREF_CAST_ENABLED.equals(key)) {
- boolean newValue = UserPreferences.isCastEnabled();
- Log.d(TAG, "onSharedPreferenceChanged(), isCastEnabled set to " + newValue);
- for (CastButtonVisibilityManager castButton : castButtons) {
- castButton.setPrefEnabled(newValue);
- }
- // PlaybackService has its own listener, so if it's active we don't have to take action here.
- if (!newValue && !PlaybackService.isRunning) {
- CastManager.getInstance().disconnect();
- }
- }
- }
-
- private void onCastConnectionChanged(boolean connected) {
- if (connected) {
- for (CastButtonVisibilityManager castButton : castButtons) {
- castButton.onConnected();
- }
- setVolumeControlStream(AudioManager.USE_DEFAULT_STREAM_TYPE);
- } else {
- for (CastButtonVisibilityManager castButton : castButtons) {
- castButton.onDisconnected();
- }
- setVolumeControlStream(AudioManager.STREAM_MUSIC);
- }
- }
-
- /**
- * Should be called by any activity or fragment for which the cast button should be shown.
- */
- public final void requestCastButton(Menu menu) {
- if (!CastManager.isInitialized()) {
- return;
- }
-
- MenuItem mediaRouteButton = menu.findItem(R.id.media_route_menu_item);
- if (mediaRouteButton == null) {
- getMenuInflater().inflate(R.menu.cast_enabled, menu);
- mediaRouteButton = menu.findItem(R.id.media_route_menu_item);
- }
-
- SwitchableMediaRouteActionProvider mediaRouteActionProvider =
- CastManager.getInstance().addMediaRouterButton(mediaRouteButton);
- CastButtonVisibilityManager castButtonVisibilityManager =
- new CastButtonVisibilityManager(CastManager.getInstance());
- castButtonVisibilityManager.setMenu(menu);
- castButtonVisibilityManager.setPrefEnabled(UserPreferences.isCastEnabled());
- castButtonVisibilityManager.mediaRouteActionProvider = mediaRouteActionProvider;
- castButtonVisibilityManager.setResumed(true);
- castButtonVisibilityManager.requestCastButton(MenuItem.SHOW_AS_ACTION_ALWAYS);
- mediaRouteActionProvider.setEnabled(castButtonVisibilityManager.shouldEnable());
- }
-}
diff --git a/app/src/play/java/de/danoeh/antennapod/config/CastCallbackImpl.java b/app/src/play/java/de/danoeh/antennapod/config/CastCallbackImpl.java
deleted file mode 100644
index 2a879c62d..000000000
--- a/app/src/play/java/de/danoeh/antennapod/config/CastCallbackImpl.java
+++ /dev/null
@@ -1,21 +0,0 @@
-package de.danoeh.antennapod.config;
-
-import androidx.annotation.NonNull;
-import androidx.mediarouter.app.MediaRouteControllerDialogFragment;
-import androidx.mediarouter.app.MediaRouteDialogFactory;
-
-import de.danoeh.antennapod.core.CastCallbacks;
-import de.danoeh.antennapod.fragment.CustomMRControllerDialogFragment;
-
-public class CastCallbackImpl implements CastCallbacks {
- @Override
- public MediaRouteDialogFactory getMediaRouterDialogFactory() {
- return new MediaRouteDialogFactory() {
- @NonNull
- @Override
- public MediaRouteControllerDialogFragment onCreateControllerDialogFragment() {
- return new CustomMRControllerDialogFragment();
- }
- };
- }
-}
diff --git a/app/src/play/java/de/danoeh/antennapod/dialog/CustomMRControllerDialog.java b/app/src/play/java/de/danoeh/antennapod/dialog/CustomMRControllerDialog.java
deleted file mode 100644
index 6d8450a18..000000000
--- a/app/src/play/java/de/danoeh/antennapod/dialog/CustomMRControllerDialog.java
+++ /dev/null
@@ -1,480 +0,0 @@
-package de.danoeh.antennapod.dialog;
-
-import android.app.PendingIntent;
-import android.content.Context;
-import android.content.res.Configuration;
-import android.graphics.Bitmap;
-import android.graphics.drawable.Drawable;
-import android.net.Uri;
-import android.os.Bundle;
-import android.os.RemoteException;
-import androidx.annotation.NonNull;
-import android.support.v4.media.MediaDescriptionCompat;
-import android.support.v4.media.MediaMetadataCompat;
-import android.support.v4.media.session.MediaControllerCompat;
-import android.support.v4.media.session.MediaSessionCompat;
-import android.support.v4.media.session.PlaybackStateCompat;
-import androidx.core.util.Pair;
-import androidx.core.view.MarginLayoutParamsCompat;
-import androidx.core.view.accessibility.AccessibilityEventCompat;
-import androidx.mediarouter.app.MediaRouteControllerDialog;
-import androidx.palette.graphics.Palette;
-import androidx.mediarouter.media.MediaRouter;
-import androidx.appcompat.widget.AppCompatImageView;
-import android.text.TextUtils;
-import android.util.Log;
-import android.util.TypedValue;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.accessibility.AccessibilityEvent;
-import android.view.accessibility.AccessibilityManager;
-import android.widget.FrameLayout;
-import android.widget.ImageButton;
-import android.widget.ImageView;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-import com.bumptech.glide.Glide;
-import com.bumptech.glide.request.RequestOptions;
-import com.bumptech.glide.request.target.Target;
-
-import java.util.concurrent.ExecutionException;
-
-import de.danoeh.antennapod.R;
-import de.danoeh.antennapod.core.glide.ApGlideSettings;
-import io.reactivex.Observable;
-import io.reactivex.android.schedulers.AndroidSchedulers;
-import io.reactivex.disposables.Disposable;
-import io.reactivex.schedulers.Schedulers;
-
-public class CustomMRControllerDialog extends MediaRouteControllerDialog {
- public static final String TAG = "CustomMRContrDialog";
-
- private MediaRouter mediaRouter;
- private MediaSessionCompat.Token token;
-
- private ImageView artView;
- private TextView titleView;
- private TextView subtitleView;
- private ImageButton playPauseButton;
- private LinearLayout rootView;
-
- private boolean viewsCreated = false;
-
- private Disposable fetchArtSubscription;
-
- private MediaControllerCompat mediaController;
- private MediaControllerCompat.Callback mediaControllerCallback;
-
- public CustomMRControllerDialog(Context context) {
- this(context, 0);
- }
-
- private CustomMRControllerDialog(Context context, int theme) {
- super(context, theme);
- mediaRouter = MediaRouter.getInstance(getContext());
- token = mediaRouter.getMediaSessionToken();
- try {
- if (token != null) {
- mediaController = new MediaControllerCompat(getContext(), token);
- }
- } catch (RemoteException e) {
- Log.e(TAG, "Error creating media controller", e);
- }
-
- if (mediaController != null) {
- mediaControllerCallback = new MediaControllerCompat.Callback() {
- @Override
- public void onSessionDestroyed() {
- if (mediaController != null) {
- mediaController.unregisterCallback(mediaControllerCallback);
- mediaController = null;
- }
- }
-
- @Override
- public void onMetadataChanged(MediaMetadataCompat metadata) {
- updateViews();
- }
-
- @Override
- public void onPlaybackStateChanged(PlaybackStateCompat state) {
- updateState();
- }
- };
- mediaController.registerCallback(mediaControllerCallback);
- }
- }
-
- @Override
- public View onCreateMediaControlView(Bundle savedInstanceState) {
- boolean landscape = getContext().getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE;
- if (landscape) {
- /*
- * When a horizontal LinearLayout measures itself, it first measures its children and
- * settles their widths on the first pass, and only then figures out its height, never
- * revisiting the widths measurements.
- * When one has a child view that imposes a certain aspect ratio (such as an ImageView),
- * then its width and height are related to each other, and so if one allows for a large
- * height, then it will request for itself a large width as well. However, on the first
- * child measurement, the LinearLayout imposes a very relaxed height bound, that the
- * child uses to tell the width it wants, a value which the LinearLayout will interpret
- * as final, even though the child will want to change it once a more restrictive height
- * bound is imposed later.
- *
- * Our solution is, given that the heights of the children do not depend on their widths
- * in this case, we first figure out the layout's height and only then perform the
- * usual sequence of measurements.
- *
- * Note: this solution does not take into account any vertical paddings nor children's
- * vertical margins in determining the height, as this View as well as its children are
- * defined in code and no paddings/margins that would influence these computations are
- * introduced.
- *
- * There were no resources online for this type of issue as far as I could gather.
- */
- rootView = new LinearLayout(getContext()) {
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- // We'd like to find the overall height before adjusting the widths within the LinearLayout
- int maxHeight = Integer.MIN_VALUE;
- if (MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY) {
- for (int i = 0; i < getChildCount(); i++) {
- int height = Integer.MIN_VALUE;
- View child = getChildAt(i);
- ViewGroup.LayoutParams lp = child.getLayoutParams();
- // we only measure children whose layout_height is not MATCH_PARENT
- if (lp.height >= 0) {
- height = lp.height;
- } else if (lp.height == ViewGroup.LayoutParams.WRAP_CONTENT) {
- child.measure(widthMeasureSpec, heightMeasureSpec);
- height = child.getMeasuredHeight();
- }
- maxHeight = Math.max(maxHeight, height);
- }
- }
- if (maxHeight > 0) {
- super.onMeasure(widthMeasureSpec,
- MeasureSpec.makeMeasureSpec(maxHeight, MeasureSpec.EXACTLY));
- } else {
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
- }
- }
- };
- rootView.setOrientation(LinearLayout.HORIZONTAL);
- } else {
- rootView = new LinearLayout(getContext());
- rootView.setOrientation(LinearLayout.VERTICAL);
- }
- FrameLayout.LayoutParams rootParams = new FrameLayout.LayoutParams(
- ViewGroup.LayoutParams.MATCH_PARENT,
- ViewGroup.LayoutParams.WRAP_CONTENT);
- rootParams.setMargins(0, 0, 0,
- getContext().getResources().getDimensionPixelSize(R.dimen.media_router_controller_bottom_margin));
- rootView.setLayoutParams(rootParams);
-
- // Start the session activity when a content item (album art, title or subtitle) is clicked.
- View.OnClickListener onClickListener = v -> {
- if (mediaController != null) {
- PendingIntent pi = mediaController.getSessionActivity();
- if (pi != null) {
- try {
- pi.send();
- dismiss();
- } catch (PendingIntent.CanceledException e) {
- Log.e(TAG, pi + " was not sent, it had been canceled.");
- }
- }
- }
- };
-
- LinearLayout.LayoutParams artParams;
- /*
- * On portrait orientation, we want to limit the artView's height to 9/16 of the available
- * width. Reason is that we need to choose the height wisely otherwise we risk the dialog
- * being much larger than the screen, and there doesn't seem to be a good way to know the
- * available height beforehand.
- *
- * On landscape orientation, we want to limit the artView's width to its available height.
- * Otherwise, horizontal images would take too much space and severely restrict the space
- * for episode title and play/pause button.
- *
- * Internal implementation of ImageView only uses the source image's aspect ratio, but we
- * want to impose our own and fallback to the source image's when it is more favorable.
- * Solutions were inspired, among other similar sources, on
- * http://stackoverflow.com/questions/18077325/scale-image-to-fill-imageview-width-and-keep-aspect-ratio
- */
- if (landscape) {
- artView = new AppCompatImageView(getContext()) {
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- int desiredWidth = widthMeasureSpec;
- int desiredMeasureMode = MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY ?
- MeasureSpec.EXACTLY : MeasureSpec.AT_MOST;
- if (MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY) {
- Drawable drawable = getDrawable();
- if (drawable != null) {
- int intrHeight = drawable.getIntrinsicHeight();
- int intrWidth = drawable.getIntrinsicWidth();
- int originalHeight = MeasureSpec.getSize(heightMeasureSpec);
- if (intrHeight < intrWidth) {
- desiredWidth = MeasureSpec.makeMeasureSpec(
- originalHeight, desiredMeasureMode);
- } else {
- desiredWidth = MeasureSpec.makeMeasureSpec(
- Math.round((float) originalHeight * intrWidth / intrHeight),
- desiredMeasureMode);
- }
- }
- }
- super.onMeasure(desiredWidth, heightMeasureSpec);
- }
- };
- artParams = new LinearLayout.LayoutParams(
- ViewGroup.LayoutParams.WRAP_CONTENT,
- ViewGroup.LayoutParams.MATCH_PARENT);
- MarginLayoutParamsCompat.setMarginStart(artParams,
- getContext().getResources().getDimensionPixelSize(R.dimen.media_router_controller_playback_control_horizontal_spacing));
- } else {
- artView = new AppCompatImageView(getContext()) {
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- int desiredHeight = heightMeasureSpec;
- if (MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY) {
- Drawable drawable = getDrawable();
- if (drawable != null) {
- int originalWidth = MeasureSpec.getSize(widthMeasureSpec);
- int intrHeight = drawable.getIntrinsicHeight();
- int intrWidth = drawable.getIntrinsicWidth();
- float scale;
- if (intrHeight*16 > intrWidth*9) {
- // image is taller than 16:9
- scale = (float) originalWidth * 9 / 16 / intrHeight;
- } else {
- // image is more horizontal than 16:9
- scale = (float) originalWidth / intrWidth;
- }
- desiredHeight = MeasureSpec.makeMeasureSpec(
- Math.round(intrHeight * scale),
- MeasureSpec.EXACTLY);
- }
- }
- super.onMeasure(widthMeasureSpec, desiredHeight);
- }
- };
- artParams = new LinearLayout.LayoutParams(
- ViewGroup.LayoutParams.MATCH_PARENT,
- ViewGroup.LayoutParams.WRAP_CONTENT);
- }
- // When we fetch the bitmap, we want to know if we should set a background color or not.
- artView.setTag(landscape);
-
- artView.setScaleType(ImageView.ScaleType.FIT_CENTER);
- artView.setOnClickListener(onClickListener);
-
- artView.setLayoutParams(artParams);
- rootView.addView(artView);
-
- ViewGroup wrapper = rootView;
-
- if (landscape) {
- // Here we wrap with a frame layout because we want to set different layout parameters
- // for landscape orientation.
- wrapper = new FrameLayout(getContext());
- wrapper.setLayoutParams(new LinearLayout.LayoutParams(
- 0,
- ViewGroup.LayoutParams.WRAP_CONTENT, 1f));
- rootView.addView(wrapper);
- rootView.setWeightSum(1f);
- }
-
- View playbackControlLayout = View.inflate(getContext(), R.layout.media_router_controller, wrapper);
-
- titleView = playbackControlLayout.findViewById(R.id.mrc_control_title);
- subtitleView = playbackControlLayout.findViewById(R.id.mrc_control_subtitle);
- playbackControlLayout.findViewById(R.id.mrc_control_title_container).setOnClickListener(onClickListener);
- playPauseButton = playbackControlLayout.findViewById(R.id.mrc_control_play_pause);
- playPauseButton.setOnClickListener(v -> {
- PlaybackStateCompat state;
- if (mediaController != null && (state = mediaController.getPlaybackState()) != null) {
- boolean isPlaying = state.getState() == PlaybackStateCompat.STATE_PLAYING;
- if (isPlaying) {
- mediaController.getTransportControls().pause();
- } else {
- mediaController.getTransportControls().play();
- }
- // Announce the action for accessibility.
- AccessibilityManager accessibilityManager = (AccessibilityManager)
- getContext().getSystemService(Context.ACCESSIBILITY_SERVICE);
- if (accessibilityManager != null && accessibilityManager.isEnabled()) {
- AccessibilityEvent event = AccessibilityEvent.obtain(
- AccessibilityEventCompat.TYPE_ANNOUNCEMENT);
- event.setPackageName(getContext().getPackageName());
- event.setClassName(getClass().getName());
- int resId = isPlaying ? R.string.mr_controller_pause : R.string.mr_controller_play;
- event.getText().add(getContext().getString(resId));
- accessibilityManager.sendAccessibilityEvent(event);
- }
- }
- });
-
- viewsCreated = true;
- updateViews();
- return rootView;
- }
-
- @Override
- public void onDetachedFromWindow() {
- if (fetchArtSubscription != null) {
- fetchArtSubscription.dispose();
- fetchArtSubscription = null;
- }
- super.onDetachedFromWindow();
- }
-
- private void updateViews() {
- if (!viewsCreated || token == null || mediaController == null) {
- rootView.setVisibility(View.GONE);
- return;
- }
- MediaMetadataCompat metadata = mediaController.getMetadata();
- MediaDescriptionCompat description = metadata == null ? null : metadata.getDescription();
- if (description == null) {
- rootView.setVisibility(View.GONE);
- return;
- }
-
- PlaybackStateCompat state = mediaController.getPlaybackState();
- MediaRouter.RouteInfo route = MediaRouter.getInstance(getContext()).getSelectedRoute();
-
- CharSequence title = description.getTitle();
- boolean hasTitle = !TextUtils.isEmpty(title);
- CharSequence subtitle = description.getSubtitle();
- boolean hasSubtitle = !TextUtils.isEmpty(subtitle);
-
- boolean showTitle = false;
- boolean showSubtitle = false;
- if (route.getPresentationDisplay() != null &&
- route.getPresentationDisplay().getDisplayId() != MediaRouter.RouteInfo.PRESENTATION_DISPLAY_ID_NONE) {
- // The user is currently casting screen.
- titleView.setText(R.string.mr_controller_casting_screen);
- showTitle = true;
- } else if (state == null || state.getState() == PlaybackStateCompat.STATE_NONE) {
- // Show "No media selected" as we don't yet know the playback state.
- // (Only exception is bluetooth where we don't show anything.)
- if (!route.isBluetooth()) {
- titleView.setText(R.string.mr_controller_no_media_selected);
- showTitle = true;
- }
- } else if (!hasTitle && !hasSubtitle) {
- titleView.setText(R.string.mr_controller_no_info_available);
- showTitle = true;
- } else {
- if (hasTitle) {
- titleView.setText(title);
- showTitle = true;
- }
- if (hasSubtitle) {
- subtitleView.setText(subtitle);
- showSubtitle = true;
- }
- }
- if (showSubtitle) {
- titleView.setSingleLine();
- } else {
- titleView.setMaxLines(2);
- }
- titleView.setVisibility(showTitle ? View.VISIBLE : View.GONE);
- subtitleView.setVisibility(showSubtitle ? View.VISIBLE : View.GONE);
-
- updateState();
-
- if(rootView.getVisibility() != View.VISIBLE) {
- artView.setVisibility(View.GONE);
- rootView.setVisibility(View.VISIBLE);
- }
-
- if (fetchArtSubscription != null) {
- fetchArtSubscription.dispose();
- }
-
- fetchArtSubscription = Observable.fromCallable(() -> fetchArt(description))
- .subscribeOn(Schedulers.io())
- .observeOn(AndroidSchedulers.mainThread())
- .subscribe(result -> {
- fetchArtSubscription = null;
- if (artView == null) {
- return;
- }
- if (result.first != null) {
- if (!((Boolean) artView.getTag())) {
- artView.setBackgroundColor(result.second);
- }
- artView.setImageBitmap(result.first);
- artView.setVisibility(View.VISIBLE);
- } else {
- artView.setVisibility(View.GONE);
- }
- }, error -> Log.e(TAG, Log.getStackTraceString(error)));
-
- }
-
- private void updateState() {
- PlaybackStateCompat state;
- if (!viewsCreated || mediaController == null ||
- (state = mediaController.getPlaybackState()) == null) {
- return;
- }
- boolean isPlaying = state.getState() == PlaybackStateCompat.STATE_BUFFERING
- || state.getState() == PlaybackStateCompat.STATE_PLAYING;
- boolean supportsPlay = (state.getActions() & (PlaybackStateCompat.ACTION_PLAY
- | PlaybackStateCompat.ACTION_PLAY_PAUSE)) != 0;
- boolean supportsPause = (state.getActions() & (PlaybackStateCompat.ACTION_PAUSE
- | PlaybackStateCompat.ACTION_PLAY_PAUSE)) != 0;
- if (isPlaying && supportsPause) {
- playPauseButton.setVisibility(View.VISIBLE);
- playPauseButton.setImageResource(getThemeResource(getContext(), R.attr.mediaRoutePauseDrawable));
- playPauseButton.setContentDescription(getContext().getResources().getText(R.string.mr_controller_pause));
- } else if (!isPlaying && supportsPlay) {
- playPauseButton.setVisibility(View.VISIBLE);
- playPauseButton.setImageResource(getThemeResource(getContext(), R.attr.mediaRoutePlayDrawable));
- playPauseButton.setContentDescription(getContext().getResources().getText(R.string.mr_controller_play));
- } else {
- playPauseButton.setVisibility(View.GONE);
- }
- }
-
- private static int getThemeResource(Context context, int attr) {
- TypedValue value = new TypedValue();
- return context.getTheme().resolveAttribute(attr, value, true) ? value.resourceId : 0;
- }
-
- @NonNull
- private Pair<Bitmap, Integer> fetchArt(@NonNull MediaDescriptionCompat description) {
- Bitmap iconBitmap = description.getIconBitmap();
- Uri iconUri = description.getIconUri();
- Bitmap art = null;
- if (iconBitmap != null) {
- art = iconBitmap;
- } else if (iconUri != null) {
- try {
- art = Glide.with(getContext().getApplicationContext())
- .asBitmap()
- .load(iconUri.toString())
- .apply(RequestOptions.diskCacheStrategyOf(ApGlideSettings.AP_DISK_CACHE_STRATEGY))
- .submit(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL)
- .get();
- } catch (InterruptedException | ExecutionException e) {
- Log.e(TAG, "Image art load failed", e);
- }
- }
- int backgroundColor = 0;
- if (art != null && art.getWidth()*9 < art.getHeight()*16) {
- // Portrait art requires dominant color as background color.
- Palette palette = new Palette.Builder(art).maximumColorCount(1).generate();
- backgroundColor = palette.getSwatches().isEmpty()
- ? 0 : palette.getSwatches().get(0).getRgb();
- }
- return new Pair<>(art, backgroundColor);
- }
-}
diff --git a/app/src/play/java/de/danoeh/antennapod/fragment/CustomMRControllerDialogFragment.java b/app/src/play/java/de/danoeh/antennapod/fragment/CustomMRControllerDialogFragment.java
deleted file mode 100644
index dad7b0bfd..000000000
--- a/app/src/play/java/de/danoeh/antennapod/fragment/CustomMRControllerDialogFragment.java
+++ /dev/null
@@ -1,15 +0,0 @@
-package de.danoeh.antennapod.fragment;
-
-import android.content.Context;
-import android.os.Bundle;
-import androidx.mediarouter.app.MediaRouteControllerDialog;
-import androidx.mediarouter.app.MediaRouteControllerDialogFragment;
-
-import de.danoeh.antennapod.dialog.CustomMRControllerDialog;
-
-public class CustomMRControllerDialogFragment extends MediaRouteControllerDialogFragment {
- @Override
- public MediaRouteControllerDialog onCreateControllerDialog(Context context, Bundle savedInstanceState) {
- return new CustomMRControllerDialog(context);
- }
-}
diff --git a/app/src/play/java/de/danoeh/antennapod/preferences/PreferenceControllerFlavorHelper.java b/app/src/play/java/de/danoeh/antennapod/preferences/PreferenceControllerFlavorHelper.java
deleted file mode 100644
index b51fb40b0..000000000
--- a/app/src/play/java/de/danoeh/antennapod/preferences/PreferenceControllerFlavorHelper.java
+++ /dev/null
@@ -1,48 +0,0 @@
-package de.danoeh.antennapod.preferences;
-
-import android.content.Context;
-import androidx.annotation.NonNull;
-import androidx.appcompat.app.AlertDialog;
-
-import com.google.android.gms.common.ConnectionResult;
-import com.google.android.gms.common.GoogleApiAvailability;
-
-import de.danoeh.antennapod.PodcastApp;
-import de.danoeh.antennapod.R;
-import de.danoeh.antennapod.core.preferences.UserPreferences;
-import de.danoeh.antennapod.fragment.preferences.PlaybackPreferencesFragment;
-
-/**
- * Implements functions from PreferenceController that are flavor dependent.
- */
-public class PreferenceControllerFlavorHelper {
-
- public static void setupFlavoredUI(PlaybackPreferencesFragment ui) {
- //checks whether Google Play Services is installed on the device (condition necessary for Cast support)
- ui.findPreference(UserPreferences.PREF_CAST_ENABLED).setOnPreferenceChangeListener((preference, o) -> {
- if (o instanceof Boolean && ((Boolean) o)) {
- final int googlePlayServicesCheck = GoogleApiAvailability.getInstance()
- .isGooglePlayServicesAvailable(ui.getActivity());
- if (googlePlayServicesCheck == ConnectionResult.SUCCESS) {
- displayRestartRequiredDialog(ui.requireContext());
- return true;
- } else {
- GoogleApiAvailability.getInstance()
- .getErrorDialog(ui.getActivity(), googlePlayServicesCheck, 0)
- .show();
- return false;
- }
- }
- return true;
- });
- }
-
- private static void displayRestartRequiredDialog(@NonNull Context context) {
- AlertDialog.Builder dialog = new AlertDialog.Builder(context);
- dialog.setTitle(android.R.string.dialog_alert_title);
- dialog.setMessage(R.string.pref_restart_required);
- dialog.setPositiveButton(android.R.string.ok, (dialog1, which) -> PodcastApp.forceRestart());
- dialog.setCancelable(false);
- dialog.show();
- }
-}
diff --git a/app/src/play/res/layout/media_router_controller.xml b/app/src/play/res/layout/media_router_controller.xml
deleted file mode 100644
index bdb1b1cc2..000000000
--- a/app/src/play/res/layout/media_router_controller.xml
+++ /dev/null
@@ -1,41 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@+id/mrc_playback_control"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:paddingTop="@dimen/media_router_controller_playback_control_vertical_padding"
- android:paddingBottom="@dimen/media_router_controller_playback_control_vertical_padding"
- android:paddingLeft="@dimen/media_router_controller_playback_control_start_padding"
- android:paddingStart="@dimen/media_router_controller_playback_control_start_padding"
- android:paddingRight="@dimen/media_router_controller_playback_control_horizontal_spacing"
- android:paddingEnd="@dimen/media_router_controller_playback_control_horizontal_spacing">
- <ImageButton android:id="@+id/mrc_control_play_pause"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginLeft="@dimen/media_router_controller_playback_control_horizontal_spacing"
- android:layout_marginStart="@dimen/media_router_controller_playback_control_horizontal_spacing"
- android:layout_alignParentRight="true"
- android:layout_alignParentEnd="true"
- android:contentDescription="@string/mr_controller_play"
- android:background="?android:attr/selectableItemBackground"/>
-
- <LinearLayout android:id="@+id/mrc_control_title_container"
- android:orientation="vertical"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_alignParentLeft="true"
- android:layout_alignParentStart="true"
- android:layout_toLeftOf="@id/mrc_control_play_pause"
- android:layout_toStartOf="@id/mrc_control_play_pause"
- android:layout_centerVertical="true">
- <TextView android:id="@+id/mrc_control_title"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:textAppearance="@style/TextAppearance.MediaRouter.PrimaryText"/>
- <TextView android:id="@+id/mrc_control_subtitle"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:textAppearance="@style/TextAppearance.MediaRouter.SecondaryText"
- android:singleLine="true" />
- </LinearLayout>
-</RelativeLayout>
diff --git a/build.gradle b/build.gradle
index f6992882c..219510eeb 100644
--- a/build.gradle
+++ b/build.gradle
@@ -3,12 +3,11 @@ buildscript {
google()
mavenCentral()
gradlePluginPortal()
- jcenter()
}
dependencies {
- classpath 'com.android.tools.build:gradle:4.1.3'
- classpath 'com.getkeepsafe.dexcount:dexcount-gradle-plugin:1.0.4'
- classpath 'de.timfreiheit.resourceplaceholders:placeholders:0.3'
+ classpath 'com.android.tools.build:gradle:4.2.2'
+ classpath 'com.getkeepsafe.dexcount:dexcount-gradle-plugin:3.0.0'
+ classpath 'de.timfreiheit.resourceplaceholders:placeholders:0.4'
classpath "gradle.plugin.com.github.spotbugs.snom:spotbugs-gradle-plugin:4.7.0"
}
}
@@ -18,7 +17,6 @@ allprojects {
google()
mavenCentral()
maven { url "https://jitpack.io" }
- jcenter()
}
}
@@ -42,8 +40,12 @@ project.ext {
annotationVersion = "1.2.0"
appcompatVersion = "1.3.1"
coreVersion = "1.5.0"
- mediaVersion = "1.1.0"
+ fragmentVersion = "1.3.6"
+ mediaVersion = "1.4.3"
+ paletteVersion = "1.0.0"
preferenceVersion = "1.1.1"
+ recyclerViewVersion = "1.2.1"
+ viewPager2Version = "1.1.0-beta01"
workManagerVersion = "2.3.4"
googleMaterialVersion = "1.1.0"
@@ -58,11 +60,11 @@ project.ext {
rxAndroidVersion = "2.1.1"
rxJavaVersion = "2.2.2"
iconifyVersion = "2.2.2"
- exoPlayerVersion = "2.11.8"
+ exoPlayerVersion = "2.14.2"
audioPlayerVersion = "v2.0.0"
// Only used for free builds. This version should be updated regularly.
- conscryptVersion = "2.4.0"
+ conscryptVersion = "2.5.2"
// Google Play build
wearableSupportVersion = "2.6.0"
@@ -70,6 +72,8 @@ project.ext {
//Tests
awaitilityVersion = "3.1.6"
+ junitVersion = "4.13"
+ robolectricVersion = "4.5-alpha-1"
robotiumSoloVersion = "5.6.3"
espressoVersion = "3.2.0"
runnerVersion = "1.2.0"
diff --git a/common.gradle b/common.gradle
index 0300ed534..fb3045f81 100644
--- a/common.gradle
+++ b/common.gradle
@@ -1,8 +1,8 @@
android {
- compileSdkVersion 30
+ compileSdkVersion 31
defaultConfig {
- minSdkVersion 16
+ minSdkVersion 19
targetSdkVersion 30
multiDexEnabled false
diff --git a/config/spotbugs/exclude.xml b/config/spotbugs/exclude.xml
index 1f06877a6..97a7481f5 100644
--- a/config/spotbugs/exclude.xml
+++ b/config/spotbugs/exclude.xml
@@ -62,7 +62,7 @@
</Match>
<Match>
<Bug pattern="UUF_UNUSED_PUBLIC_OR_PROTECTED_FIELD"/>
- <Class name="de.danoeh.antennapod.core.storage.NavDrawerData$FolderDrawerItem"/>
+ <Class name="de.danoeh.antennapod.core.storage.NavDrawerData$TagDrawerItem"/>
</Match>
<Match>
<Bug pattern="UWF_UNWRITTEN_PUBLIC_OR_PROTECTED_FIELD"/>
diff --git a/core/build.gradle b/core/build.gradle
index 953a85a97..700487701 100644
--- a/core/build.gradle
+++ b/core/build.gradle
@@ -20,12 +20,15 @@ android {
}
dependencies {
+ implementation project(':event')
implementation project(':model')
implementation project(':net:ssl')
implementation project(':net:sync:gpoddernet')
implementation project(':net:sync:model')
implementation project(':parser:feed')
implementation project(':parser:media')
+ implementation project(':playback:base')
+ implementation project(':playback:cast')
implementation project(':ui:app-start-intent')
implementation project(':ui:common')
implementation project(':ui:png-icons')
@@ -34,7 +37,9 @@ dependencies {
implementation "androidx.appcompat:appcompat:$appcompatVersion"
implementation "androidx.core:core:$coreVersion"
implementation 'androidx.documentfile:documentfile:1.0.1'
+ implementation "androidx.fragment:fragment:$fragmentVersion"
implementation "androidx.media:media:$mediaVersion"
+ implementation "androidx.palette:palette:$paletteVersion"
implementation "androidx.preference:preference:$preferenceVersion"
implementation "androidx.work:work-runtime:$workManagerVersion"
implementation "com.google.android.material:material:$googleMaterialVersion"
@@ -55,20 +60,18 @@ dependencies {
implementation "com.google.android.exoplayer:exoplayer-core:$exoPlayerVersion"
implementation "com.google.android.exoplayer:exoplayer-ui:$exoPlayerVersion"
+ implementation "com.google.android.exoplayer:extension-okhttp:$exoPlayerVersion"
implementation "com.github.AntennaPod:AntennaPod-AudioPlayer:$audioPlayerVersion"
// Non-free dependencies:
- playApi 'com.google.android.libraries.cast.companionlibrary:ccl:2.9.1'
- playApi 'androidx.mediarouter:mediarouter:1.0.0'
- playApi "com.google.android.gms:play-services-cast:$playServicesVersion"
playApi "com.google.android.support:wearable:$wearableSupportVersion"
compileOnly "com.google.android.wearable:wearable:$wearableSupportVersion"
testImplementation 'androidx.test:core:1.2.0'
testImplementation "org.awaitility:awaitility:$awaitilityVersion"
- testImplementation 'junit:junit:4.13'
+ testImplementation "junit:junit:$junitVersion"
testImplementation 'org.mockito:mockito-inline:3.5.13'
- testImplementation 'org.robolectric:robolectric:4.5-alpha-1'
+ testImplementation "org.robolectric:robolectric:$robolectricVersion"
testImplementation 'javax.inject:javax.inject:1'
androidTestImplementation "com.jayway.android.robotium:robotium-solo:$robotiumSoloVersion"
androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion"
diff --git a/core/lint.xml b/core/lint.xml
index fd9f5eb99..aa2c50677 100644
--- a/core/lint.xml
+++ b/core/lint.xml
@@ -8,4 +8,8 @@
<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/debug/res/drawable-nodpi/ic_launcher_foreground_no_finish.png b/core/src/debug/res/drawable-nodpi/ic_launcher_foreground_no_finish.png
new file mode 100644
index 000000000..825421990
--- /dev/null
+++ b/core/src/debug/res/drawable-nodpi/ic_launcher_foreground_no_finish.png
Binary files differ
diff --git a/core/src/free/java/de/danoeh/antennapod/core/CastCallbacks.java b/core/src/free/java/de/danoeh/antennapod/core/CastCallbacks.java
deleted file mode 100644
index 2e266c736..000000000
--- a/core/src/free/java/de/danoeh/antennapod/core/CastCallbacks.java
+++ /dev/null
@@ -1,7 +0,0 @@
-package de.danoeh.antennapod.core;
-
-/**
- * Callbacks for Chromecast support on the core module
- */
-public interface CastCallbacks {
-}
diff --git a/core/src/free/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceFlavorHelper.java b/core/src/free/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceFlavorHelper.java
deleted file mode 100644
index 837cb1bd0..000000000
--- a/core/src/free/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceFlavorHelper.java
+++ /dev/null
@@ -1,54 +0,0 @@
-package de.danoeh.antennapod.core.service.playback;
-
-import android.content.Context;
-import androidx.annotation.StringRes;
-import android.support.v4.media.session.MediaSessionCompat;
-import android.support.v4.media.session.PlaybackStateCompat;
-
-/**
- * Class intended to work along PlaybackService and provide support for different flavors.
- */
-class PlaybackServiceFlavorHelper {
-
- private final PlaybackService.FlavorHelperCallback callback;
-
- PlaybackServiceFlavorHelper(Context context, PlaybackService.FlavorHelperCallback callback) {
- this.callback = callback;
- }
-
- void initializeMediaPlayer(Context context) {
- callback.setMediaPlayer(new LocalPSMP(context, callback.getMediaPlayerCallback()));
- }
-
- void removeCastConsumer() {
- // no-op
- }
-
- boolean castDisconnect(boolean castDisconnect) {
- return false;
- }
-
- boolean onMediaPlayerInfo(Context context, int code, @StringRes int resourceId) {
- return false;
- }
-
- void registerWifiBroadcastReceiver() {
- // no-op
- }
-
- void unregisterWifiBroadcastReceiver() {
- // no-op
- }
-
- boolean onSharedPreference(String key) {
- return false;
- }
-
- void sessionStateAddActionForWear(PlaybackStateCompat.Builder sessionState, String actionName, CharSequence name, int icon) {
- // no-op
- }
-
- void mediaSessionSetExtraForWear(MediaSessionCompat mediaSession) {
- // no-op
- }
-}
diff --git a/core/src/free/java/de/danoeh/antennapod/core/service/playback/WearMediaSession.java b/core/src/free/java/de/danoeh/antennapod/core/service/playback/WearMediaSession.java
new file mode 100644
index 000000000..373b24bc8
--- /dev/null
+++ b/core/src/free/java/de/danoeh/antennapod/core/service/playback/WearMediaSession.java
@@ -0,0 +1,15 @@
+package de.danoeh.antennapod.core.service.playback;
+
+import android.support.v4.media.session.MediaSessionCompat;
+import android.support.v4.media.session.PlaybackStateCompat;
+
+class WearMediaSession {
+ static void sessionStateAddActionForWear(PlaybackStateCompat.Builder sessionState, String actionName,
+ CharSequence name, int icon) {
+ // no-op
+ }
+
+ static void mediaSessionSetExtraForWear(MediaSessionCompat mediaSession) {
+ // no-op
+ }
+}
diff --git a/core/src/free/res/values/strings.xml b/core/src/free/res/values/strings.xml
deleted file mode 100644
index fb49bbbe7..000000000
--- a/core/src/free/res/values/strings.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<resources>
- <string name="pref_cast_message" translatable="false">@string/pref_cast_message_free_flavor</string>
-</resources>
diff --git a/core/src/free/java/de/danoeh/antennapod/core/ClientConfig.java b/core/src/main/java/de/danoeh/antennapod/core/ClientConfig.java
index 755bec14e..ac67fb042 100644
--- a/core/src/free/java/de/danoeh/antennapod/core/ClientConfig.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/ClientConfig.java
@@ -30,8 +30,6 @@ public class ClientConfig {
public static DownloadServiceCallbacks downloadServiceCallbacks;
- public static CastCallbacks castCallbacks;
-
private static boolean initialized = false;
public static synchronized void initialize(Context context) {
diff --git a/core/src/main/java/de/danoeh/antennapod/core/export/favorites/FavoritesWriter.java b/core/src/main/java/de/danoeh/antennapod/core/export/favorites/FavoritesWriter.java
index 9bc273c9e..29de6ca80 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/export/favorites/FavoritesWriter.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/export/favorites/FavoritesWriter.java
@@ -117,10 +117,17 @@ public class FavoritesWriter implements ExportWriter {
}
private void writeFavoriteItem(Writer writer, FeedItem item, String favoriteTemplate) throws IOException {
- String favItem = favoriteTemplate
- .replace("{FAV_TITLE}", item.getTitle().trim())
- .replace("{FAV_WEBSITE}", item.getLink())
- .replace("{FAV_MEDIA}", item.getMedia().getDownload_url());
+ String favItem = favoriteTemplate.replace("{FAV_TITLE}", item.getTitle().trim());
+ if (item.getLink() != null) {
+ favItem = favItem.replace("{FAV_WEBSITE}", item.getLink());
+ } else {
+ favItem = favItem.replace("{FAV_WEBSITE}", "");
+ }
+ if (item.getMedia() != null && item.getMedia().getDownload_url() != null) {
+ favItem = favItem.replace("{FAV_MEDIA}", item.getMedia().getDownload_url());
+ } else {
+ favItem = favItem.replace("{FAV_MEDIA}", "");
+ }
writer.append(favItem);
}
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 82583b7b5..5d685c24f 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
@@ -178,7 +178,7 @@ public class LocalFeedUpdater {
private static FeedItem createFeedItem(Feed feed, DocumentFile file, Context context) {
FeedItem item = new FeedItem(0, file.getName(), UUID.randomUUID().toString(),
file.getName(), new Date(file.lastModified()), FeedItem.UNPLAYED, feed);
- item.setAutoDownload(false);
+ item.disableAutoDownload();
long size = file.length();
FeedMedia media = new FeedMedia(0, item, 0, 0, size, file.getType(),
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 defe6c9f8..797addcc1 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
@@ -2,6 +2,7 @@ package de.danoeh.antennapod.core.glide;
import android.annotation.SuppressLint;
import android.content.Context;
+import android.graphics.Bitmap;
import android.util.Log;
import androidx.annotation.NonNull;
@@ -11,7 +12,6 @@ import com.bumptech.glide.Registry;
import com.bumptech.glide.annotation.GlideModule;
import com.bumptech.glide.load.DecodeFormat;
import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory;
-import com.bumptech.glide.load.model.StringLoader;
import com.bumptech.glide.module.AppGlideModule;
import de.danoeh.antennapod.model.feed.EmbeddedChapterImage;
@@ -43,8 +43,9 @@ public class ApGlideModule extends AppGlideModule {
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 ApOkHttpUrlLoader.Factory());
- registry.append(String.class, InputStream.class, new StringLoader.StreamFactory());
+ registry.append(String.class, InputStream.class, new NoHttpStringLoader.StreamFactory());
registry.append(EmbeddedChapterImage.class, ByteBuffer.class, new ChapterImageModelLoader.Factory());
+ registry.register(Bitmap.class, PaletteBitmap.class, new PaletteBitmapTranscoder());
}
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/glide/NoHttpStringLoader.java b/core/src/main/java/de/danoeh/antennapod/core/glide/NoHttpStringLoader.java
new file mode 100644
index 000000000..9cda3b1aa
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/glide/NoHttpStringLoader.java
@@ -0,0 +1,39 @@
+package de.danoeh.antennapod.core.glide;
+
+import android.net.Uri;
+import androidx.annotation.NonNull;
+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.load.model.StringLoader;
+
+import java.io.InputStream;
+
+/**
+ * StringLoader that does not handle http/https urls. Used to avoid fallback to StringLoader when
+ * AntennaPod blocks mobile image loading.
+ */
+public final class NoHttpStringLoader extends StringLoader<InputStream> {
+
+ public static class StreamFactory implements ModelLoaderFactory<String, InputStream> {
+ @NonNull
+ @Override
+ public ModelLoader<String, InputStream> build(@NonNull MultiModelLoaderFactory multiFactory) {
+ return new NoHttpStringLoader(multiFactory.build(Uri.class, InputStream.class));
+ }
+
+ @Override
+ public void teardown() {
+ // Do nothing.
+ }
+ }
+
+ public NoHttpStringLoader(ModelLoader<Uri, InputStream> uriLoader) {
+ super(uriLoader);
+ }
+
+ @Override
+ public boolean handles(@NonNull String model) {
+ return !model.startsWith("http") && super.handles(model);
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/glide/PaletteBitmap.java b/core/src/main/java/de/danoeh/antennapod/core/glide/PaletteBitmap.java
new file mode 100644
index 000000000..59ecd3d0d
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/glide/PaletteBitmap.java
@@ -0,0 +1,20 @@
+/*
+ * Source: https://github.com/bumptech/glide/wiki/Custom-targets#palette-example
+ */
+
+package de.danoeh.antennapod.core.glide;
+
+import android.graphics.Bitmap;
+
+import androidx.annotation.NonNull;
+import androidx.palette.graphics.Palette;
+
+public class PaletteBitmap {
+ public final Palette palette;
+ public final Bitmap bitmap;
+
+ public PaletteBitmap(@NonNull Bitmap bitmap, Palette palette) {
+ this.bitmap = bitmap;
+ this.palette = palette;
+ }
+} \ No newline at end of file
diff --git a/core/src/main/java/de/danoeh/antennapod/core/glide/PaletteBitmapResource.java b/core/src/main/java/de/danoeh/antennapod/core/glide/PaletteBitmapResource.java
new file mode 100644
index 000000000..fef0bccd3
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/glide/PaletteBitmapResource.java
@@ -0,0 +1,40 @@
+/*
+ * Source: https://github.com/bumptech/glide/wiki/Custom-targets#palette-example
+ */
+
+package de.danoeh.antennapod.core.glide;
+
+import androidx.annotation.NonNull;
+
+import com.bumptech.glide.load.engine.Resource;
+import com.bumptech.glide.util.Util;
+
+public class PaletteBitmapResource implements Resource<PaletteBitmap> {
+ private final PaletteBitmap paletteBitmap;
+
+ public PaletteBitmapResource(@NonNull PaletteBitmap paletteBitmap) {
+ this.paletteBitmap = paletteBitmap;
+ }
+
+ @NonNull
+ @Override
+ public Class<PaletteBitmap> getResourceClass() {
+ return PaletteBitmap.class;
+ }
+
+ @NonNull
+ @Override
+ public PaletteBitmap get() {
+ return paletteBitmap;
+ }
+
+ @Override
+ public int getSize() {
+ return Util.getBitmapByteSize(paletteBitmap.bitmap);
+ }
+
+ @Override
+ public void recycle() {
+ paletteBitmap.bitmap.recycle();
+ }
+} \ No newline at end of file
diff --git a/core/src/main/java/de/danoeh/antennapod/core/glide/PaletteBitmapTranscoder.java b/core/src/main/java/de/danoeh/antennapod/core/glide/PaletteBitmapTranscoder.java
new file mode 100644
index 000000000..a6a606cb8
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/glide/PaletteBitmapTranscoder.java
@@ -0,0 +1,32 @@
+/*
+ * Source: https://github.com/bumptech/glide/wiki/Custom-targets#palette-example
+ */
+
+package de.danoeh.antennapod.core.glide;
+
+import android.graphics.Bitmap;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.palette.graphics.Palette;
+
+import com.bumptech.glide.load.Options;
+import com.bumptech.glide.load.engine.Resource;
+import com.bumptech.glide.load.resource.transcode.ResourceTranscoder;
+
+import de.danoeh.antennapod.core.preferences.UserPreferences;
+
+public class PaletteBitmapTranscoder implements ResourceTranscoder<Bitmap, PaletteBitmap> {
+
+ @Nullable
+ @Override
+ public Resource<PaletteBitmap> transcode(@NonNull Resource<Bitmap> toTranscode, @NonNull Options options) {
+ Bitmap bitmap = toTranscode.get();
+ Palette palette = null;
+ if (UserPreferences.shouldShowSubscriptionTitle()) {
+ palette = new Palette.Builder(bitmap).generate();
+ }
+ PaletteBitmap result = new PaletteBitmap(bitmap, palette);
+ return new PaletteBitmapResource(result);
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/glide/ResizingOkHttpStreamFetcher.java b/core/src/main/java/de/danoeh/antennapod/core/glide/ResizingOkHttpStreamFetcher.java
index 11e2f944e..1871723bb 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/glide/ResizingOkHttpStreamFetcher.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/glide/ResizingOkHttpStreamFetcher.java
@@ -3,12 +3,13 @@ package de.danoeh.antennapod.core.glide;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Build;
+import android.util.Log;
+
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.bumptech.glide.Priority;
import com.bumptech.glide.integration.okhttp3.OkHttpStreamFetcher;
import com.bumptech.glide.load.model.GlideUrl;
-import com.google.android.exoplayer2.util.Log;
import okhttp3.Call;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
@@ -22,7 +23,7 @@ import java.io.InputStream;
import java.io.OutputStream;
public class ResizingOkHttpStreamFetcher extends OkHttpStreamFetcher {
- private static final String TAG = "ResizingOkHttpStreamFetcher";
+ private static final String TAG = "ResizingOkHttpStreamFet";
private static final int MAX_DIMENSIONS = 1500;
private static final int MAX_FILE_SIZE = 1024 * 1024; // 1 MB
diff --git a/core/src/main/java/de/danoeh/antennapod/core/preferences/GpodnetPreferences.java b/core/src/main/java/de/danoeh/antennapod/core/preferences/GpodnetPreferences.java
deleted file mode 100644
index e338e0d01..000000000
--- a/core/src/main/java/de/danoeh/antennapod/core/preferences/GpodnetPreferences.java
+++ /dev/null
@@ -1,115 +0,0 @@
-package de.danoeh.antennapod.core.preferences;
-
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.util.Log;
-import de.danoeh.antennapod.core.BuildConfig;
-import de.danoeh.antennapod.core.ClientConfig;
-import de.danoeh.antennapod.core.sync.SyncService;
-import de.danoeh.antennapod.net.sync.gpoddernet.GpodnetService;
-
-/**
- * Manages preferences for accessing gpodder.net service
- */
-public class GpodnetPreferences {
-
- private GpodnetPreferences(){}
-
- private static final String TAG = "GpodnetPreferences";
-
- private static final String PREF_NAME = "gpodder.net";
- private static final String PREF_GPODNET_USERNAME = "de.danoeh.antennapod.preferences.gpoddernet.username";
- private static final String PREF_GPODNET_PASSWORD = "de.danoeh.antennapod.preferences.gpoddernet.password";
- private static final String PREF_GPODNET_DEVICEID = "de.danoeh.antennapod.preferences.gpoddernet.deviceID";
- private static final String PREF_GPODNET_HOSTNAME = "prefGpodnetHostname";
-
- private static String username;
- private static String password;
- private static String deviceID;
- private static String hosturl;
-
- private static boolean preferencesLoaded = false;
-
- private static SharedPreferences getPreferences() {
- return ClientConfig.applicationCallbacks.getApplicationInstance().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
- }
-
- private static synchronized void ensurePreferencesLoaded() {
- if (!preferencesLoaded) {
- SharedPreferences prefs = getPreferences();
- username = prefs.getString(PREF_GPODNET_USERNAME, null);
- password = prefs.getString(PREF_GPODNET_PASSWORD, null);
- deviceID = prefs.getString(PREF_GPODNET_DEVICEID, null);
- hosturl = prefs.getString(PREF_GPODNET_HOSTNAME, GpodnetService.DEFAULT_BASE_HOST);
-
- preferencesLoaded = true;
- }
- }
-
- private static void writePreference(String key, String value) {
- SharedPreferences.Editor editor = getPreferences().edit();
- editor.putString(key, value);
- editor.apply();
- }
-
- public static String getUsername() {
- ensurePreferencesLoaded();
- return username;
- }
-
- public static void setUsername(String username) {
- GpodnetPreferences.username = username;
- writePreference(PREF_GPODNET_USERNAME, username);
- }
-
- public static String getPassword() {
- ensurePreferencesLoaded();
- return password;
- }
-
- public static void setPassword(String password) {
- GpodnetPreferences.password = password;
- writePreference(PREF_GPODNET_PASSWORD, password);
- }
-
- public static String getDeviceID() {
- ensurePreferencesLoaded();
- return deviceID;
- }
-
- public static void setDeviceID(String deviceID) {
- GpodnetPreferences.deviceID = deviceID;
- writePreference(PREF_GPODNET_DEVICEID, deviceID);
- }
-
- public static String getHosturl() {
- ensurePreferencesLoaded();
- return hosturl;
- }
-
- public static void setHosturl(String value) {
- if (!value.equals(hosturl)) {
- logout();
- writePreference(PREF_GPODNET_HOSTNAME, value);
- hosturl = value;
- }
- }
-
- /**
- * Returns true if device ID, username and password have a non-null value
- */
- public static boolean loggedIn() {
- ensurePreferencesLoaded();
- return deviceID != null && username != null && password != null;
- }
-
- public static synchronized void logout() {
- if (BuildConfig.DEBUG) Log.d(TAG, "Logout: Clearing preferences");
- setUsername(null);
- setPassword(null);
- setDeviceID(null);
- SyncService.clearQueue(ClientConfig.applicationCallbacks.getApplicationInstance());
- UserPreferences.setGpodnetNotificationsEnabled();
- }
-
-}
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 9c73ed9ae..f0c61403f 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
@@ -5,11 +5,11 @@ import android.content.SharedPreferences;
import androidx.preference.PreferenceManager;
import android.util.Log;
-import de.danoeh.antennapod.core.event.PlayerStatusEvent;
+import de.danoeh.antennapod.event.PlayerStatusEvent;
import de.danoeh.antennapod.model.feed.FeedMedia;
import de.danoeh.antennapod.model.playback.MediaType;
-import de.danoeh.antennapod.core.service.playback.PlayerStatus;
import de.danoeh.antennapod.model.playback.Playable;
+import de.danoeh.antennapod.playback.base.PlayerStatus;
import org.greenrobot.eventbus.EventBus;
import static de.danoeh.antennapod.model.feed.FeedPreferences.SPEED_USE_GLOBAL;
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 8b36d88a1..08daf01e2 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
@@ -69,6 +69,7 @@ public class UserPreferences {
public static final String PREF_BACK_BUTTON_BEHAVIOR = "prefBackButtonBehavior";
private static final String PREF_BACK_BUTTON_GO_TO_PAGE = "prefBackButtonGoToPage";
public static final String PREF_FILTER_FEED = "prefSubscriptionsFilter";
+ public static final String PREF_SUBSCRIPTION_TITLE = "prefSubscriptionTitle";
public static final String PREF_QUEUE_KEEP_SORTED = "prefQueueKeepSorted";
public static final String PREF_QUEUE_KEEP_SORTED_ORDER = "prefQueueKeepSortedOrder";
@@ -85,7 +86,7 @@ public class UserPreferences {
private static final String PREF_AUTO_DELETE = "prefAutoDelete";
public static final String PREF_SMART_MARK_AS_PLAYED_SECS = "prefSmartMarkAsPlayedSecs";
private static final String PREF_PLAYBACK_SPEED_ARRAY = "prefPlaybackSpeedArray";
- private static final String PREF_PAUSE_PLAYBACK_FOR_FOCUS_LOSS = "prefPauseForFocusLoss";
+ 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";
@@ -207,7 +208,7 @@ public class UserPreferences {
public static List<Integer> getCompactNotificationButtons() {
String[] buttons = TextUtils.split(
prefs.getString(PREF_COMPACT_NOTIFICATION_BUTTONS,
- String.valueOf(NOTIFICATION_BUTTON_SKIP)),
+ NOTIFICATION_BUTTON_REWIND + "," + NOTIFICATION_BUTTON_FAST_FORWARD),
",");
List<Integer> notificationButtons = new ArrayList<>();
for (String button : buttons) {
@@ -467,7 +468,7 @@ public class UserPreferences {
}
public static boolean shouldPauseForFocusLoss() {
- return prefs.getBoolean(PREF_PAUSE_PLAYBACK_FOR_FOCUS_LOSS, false);
+ return prefs.getBoolean(PREF_PAUSE_PLAYBACK_FOR_FOCUS_LOSS, true);
}
@@ -530,7 +531,8 @@ public class UserPreferences {
private static void setAllowMobileFor(String type, boolean allow) {
HashSet<String> defaultValue = new HashSet<>();
defaultValue.add("images");
- Set<String> allowed = prefs.getStringSet(PREF_MOBILE_UPDATE, defaultValue);
+ final Set<String> getValueStringSet = prefs.getStringSet(PREF_MOBILE_UPDATE, defaultValue);
+ final Set<String> allowed = new HashSet<>(getValueStringSet);
if (allow) {
allowed.add(type);
} else {
@@ -609,6 +611,11 @@ public class UserPreferences {
public static void setProxyConfig(ProxyConfig config) {
SharedPreferences.Editor editor = prefs.edit();
editor.putString(PREF_PROXY_TYPE, config.type.name());
+ Proxy.Type type = Proxy.Type.valueOf(config.type.name());
+ if (type == Proxy.Type.DIRECT) {
+ editor.apply();
+ return;
+ }
if(TextUtils.isEmpty(config.host)) {
editor.remove(PREF_PROXY_HOST);
} else {
@@ -1084,4 +1091,13 @@ public class UserPreferences {
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/service/download/DownloadService.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadService.java
index 26ab4a414..6cf6ce107 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
@@ -8,6 +8,7 @@ import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Binder;
+import android.os.Build;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
@@ -20,7 +21,6 @@ import androidx.annotation.VisibleForTesting;
import androidx.core.app.ServiceCompat;
import de.danoeh.antennapod.core.R;
-import de.danoeh.antennapod.core.sync.SyncService;
import org.apache.commons.io.FileUtils;
import org.greenrobot.eventbus.EventBus;
@@ -40,7 +40,8 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import de.danoeh.antennapod.core.event.DownloadEvent;
-import de.danoeh.antennapod.core.event.FeedItemEvent;
+import de.danoeh.antennapod.core.util.download.ConnectionStateMonitor;
+import de.danoeh.antennapod.event.FeedItemEvent;
import de.danoeh.antennapod.model.feed.Feed;
import de.danoeh.antennapod.model.feed.FeedItem;
import de.danoeh.antennapod.model.feed.FeedMedia;
@@ -121,7 +122,7 @@ public class DownloadService extends Service {
private static final int SCHED_EX_POOL_SIZE = 1;
private final ScheduledThreadPoolExecutor schedExecutor;
private static DownloaderFactory downloaderFactory = new DefaultDownloaderFactory();
-
+ private ConnectionStateMonitor connectionMonitor;
private final IBinder mBinder = new LocalBinder();
private class LocalBinder extends Binder {
@@ -192,6 +193,11 @@ public class DownloadService extends Service {
cancelDownloadReceiverFilter.addAction(ACTION_CANCEL_DOWNLOAD);
registerReceiver(cancelDownloadReceiver, cancelDownloadReceiverFilter);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ connectionMonitor = new ConnectionStateMonitor();
+ connectionMonitor.enable(getApplicationContext());
+ }
+
downloadCompletionThread.start();
}
@@ -226,10 +232,9 @@ public class DownloadService extends Service {
downloadPostFuture.cancel(true);
}
unregisterReceiver(cancelDownloadReceiver);
-
- // if this was the initial gpodder sync, i.e. we just synced the feeds successfully,
- // it is now time to sync the episode actions
- SyncService.sync(this);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ connectionMonitor.disable(getApplicationContext());
+ }
// start auto download in case anything new has shown up
DBTasks.autodownloadUndownloadedItems(getApplicationContext());
@@ -326,18 +331,8 @@ public class DownloadService extends Service {
if (item == null) {
return;
}
- boolean unknownHost = status.getReason() == DownloadError.ERROR_UNKNOWN_HOST;
- boolean unsupportedType = status.getReason() == DownloadError.ERROR_UNSUPPORTED_TYPE;
- boolean wrongSize = status.getReason() == DownloadError.ERROR_IO_WRONG_SIZE;
-
- if (! (unknownHost || unsupportedType || wrongSize)) {
- try {
- DBWriter.saveFeedItemAutoDownloadFailed(item).get();
- } catch (ExecutionException | InterruptedException e) {
- Log.d(TAG, "Ignoring exception while setting item download status");
- e.printStackTrace();
- }
- }
+ item.increaseFailedAutoDownloadAttempts(System.currentTimeMillis());
+ DBWriter.setFeedItem(item);
// to make lists reload the failed item, we fake an item update
EventBus.getDefault().post(FeedItemEvent.updated(item));
}
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 781110f82..cbfb2cede 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
@@ -4,6 +4,7 @@ import androidx.annotation.NonNull;
import android.text.TextUtils;
import android.util.Log;
+import de.danoeh.antennapod.core.util.NetworkUtils;
import okhttp3.CacheControl;
import org.apache.commons.io.IOUtils;
@@ -19,8 +20,6 @@ import java.net.URI;
import java.net.UnknownHostException;
import java.util.Collections;
import java.util.Date;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
import de.danoeh.antennapod.core.R;
import de.danoeh.antennapod.model.feed.FeedMedia;
@@ -39,7 +38,6 @@ public class HttpDownloader extends Downloader {
private static final String TAG = "HttpDownloader";
private static final int BUFFER_SIZE = 8 * 1024;
- private static final String REGEX_PATTERN_IP_ADDRESS = "([0-9]{1,3}[\\.]){3}[0-9]{1,3}";
public HttpDownloader(@NonNull DownloadRequest request) {
super(request);
@@ -259,21 +257,14 @@ public class HttpDownloader extends Downloader {
onFail(DownloadError.ERROR_UNKNOWN_HOST, e.getMessage());
} catch (IOException e) {
e.printStackTrace();
+ if (NetworkUtils.wasDownloadBlocked(e)) {
+ onFail(DownloadError.ERROR_IO_BLOCKED, e.getMessage());
+ return;
+ }
String message = e.getMessage();
- if (message != null) {
- // Try to parse message for a more detailed error message
- Pattern pattern = Pattern.compile(REGEX_PATTERN_IP_ADDRESS);
- Matcher matcher = pattern.matcher(message);
- if (matcher.find()) {
- String ip = matcher.group();
- if (ip.startsWith("127.") || ip.startsWith("0.")) {
- onFail(DownloadError.ERROR_IO_BLOCKED, e.getMessage());
- return;
- }
- } else if (message.contains("Trust anchor for certification path not found")) {
- onFail(DownloadError.ERROR_CERTIFICATE, e.getMessage());
- return;
- }
+ if (message != null && message.contains("Trust anchor for certification path not found")) {
+ onFail(DownloadError.ERROR_CERTIFICATE, e.getMessage());
+ return;
}
onFail(DownloadError.ERROR_IO_ERROR, e.getMessage());
} catch (NullPointerException 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 869205b64..f7ed049cd 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
@@ -8,6 +8,7 @@ import android.content.Intent;
import android.content.res.Resources;
import android.graphics.Bitmap;
+import android.os.Build;
import android.util.Log;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
@@ -68,7 +69,8 @@ public class NewEpisodesNotification {
intent.setComponent(new ComponentName(context, "de.danoeh.antennapod.activity.MainActivity"));
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
intent.putExtra("fragment_feed_id", feed.getId());
- PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0);
+ PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent,
+ (Build.VERSION.SDK_INT >= 23 ? PendingIntent.FLAG_IMMUTABLE : 0));
Notification notification = new NotificationCompat.Builder(
context, NotificationUtils.CHANNEL_ID_EPISODE_NOTIFICATIONS)
@@ -79,6 +81,7 @@ public class NewEpisodesNotification {
.setContentIntent(pendingIntent)
.setGroup(GROUP_KEY)
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY)
+ .setOnlyAlertOnce(true)
.setAutoCancel(true)
.build();
@@ -92,7 +95,8 @@ public class NewEpisodesNotification {
intent.setComponent(new ComponentName(context, "de.danoeh.antennapod.activity.MainActivity"));
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
intent.putExtra("fragment_tag", "EpisodesFragment");
- PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0);
+ PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent,
+ (Build.VERSION.SDK_INT >= 23 ? PendingIntent.FLAG_IMMUTABLE : 0));
Notification notificationGroupSummary = new NotificationCompat.Builder(
context, NotificationUtils.CHANNEL_ID_EPISODE_NOTIFICATIONS)
@@ -102,6 +106,7 @@ public class NewEpisodesNotification {
.setGroup(GROUP_KEY)
.setGroupSummary(true)
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY)
+ .setOnlyAlertOnce(true)
.setAutoCancel(true)
.build();
notificationManager.notify(NotificationUtils.CHANNEL_ID_EPISODE_NOTIFICATIONS, 0, notificationGroupSummary);
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 8c9035621..541e17cf6 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
@@ -6,21 +6,22 @@ import android.util.Log;
import androidx.annotation.NonNull;
+import org.greenrobot.eventbus.EventBus;
+
import java.io.File;
import java.util.concurrent.ExecutionException;
-import de.danoeh.antennapod.core.event.UnreadItemsUpdateEvent;
-import de.danoeh.antennapod.model.feed.FeedItem;
-import de.danoeh.antennapod.model.feed.FeedMedia;
+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.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.core.sync.SyncService;
+import de.danoeh.antennapod.model.feed.FeedItem;
+import de.danoeh.antennapod.model.feed.FeedMedia;
import de.danoeh.antennapod.net.sync.model.EpisodeAction;
-import org.greenrobot.eventbus.EventBus;
/**
* Handles a completed media download.
@@ -82,7 +83,7 @@ public class MediaDownloadedHandler implements Runnable {
// we've received the media, we don't want to autodownload it again
if (item != null) {
- item.setAutoDownload(false);
+ item.disableAutoDownload();
// setFeedItem() signals (via EventBus) that the item has been updated,
// so we do it after the enclosing media has been updated above,
// to ensure subscribers will get the updated FeedMedia as well
@@ -103,7 +104,7 @@ public class MediaDownloadedHandler implements Runnable {
EpisodeAction action = new EpisodeAction.Builder(item, EpisodeAction.DOWNLOAD)
.currentTimestamp()
.build();
- SyncService.enqueueEpisodeAction(context, action);
+ SynchronizationQueueSink.enqueueEpisodeActionIfSynchronizationIsActive(context, action);
}
}
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 0a9bf5f43..d4008b3f2 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
@@ -5,36 +5,43 @@ import android.net.Uri;
import android.text.TextUtils;
import android.util.Log;
import android.view.SurfaceHolder;
+import androidx.annotation.NonNull;
+import androidx.core.util.Consumer;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.DefaultLoadControl;
import com.google.android.exoplayer2.DefaultRenderersFactory;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.SeekParameters;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.audio.AudioAttributes;
+import com.google.android.exoplayer2.ext.okhttp.OkHttpDataSource;
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
import com.google.android.exoplayer2.extractor.mp3.Mp3Extractor;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
+import com.google.android.exoplayer2.trackselection.ExoTrackSelection;
import com.google.android.exoplayer2.trackselection.MappingTrackSelector;
-import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.ui.DefaultTrackNameProvider;
import com.google.android.exoplayer2.ui.TrackNameProvider;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
-import com.google.android.exoplayer2.upstream.DefaultHttpDataSource;
-import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
+import com.google.android.exoplayer2.upstream.HttpDataSource;
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.util.NetworkUtils;
import de.danoeh.antennapod.core.util.playback.IPlayer;
+import de.danoeh.antennapod.playback.base.PlaybackServiceMediaPlayer;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
@@ -42,6 +49,7 @@ import org.antennapod.audio.MediaPlayer;
import java.util.ArrayList;
import java.util.Collections;
+import java.util.HashMap;
import java.util.List;
import java.util.concurrent.TimeUnit;
@@ -54,7 +62,7 @@ public class ExoPlayerWrapper implements IPlayer {
private MediaSource mediaSource;
private MediaPlayer.OnSeekCompleteListener audioSeekCompleteListener;
private MediaPlayer.OnCompletionListener audioCompletionListener;
- private MediaPlayer.OnErrorListener audioErrorListener;
+ private Consumer<String> audioErrorListener;
private MediaPlayer.OnBufferingUpdateListener bufferingUpdateListener;
private PlaybackParameters playbackParameters;
private MediaPlayer.OnInfoListener infoListener;
@@ -82,12 +90,12 @@ public class ExoPlayerWrapper implements IPlayer {
trackSelector = new DefaultTrackSelector(context);
exoPlayer = new SimpleExoPlayer.Builder(context, new DefaultRenderersFactory(context))
.setTrackSelector(trackSelector)
- .setLoadControl(loadControl.createDefaultLoadControl())
+ .setLoadControl(loadControl.build())
.build();
exoPlayer.setSeekParameters(SeekParameters.EXACT);
- exoPlayer.addListener(new Player.EventListener() {
+ exoPlayer.addListener(new Player.Listener() {
@Override
- public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
+ public void onPlaybackStateChanged(@Player.State int playbackState) {
if (audioCompletionListener != null && playbackState == Player.STATE_ENDED) {
audioCompletionListener.onCompletion(null);
} else if (infoListener != null && playbackState == Player.STATE_BUFFERING) {
@@ -98,15 +106,25 @@ public class ExoPlayerWrapper implements IPlayer {
}
@Override
- public void onPlayerError(ExoPlaybackException error) {
+ public void onPlayerError(@NonNull ExoPlaybackException error) {
if (audioErrorListener != null) {
- audioErrorListener.onError(null, error.type + ERROR_CODE_OFFSET, 0);
+ if (NetworkUtils.wasDownloadBlocked(error)) {
+ audioErrorListener.accept(context.getString(R.string.download_error_blocked));
+ } else {
+ Throwable cause = error.getCause();
+ if (cause instanceof HttpDataSource.HttpDataSourceException) {
+ cause = cause.getCause();
+ }
+ audioErrorListener.accept(cause != null ? cause.getMessage() : error.getMessage());
+ }
}
}
@Override
- public void onSeekProcessed() {
- if (audioSeekCompleteListener != null) {
+ public void onPositionDiscontinuity(@NonNull Player.PositionInfo oldPosition,
+ @NonNull Player.PositionInfo newPosition,
+ @Player.DiscontinuityReason int reason) {
+ if (audioSeekCompleteListener != null && reason == Player.DISCONTINUITY_REASON_SEEK) {
audioSeekCompleteListener.onSeekComplete(null);
}
}
@@ -143,12 +161,13 @@ public class ExoPlayerWrapper implements IPlayer {
@Override
public void pause() {
- exoPlayer.setPlayWhenReady(false);
+ exoPlayer.pause();
}
@Override
public void prepare() throws IllegalStateException {
- exoPlayer.prepare(mediaSource, false, true);
+ exoPlayer.setMediaSource(mediaSource, false);
+ exoPlayer.prepare();
}
@Override
@@ -184,31 +203,31 @@ public class ExoPlayerWrapper implements IPlayer {
b.setContentType(i);
b.setFlags(a.flags);
b.setUsage(a.usage);
- exoPlayer.setAudioAttributes(b.build());
+ exoPlayer.setAudioAttributes(b.build(), false);
}
public void setDataSource(String s, String user, String password)
throws IllegalArgumentException, IllegalStateException {
Log.d(TAG, "setDataSource: " + s);
- DefaultHttpDataSourceFactory httpDataSourceFactory = new DefaultHttpDataSourceFactory(
- ClientConfig.USER_AGENT, null,
- DefaultHttpDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS,
- DefaultHttpDataSource.DEFAULT_READ_TIMEOUT_MILLIS,
- true);
+ final OkHttpDataSource.Factory httpDataSourceFactory =
+ new OkHttpDataSource.Factory(AntennapodHttpClient.getHttpClient())
+ .setUserAgent(ClientConfig.USER_AGENT);
if (!TextUtils.isEmpty(user) && !TextUtils.isEmpty(password)) {
- httpDataSourceFactory.getDefaultRequestProperties().set("Authorization",
- HttpDownloader.encodeCredentials(
- user,
- password,
- "ISO-8859-1"));
+ final HashMap<String, String> requestProperties = new HashMap<>();
+ requestProperties.put(
+ "Authorization",
+ HttpDownloader.encodeCredentials(user, password, "ISO-8859-1")
+ );
+ httpDataSourceFactory.setDefaultRequestProperties(requestProperties);
}
DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(context, null, httpDataSourceFactory);
DefaultExtractorsFactory extractorsFactory = new DefaultExtractorsFactory();
extractorsFactory.setConstantBitrateSeekingEnabled(true);
extractorsFactory.setMp3ExtractorFlags(Mp3Extractor.FLAG_DISABLE_ID3_METADATA);
ProgressiveMediaSource.Factory f = new ProgressiveMediaSource.Factory(dataSourceFactory, extractorsFactory);
- mediaSource = f.createMediaSource(Uri.parse(s));
+ final MediaItem mediaItem = MediaItem.fromUri(Uri.parse(s));
+ mediaSource = f.createMediaSource(mediaItem);
}
@Override
@@ -223,7 +242,8 @@ public class ExoPlayerWrapper implements IPlayer {
@Override
public void setPlaybackParams(float speed, boolean skipSilence) {
- playbackParameters = new PlaybackParameters(speed, playbackParameters.pitch, skipSilence);
+ playbackParameters = new PlaybackParameters(speed, playbackParameters.pitch);
+ exoPlayer.setSkipSilenceEnabled(skipSilence);
exoPlayer.setPlaybackParameters(playbackParameters);
}
@@ -244,7 +264,7 @@ public class ExoPlayerWrapper implements IPlayer {
@Override
public void start() {
- exoPlayer.setPlayWhenReady(true);
+ exoPlayer.play();
// Can't set params when paused - so always set it on start in case they changed
exoPlayer.setPlaybackParameters(playbackParameters);
}
@@ -304,7 +324,7 @@ public class ExoPlayerWrapper implements IPlayer {
TrackSelectionArray trackSelections = exoPlayer.getCurrentTrackSelections();
List<Format> availableFormats = getFormats();
for (int i = 0; i < trackSelections.length; i++) {
- TrackSelection track = trackSelections.get(i);
+ ExoTrackSelection track = (ExoTrackSelection) trackSelections.get(i);
if (track == null) {
continue;
}
@@ -323,7 +343,7 @@ public class ExoPlayerWrapper implements IPlayer {
this.audioSeekCompleteListener = audioSeekCompleteListener;
}
- void setOnErrorListener(MediaPlayer.OnErrorListener audioErrorListener) {
+ void setOnErrorListener(Consumer<String> audioErrorListener) {
this.audioErrorListener = audioErrorListener;
}
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 f74e3b9ad..34fc7d699 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
@@ -14,7 +14,12 @@ import android.view.SurfaceHolder;
import androidx.media.AudioAttributesCompat;
import androidx.media.AudioFocusRequestCompat;
import androidx.media.AudioManagerCompat;
-import de.danoeh.antennapod.core.storage.DBReader;
+import de.danoeh.antennapod.event.PlayerErrorEvent;
+import de.danoeh.antennapod.event.playback.BufferUpdateEvent;
+import de.danoeh.antennapod.event.playback.SpeedChangedEvent;
+import de.danoeh.antennapod.core.util.playback.MediaPlayerError;
+import de.danoeh.antennapod.playback.base.PlaybackServiceMediaPlayer;
+import de.danoeh.antennapod.playback.base.PlayerStatus;
import org.antennapod.audio.MediaPlayer;
import java.io.File;
@@ -35,12 +40,13 @@ import de.danoeh.antennapod.model.playback.MediaType;
import de.danoeh.antennapod.model.feed.VolumeAdaptionSetting;
import de.danoeh.antennapod.core.feed.util.PlaybackSpeedUtils;
import de.danoeh.antennapod.core.preferences.UserPreferences;
-import de.danoeh.antennapod.core.util.RewindAfterPauseUtils;
+import de.danoeh.antennapod.playback.base.RewindAfterPauseUtils;
import de.danoeh.antennapod.core.util.playback.AudioPlayer;
import de.danoeh.antennapod.core.util.playback.IPlayer;
import de.danoeh.antennapod.model.playback.Playable;
import de.danoeh.antennapod.core.util.playback.PlaybackServiceStarter;
import de.danoeh.antennapod.core.util.playback.VideoPlayer;
+import org.greenrobot.eventbus.EventBus;
/**
* Manages the MediaPlayer object of the PlaybackService.
@@ -68,6 +74,7 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer {
private final PlayerLock playerLock;
private final PlayerExecutor executor;
private boolean useCallerThread = true;
+ private boolean isShutDown = false;
private CountDownLatch seekLatch;
@@ -142,7 +149,7 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer {
}
public LocalPSMP(@NonNull Context context,
- @NonNull PSMPCallback callback) {
+ @NonNull PlaybackServiceMediaPlayer.PSMPCallback callback) {
super(context, callback);
this.audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
this.playerLock = new PlayerLock();
@@ -259,9 +266,7 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer {
LocalPSMP.this.startWhenPrepared.set(startWhenPrepared);
setPlayerStatus(PlayerStatus.INITIALIZING, media);
try {
- if (media instanceof FeedMedia && ((FeedMedia) media).getItem() == null) {
- ((FeedMedia) media).setItem(DBReader.getFeedItem(((FeedMedia) media).getItemId()));
- }
+ callback.ensureMediaInfoLoaded(media);
callback.onMediaChanged(false);
setPlaybackParams(PlaybackSpeedUtils.getCurrentPlaybackSpeed(media), UserPreferences.isSkipSilence());
if (stream) {
@@ -294,6 +299,7 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer {
} catch (IOException | IllegalStateException e) {
e.printStackTrace();
setPlayerStatus(PlayerStatus.ERROR, null);
+ EventBus.getDefault().postSticky(new PlayerErrorEvent(e.getLocalizedMessage()));
}
}
@@ -402,6 +408,7 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer {
} catch (IOException e) {
e.printStackTrace();
setPlayerStatus(PlayerStatus.ERROR, null);
+ EventBus.getDefault().postSticky(new PlayerErrorEvent(e.getLocalizedMessage()));
}
}
playerLock.unlock();
@@ -611,7 +618,7 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer {
private void setSpeedSyncAndSkipSilence(float speed, boolean skipSilence) {
playerLock.lock();
Log.d(TAG, "Playback speed was set to " + speed);
- callback.playbackSpeedChanged(speed);
+ EventBus.getDefault().post(new SpeedChangedEvent(speed));
mediaPlayer.setPlaybackParams(speed, skipSilence);
playerLock.unlock();
}
@@ -712,32 +719,22 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer {
*/
@Override
public void shutdown() {
- executor.shutdown();
if (mediaPlayer != null) {
try {
- removeMediaPlayerErrorListener();
+ clearMediaPlayerListeners();
if (mediaPlayer.isPlaying()) {
mediaPlayer.stop();
}
} catch (Exception ignore) { }
mediaPlayer.release();
+ mediaPlayer = null;
}
+ isShutDown = true;
+ executor.shutdown();
+ abandonAudioFocus();
releaseWifiLockIfNecessary();
}
- private void removeMediaPlayerErrorListener() {
- if (mediaPlayer instanceof VideoPlayer) {
- VideoPlayer vp = (VideoPlayer) mediaPlayer;
- vp.setOnErrorListener((mp, what, extra) -> true);
- } else if (mediaPlayer instanceof AudioPlayer) {
- AudioPlayer ap = (AudioPlayer) mediaPlayer;
- ap.setOnErrorListener((mediaPlayer, i, i1) -> true);
- } else if (mediaPlayer instanceof ExoPlayerWrapper) {
- ExoPlayerWrapper ap = (ExoPlayerWrapper) mediaPlayer;
- ap.setOnErrorListener((mediaPlayer, i, i1) -> true);
- }
- }
-
/**
* 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.
@@ -857,10 +854,14 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer {
@Override
public void onAudioFocusChange(final int focusChange) {
+ if (isShutDown) {
+ return;
+ }
if (!PlaybackService.isRunning) {
abandonAudioFocus();
Log.d(TAG, "onAudioFocusChange: PlaybackService is no longer running");
if (focusChange == AudioManager.AUDIOFOCUS_GAIN && pausedBecauseOfTransientAudiofocusLoss) {
+ pausedBecauseOfTransientAudiofocusLoss = false;
new PlaybackServiceStarter(context, getPlayable())
.startWhenPrepared(true)
.streamIfLastWasStream()
@@ -1004,9 +1005,9 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer {
return stream;
}
- private IPlayer setMediaPlayerListeners(IPlayer mp) {
+ private void setMediaPlayerListeners(IPlayer mp) {
if (mp == null || media == null) {
- return mp;
+ return;
}
if (mp instanceof VideoPlayer) {
if (media.getMediaType() != MediaType.VIDEO) {
@@ -1033,12 +1034,36 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer {
ap.setOnCompletionListener(audioCompletionListener);
ap.setOnSeekCompleteListener(audioSeekCompleteListener);
ap.setOnBufferingUpdateListener(audioBufferingUpdateListener);
- ap.setOnErrorListener(audioErrorListener);
+ ap.setOnErrorListener(message -> EventBus.getDefault().postSticky(new PlayerErrorEvent(message)));
ap.setOnInfoListener(audioInfoListener);
} else {
Log.w(TAG, "Unknown media player: " + mp);
}
- return mp;
+ }
+
+ private void clearMediaPlayerListeners() {
+ if (mediaPlayer instanceof VideoPlayer) {
+ VideoPlayer vp = (VideoPlayer) mediaPlayer;
+ vp.setOnCompletionListener(x -> { });
+ vp.setOnSeekCompleteListener(x -> { });
+ vp.setOnErrorListener((mediaPlayer, i, i1) -> false);
+ vp.setOnBufferingUpdateListener((mediaPlayer, i) -> { });
+ vp.setOnInfoListener((mediaPlayer, i, i1) -> false);
+ } else if (mediaPlayer instanceof AudioPlayer) {
+ AudioPlayer ap = (AudioPlayer) mediaPlayer;
+ ap.setOnCompletionListener(x -> { });
+ ap.setOnSeekCompleteListener(x -> { });
+ ap.setOnErrorListener((x, y, z) -> false);
+ ap.setOnBufferingUpdateListener((arg0, percent) -> { });
+ ap.setOnInfoListener((arg0, what, extra) -> false);
+ } else if (mediaPlayer instanceof ExoPlayerWrapper) {
+ ExoPlayerWrapper ap = (ExoPlayerWrapper) mediaPlayer;
+ ap.setOnCompletionListener(x -> { });
+ ap.setOnSeekCompleteListener(x -> { });
+ ap.setOnBufferingUpdateListener((arg0, percent) -> { });
+ ap.setOnErrorListener(x -> { });
+ ap.setOnInfoListener((arg0, what, extra) -> false);
+ }
}
private final MediaPlayer.OnCompletionListener audioCompletionListener =
@@ -1052,14 +1077,10 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer {
}
private final MediaPlayer.OnBufferingUpdateListener audioBufferingUpdateListener =
- (mp, percent) -> genericOnBufferingUpdate(percent);
+ (mp, percent) -> EventBus.getDefault().post(BufferUpdateEvent.progressUpdate(0.01f * percent));
private final android.media.MediaPlayer.OnBufferingUpdateListener videoBufferingUpdateListener =
- (mp, percent) -> genericOnBufferingUpdate(percent);
-
- private void genericOnBufferingUpdate(int percent) {
- callback.onBufferingUpdate(percent);
- }
+ (mp, percent) -> EventBus.getDefault().post(BufferUpdateEvent.progressUpdate(0.01f * percent));
private final MediaPlayer.OnInfoListener audioInfoListener =
(mp, what, extra) -> genericInfoListener(what);
@@ -1068,7 +1089,16 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer {
(mp, what, extra) -> genericInfoListener(what);
private boolean genericInfoListener(int what) {
- return callback.onMediaPlayerInfo(what, 0);
+ switch (what) {
+ case android.media.MediaPlayer.MEDIA_INFO_BUFFERING_START:
+ EventBus.getDefault().post(BufferUpdateEvent.started());
+ return true;
+ case android.media.MediaPlayer.MEDIA_INFO_BUFFERING_END:
+ EventBus.getDefault().post(BufferUpdateEvent.ended());
+ return true;
+ default:
+ return true;
+ }
}
private final MediaPlayer.OnErrorListener audioErrorListener =
@@ -1084,7 +1114,8 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer {
private final android.media.MediaPlayer.OnErrorListener videoErrorListener = this::genericOnError;
private boolean genericOnError(Object inObj, int what, int extra) {
- return callback.onMediaPlayerError(inObj, what, extra);
+ EventBus.getDefault().postSticky(new PlayerErrorEvent(MediaPlayerError.getErrorString(context, what)));
+ return true;
}
private final MediaPlayer.OnSeekCompleteListener audioSeekCompleteListener =
@@ -1116,4 +1147,9 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer {
executor.submit(r);
}
}
+
+ @Override
+ public boolean isCasting() {
+ return false;
+ }
}
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 8ba5215df..805956094 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
@@ -1,5 +1,7 @@
package de.danoeh.antennapod.core.service.playback;
+import static de.danoeh.antennapod.model.feed.FeedPreferences.SPEED_USE_GLOBAL;
+
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
@@ -14,20 +16,13 @@ import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.media.AudioManager;
-import android.media.MediaPlayer;
import android.net.Uri;
import android.os.Binder;
import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
import android.os.Vibrator;
-import androidx.preference.PreferenceManager;
-import androidx.annotation.NonNull;
-import androidx.annotation.StringRes;
-import androidx.core.app.NotificationCompat;
-import androidx.core.app.NotificationManagerCompat;
import android.support.v4.media.MediaBrowserCompat;
-import androidx.media.MediaBrowserServiceCompat;
import android.support.v4.media.MediaDescriptionCompat;
import android.support.v4.media.MediaMetadataCompat;
import android.support.v4.media.session.MediaSessionCompat;
@@ -40,24 +35,38 @@ import android.view.SurfaceHolder;
import android.webkit.URLUtil;
import android.widget.Toast;
+import androidx.annotation.DrawableRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.StringRes;
+import androidx.core.app.NotificationCompat;
+import androidx.core.app.NotificationManagerCompat;
+import androidx.media.MediaBrowserServiceCompat;
+import androidx.preference.PreferenceManager;
+
+import de.danoeh.antennapod.event.playback.BufferUpdateEvent;
+import de.danoeh.antennapod.event.playback.PlaybackServiceEvent;
+import de.danoeh.antennapod.event.PlayerErrorEvent;
+import de.danoeh.antennapod.event.playback.SleepTimerUpdatedEvent;
+import de.danoeh.antennapod.playback.base.PlaybackServiceMediaPlayer;
+import de.danoeh.antennapod.playback.base.PlayerStatus;
+import de.danoeh.antennapod.playback.cast.CastPsmp;
+import de.danoeh.antennapod.playback.cast.CastStateListener;
+import org.greenrobot.eventbus.EventBus;
+import org.greenrobot.eventbus.Subscribe;
+import org.greenrobot.eventbus.ThreadMode;
+
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
import de.danoeh.antennapod.core.R;
-import de.danoeh.antennapod.core.event.MessageEvent;
-import de.danoeh.antennapod.core.event.PlaybackPositionEvent;
-import de.danoeh.antennapod.core.event.ServiceEvent;
-import de.danoeh.antennapod.core.event.settings.SkipIntroEndingChangedEvent;
-import de.danoeh.antennapod.core.event.settings.SpeedPresetChangedEvent;
-import de.danoeh.antennapod.core.event.settings.VolumeAdaptionChangedEvent;
-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.model.feed.FeedPreferences;
-import de.danoeh.antennapod.model.playback.MediaType;
+import de.danoeh.antennapod.event.MessageEvent;
+import de.danoeh.antennapod.event.playback.PlaybackPositionEvent;
+import de.danoeh.antennapod.event.settings.SkipIntroEndingChangedEvent;
+import de.danoeh.antennapod.event.settings.SpeedPresetChangedEvent;
+import de.danoeh.antennapod.event.settings.VolumeAdaptionChangedEvent;
import de.danoeh.antennapod.core.preferences.PlaybackPreferences;
import de.danoeh.antennapod.core.preferences.SleepTimerPreferences;
import de.danoeh.antennapod.core.preferences.UserPreferences;
@@ -66,15 +75,21 @@ import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.storage.DBTasks;
import de.danoeh.antennapod.core.storage.DBWriter;
import de.danoeh.antennapod.core.storage.FeedSearcher;
-import de.danoeh.antennapod.core.sync.SyncService;
+import de.danoeh.antennapod.core.sync.queue.SynchronizationQueueSink;
import de.danoeh.antennapod.core.util.FeedItemUtil;
import de.danoeh.antennapod.core.util.IntentUtils;
import de.danoeh.antennapod.core.util.NetworkUtils;
import de.danoeh.antennapod.core.util.gui.NotificationUtils;
-import de.danoeh.antennapod.model.playback.Playable;
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;
+import de.danoeh.antennapod.model.feed.FeedPreferences;
+import de.danoeh.antennapod.model.playback.MediaType;
+import de.danoeh.antennapod.model.playback.Playable;
import de.danoeh.antennapod.ui.appstartintent.MainActivityStarter;
import de.danoeh.antennapod.ui.appstartintent.VideoPlayerActivityStarter;
import io.reactivex.Completable;
@@ -83,11 +98,6 @@ import io.reactivex.Single;
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 static de.danoeh.antennapod.model.feed.FeedPreferences.SPEED_USE_GLOBAL;
/**
* Controls the MediaPlayer that plays a FeedMedia-file
@@ -98,24 +108,10 @@ public class PlaybackService extends MediaBrowserServiceCompat {
*/
private static final String TAG = "PlaybackService";
- /**
- * Parcelable of type Playable.
- */
public static final String EXTRA_PLAYABLE = "PlaybackService.PlayableExtra";
- /**
- * True if cast session should disconnect.
- */
- public static final String EXTRA_CAST_DISCONNECT = "extra.de.danoeh.antennapod.core.service.castDisconnect";
- /**
- * True if media should be streamed.
- */
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";
- /**
- * True if playback should be started immediately after media has been
- * prepared.
- */
public static final String EXTRA_START_WHEN_PREPARED = "extra.de.danoeh.antennapod.core.service.startWhenPrepared";
public static final String EXTRA_PREPARE_IMMEDIATELY = "extra.de.danoeh.antennapod.core.service.prepareImmediately";
@@ -159,30 +155,22 @@ public class PlaybackService extends MediaBrowserServiceCompat {
public static final int EXTRA_CODE_VIDEO = 2;
public static final int EXTRA_CODE_CAST = 3;
- public static final int NOTIFICATION_TYPE_ERROR = 0;
- public static final int NOTIFICATION_TYPE_BUFFER_UPDATE = 2;
-
/**
* Receivers of this intent should update their information about the curently playing media
*/
public static final int NOTIFICATION_TYPE_RELOAD = 3;
+
/**
- * The state of the sleeptimer changed.
+ * Set a max number of episodes to load for Android Auto, otherwise there could be performance issues
*/
- public static final int NOTIFICATION_TYPE_SLEEPTIMER_UPDATE = 4;
- public static final int NOTIFICATION_TYPE_BUFFER_START = 5;
- public static final int NOTIFICATION_TYPE_BUFFER_END = 6;
+ public static final int MAX_ANDROID_AUTO_EPISODES_PER_FEED = 100;
+
/**
* No more episodes are going to be played.
*/
public static final int NOTIFICATION_TYPE_PLAYBACK_END = 7;
/**
- * Playback speed has changed
- */
- public static final int NOTIFICATION_TYPE_PLAYBACK_SPEED_CHANGE = 8;
-
- /**
* Returned by getPositionSafe() or getDurationSafe() if the playbackService
* is in an invalid state.
*/
@@ -203,10 +191,10 @@ public class PlaybackService extends MediaBrowserServiceCompat {
private PlaybackServiceMediaPlayer mediaPlayer;
private PlaybackServiceTaskManager taskManager;
- private PlaybackServiceFlavorHelper flavorHelper;
private PlaybackServiceStateManager stateManager;
private Disposable positionEventTimer;
private PlaybackServiceNotificationBuilder notificationBuilder;
+ private CastStateListener castStateListener;
private String autoSkippedFeedMediaId = null;
@@ -283,14 +271,14 @@ public class PlaybackService extends MediaBrowserServiceCompat {
EventBus.getDefault().register(this);
taskManager = new PlaybackServiceTaskManager(this, taskManagerCallback);
- flavorHelper = new PlaybackServiceFlavorHelper(PlaybackService.this, flavorHelperCallback);
PreferenceManager.getDefaultSharedPreferences(this)
.registerOnSharedPreferenceChangeListener(prefListener);
ComponentName eventReceiver = new ComponentName(getApplicationContext(), MediaButtonReceiver.class);
Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON);
mediaButtonIntent.setComponent(eventReceiver);
- PendingIntent buttonReceiverIntent = PendingIntent.getBroadcast(this, 0, mediaButtonIntent, PendingIntent.FLAG_UPDATE_CURRENT);
+ PendingIntent buttonReceiverIntent = PendingIntent.getBroadcast(this, 0, mediaButtonIntent,
+ PendingIntent.FLAG_UPDATE_CURRENT | (Build.VERSION.SDK_INT >= 31 ? PendingIntent.FLAG_MUTABLE : 0));
mediaSession = new MediaSessionCompat(getApplicationContext(), TAG, eventReceiver, buttonReceiverIntent);
setSessionToken(mediaSession.getSessionToken());
@@ -307,10 +295,34 @@ public class PlaybackService extends MediaBrowserServiceCompat {
npe.printStackTrace();
}
- flavorHelper.initializeMediaPlayer(PlaybackService.this);
+ recreateMediaPlayer();
mediaSession.setActive(true);
+ castStateListener = new CastStateListener(this) {
+ @Override
+ public void onSessionStartedOrEnded() {
+ recreateMediaPlayer();
+ }
+ };
+ EventBus.getDefault().post(new PlaybackServiceEvent(PlaybackServiceEvent.Action.SERVICE_STARTED));
+ }
- EventBus.getDefault().post(new ServiceEvent(ServiceEvent.Action.SERVICE_STARTED));
+ void recreateMediaPlayer() {
+ Playable media = null;
+ boolean wasPlaying = false;
+ if (mediaPlayer != null) {
+ media = mediaPlayer.getPlayable();
+ wasPlaying = mediaPlayer.getPlayerStatus() == PlayerStatus.PLAYING;
+ mediaPlayer.pause(true, false);
+ mediaPlayer.shutdown();
+ }
+ mediaPlayer = CastPsmp.getInstanceIfConnected(this, mediaPlayerCallback);
+ if (mediaPlayer == null) {
+ mediaPlayer = new LocalPSMP(this, mediaPlayerCallback); // Cast not supported or not connected
+ }
+ if (media != null) {
+ mediaPlayer.playMediaObject(media, !media.localFileAvailable(), wasPlaying, true);
+ }
+ isCasting = mediaPlayer.isCasting();
}
@Override
@@ -326,6 +338,7 @@ public class PlaybackService extends MediaBrowserServiceCompat {
stateManager.stopForeground(!UserPreferences.isPersistNotify());
isRunning = false;
currentMediaType = MediaType.UNKNOWN;
+ castStateListener.destroy();
cancelPositionObserver();
PreferenceManager.getDefaultSharedPreferences(this).unregisterOnSharedPreferenceChangeListener(prefListener);
@@ -339,8 +352,6 @@ public class PlaybackService extends MediaBrowserServiceCompat {
unregisterReceiver(audioBecomingNoisy);
unregisterReceiver(skipCurrentEpisodeReceiver);
unregisterReceiver(pausePlayCurrentEpisodeReceiver);
- flavorHelper.removeCastConsumer();
- flavorHelper.unregisterWifiBroadcastReceiver();
mediaPlayer.shutdown();
taskManager.shutdown();
}
@@ -370,30 +381,22 @@ public class PlaybackService extends MediaBrowserServiceCompat {
.subscribe(queueItems -> mediaSession.setQueue(queueItems), Throwable::printStackTrace);
}
- private MediaBrowserCompat.MediaItem createBrowsableMediaItemForRoot() {
+ private MediaBrowserCompat.MediaItem createBrowsableMediaItem(
+ @StringRes int title, @DrawableRes int icon, int numEpisodes) {
Uri uri = new Uri.Builder()
.scheme(ContentResolver.SCHEME_ANDROID_RESOURCE)
- .authority(getResources().getResourcePackageName(R.drawable.ic_playlist_black))
- .appendPath(getResources().getResourceTypeName(R.drawable.ic_playlist_black))
- .appendPath(getResources().getResourceEntryName(R.drawable.ic_playlist_black))
+ .authority(getResources().getResourcePackageName(icon))
+ .appendPath(getResources().getResourceTypeName(icon))
+ .appendPath(getResources().getResourceEntryName(icon))
.build();
- String subtitle = "";
- try {
- int count = taskManager.getQueue().size();
- subtitle = getResources().getQuantityString(R.plurals.num_episodes, count, count);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
-
MediaDescriptionCompat description = new MediaDescriptionCompat.Builder()
.setIconUri(uri)
- .setMediaId(getResources().getString(R.string.queue_label))
- .setTitle(getResources().getString(R.string.queue_label))
- .setSubtitle(subtitle)
+ .setMediaId(getResources().getString(title))
+ .setTitle(getResources().getString(title))
+ .setSubtitle(getResources().getQuantityString(R.plurals.num_episodes, numEpisodes, numEpisodes))
.build();
- return new MediaBrowserCompat.MediaItem(description,
- MediaBrowserCompat.MediaItem.FLAG_BROWSABLE);
+ return new MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE);
}
private MediaBrowserCompat.MediaItem createBrowsableMediaItemForFeed(Feed feed) {
@@ -425,42 +428,47 @@ public class PlaybackService extends MediaBrowserServiceCompat {
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
- .subscribe(() -> { }, Throwable::printStackTrace);
+ .subscribe(
+ () -> {
+ }, e -> {
+ e.printStackTrace();
+ result.sendResult(null);
+ });
}
- private List<MediaBrowserCompat.MediaItem> loadChildrenSynchronous(@NonNull String parentId) {
+ private List<MediaBrowserCompat.MediaItem> loadChildrenSynchronous(@NonNull String parentId)
+ throws InterruptedException {
List<MediaBrowserCompat.MediaItem> mediaItems = new ArrayList<>();
if (parentId.equals(getResources().getString(R.string.app_name))) {
- // Root List
- try {
- if (!(taskManager.getQueue().isEmpty())) {
- mediaItems.add(createBrowsableMediaItemForRoot());
- }
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
+ mediaItems.add(createBrowsableMediaItem(R.string.queue_label, R.drawable.ic_playlist_black,
+ taskManager.getQueue().size()));
+ mediaItems.add(createBrowsableMediaItem(R.string.downloads_label, R.drawable.ic_download_black,
+ DBReader.getDownloadedItems().size()));
List<Feed> feeds = DBReader.getFeedList();
for (Feed feed : feeds) {
mediaItems.add(createBrowsableMediaItemForFeed(feed));
}
- } else if (parentId.equals(getResources().getString(R.string.queue_label))) {
- // Child List
- try {
- for (FeedItem feedItem : taskManager.getQueue()) {
- FeedMedia media = feedItem.getMedia();
- if (media != null) {
- mediaItems.add(media.getMediaItem());
- }
- }
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
+ return mediaItems;
+ }
+
+ List<FeedItem> feedItems;
+ if (parentId.equals(getResources().getString(R.string.queue_label))) {
+ feedItems = taskManager.getQueue();
+ } else if (parentId.equals(getResources().getString(R.string.downloads_label))) {
+ feedItems = DBReader.getDownloadedItems();
} else if (parentId.startsWith("FeedId:")) {
long feedId = Long.parseLong(parentId.split(":")[1]);
- List<FeedItem> feedItems = DBReader.getFeedItemList(DBReader.getFeed(feedId));
- for (FeedItem feedItem : feedItems) {
- if (feedItem.getMedia() != null && feedItem.getMedia().getMediaItem() != null) {
- mediaItems.add(feedItem.getMedia().getMediaItem());
+ feedItems = DBReader.getFeedItemList(DBReader.getFeed(feedId));
+ } else {
+ Log.e(TAG, "Parent ID not found: " + parentId);
+ return null;
+ }
+ int count = 0;
+ for (FeedItem feedItem : feedItems) {
+ if (feedItem.getMedia() != null && feedItem.getMedia().getMediaItem() != null) {
+ mediaItems.add(feedItem.getMedia().getMediaItem());
+ if (++count >= MAX_ANDROID_AUTO_EPISODES_PER_FEED) {
+ break;
}
}
}
@@ -488,9 +496,8 @@ public class PlaybackService extends MediaBrowserServiceCompat {
final int keycode = intent.getIntExtra(MediaButtonReceiver.EXTRA_KEYCODE, -1);
final boolean hardwareButton = intent.getBooleanExtra(MediaButtonReceiver.EXTRA_HARDWAREBUTTON, false);
- final boolean castDisconnect = intent.getBooleanExtra(EXTRA_CAST_DISCONNECT, false);
Playable playable = intent.getParcelableExtra(EXTRA_PLAYABLE);
- if (keycode == -1 && playable == null && !castDisconnect) {
+ if (keycode == -1 && playable == null) {
Log.e(TAG, "PlaybackService was started with no arguments");
stateManager.stopService();
return Service.START_NOT_STICKY;
@@ -514,7 +521,7 @@ public class PlaybackService extends MediaBrowserServiceCompat {
stateManager.stopService();
return Service.START_NOT_STICKY;
}
- } else if (!flavorHelper.castDisconnect(castDisconnect) && playable != null) {
+ } else {
stateManager.validStartCommandWasReceived();
boolean stream = intent.getBooleanExtra(EXTRA_SHOULD_STREAM, true);
boolean allowStreamThisTime = intent.getBooleanExtra(EXTRA_ALLOW_STREAM_THIS_TIME, false);
@@ -558,9 +565,6 @@ public class PlaybackService extends MediaBrowserServiceCompat {
stateManager.stopService();
});
return Service.START_NOT_STICKY;
- } else {
- Log.d(TAG, "Did not handle intent to PlaybackService: " + intent);
- Log.d(TAG, "Extras: " + intent.getExtras());
}
}
@@ -598,10 +602,12 @@ public class PlaybackService extends MediaBrowserServiceCompat {
PendingIntent pendingIntentAllowThisTime;
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
pendingIntentAllowThisTime = PendingIntent.getForegroundService(this,
- R.id.pending_intent_allow_stream_this_time, intentAllowThisTime, PendingIntent.FLAG_UPDATE_CURRENT);
+ R.id.pending_intent_allow_stream_this_time, intentAllowThisTime,
+ PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
} else {
pendingIntentAllowThisTime = PendingIntent.getService(this,
- R.id.pending_intent_allow_stream_this_time, intentAllowThisTime, PendingIntent.FLAG_UPDATE_CURRENT);
+ R.id.pending_intent_allow_stream_this_time, intentAllowThisTime, PendingIntent.FLAG_UPDATE_CURRENT
+ | (Build.VERSION.SDK_INT >= 23 ? PendingIntent.FLAG_IMMUTABLE : 0));
}
Intent intentAlwaysAllow = new Intent(intentAllowThisTime);
@@ -610,10 +616,12 @@ public class PlaybackService extends MediaBrowserServiceCompat {
PendingIntent pendingIntentAlwaysAllow;
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
pendingIntentAlwaysAllow = PendingIntent.getForegroundService(this,
- R.id.pending_intent_allow_stream_always, intentAlwaysAllow, PendingIntent.FLAG_UPDATE_CURRENT);
+ R.id.pending_intent_allow_stream_always, intentAlwaysAllow,
+ PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
} else {
pendingIntentAlwaysAllow = PendingIntent.getService(this,
- R.id.pending_intent_allow_stream_always, intentAlwaysAllow, PendingIntent.FLAG_UPDATE_CURRENT);
+ R.id.pending_intent_allow_stream_always, intentAlwaysAllow, PendingIntent.FLAG_UPDATE_CURRENT
+ | (Build.VERSION.SDK_INT >= 23 ? PendingIntent.FLAG_IMMUTABLE : 0));
}
NotificationCompat.Builder builder = new NotificationCompat.Builder(this,
@@ -783,26 +791,6 @@ public class PlaybackService extends MediaBrowserServiceCompat {
}
@Override
- public void onSleepTimerAlmostExpired(long timeLeft) {
- final float[] multiplicators = {0.1f, 0.2f, 0.3f, 0.3f, 0.3f, 0.4f, 0.4f, 0.4f, 0.6f, 0.8f};
- float multiplicator = multiplicators[Math.max(0, (int) timeLeft / 1000)];
- Log.d(TAG, "onSleepTimerAlmostExpired: " + multiplicator);
- mediaPlayer.setVolume(multiplicator, multiplicator);
- }
-
- @Override
- public void onSleepTimerExpired() {
- mediaPlayer.pause(true, true);
- mediaPlayer.setVolume(1.0f, 1.0f);
- sendNotificationBroadcast(NOTIFICATION_TYPE_SLEEPTIMER_UPDATE, 0);
- }
-
- @Override
- public void onSleepTimerReset() {
- mediaPlayer.setVolume(1.0f, 1.0f);
- }
-
- @Override
public WidgetUpdater.WidgetState requestWidgetState() {
return new WidgetUpdater.WidgetState(getPlayable(), getStatus(),
getCurrentPosition(), getDuration(), getCurrentPlaybackSpeed(), isCasting());
@@ -834,9 +822,8 @@ public class PlaybackService extends MediaBrowserServiceCompat {
taskManager.startChapterLoader(newInfo.playable);
break;
case PAUSED:
- if ((UserPreferences.isPersistNotify() || isCasting) &&
- android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
- // do not remove notification on pause based on user pref and whether android version supports expanded notifications
+ if (UserPreferences.isPersistNotify() || isCasting) {
+ // do not remove notification on pause based on user pref
// Change [Play] button to [Pause]
updateNotificationAndMediaSession(newInfo.playable);
} else if (!UserPreferences.isPersistNotify() && !isCasting) {
@@ -884,16 +871,6 @@ public class PlaybackService extends MediaBrowserServiceCompat {
}
@Override
- public void playbackSpeedChanged(float s) {
- sendNotificationBroadcast(NOTIFICATION_TYPE_PLAYBACK_SPEED_CHANGE, 0);
- }
-
- @Override
- public void onBufferingUpdate(int percent) {
- sendNotificationBroadcast(NOTIFICATION_TYPE_BUFFER_UPDATE, percent);
- }
-
- @Override
public void onMediaChanged(boolean reloadUI) {
Log.d(TAG, "reloadUI callback reached");
if (reloadUI) {
@@ -903,43 +880,6 @@ public class PlaybackService extends MediaBrowserServiceCompat {
}
@Override
- public boolean onMediaPlayerInfo(int code, @StringRes int resourceId) {
- switch (code) {
- case MediaPlayer.MEDIA_INFO_BUFFERING_START:
- sendNotificationBroadcast(NOTIFICATION_TYPE_BUFFER_START, 0);
- return true;
- case MediaPlayer.MEDIA_INFO_BUFFERING_END:
- sendNotificationBroadcast(NOTIFICATION_TYPE_BUFFER_END, 0);
-
- Playable playable = getPlayable();
- if (getPlayable() instanceof FeedMedia
- && playable.getDuration() <= 0 && mediaPlayer.getDuration() > 0) {
- // Playable is being streamed and does not have a duration specified in the feed
- playable.setDuration(mediaPlayer.getDuration());
- DBWriter.setFeedMedia((FeedMedia) playable);
- updateNotificationAndMediaSession(playable);
- }
-
- return true;
- default:
- return flavorHelper.onMediaPlayerInfo(PlaybackService.this, code, resourceId);
- }
- }
-
- @Override
- public boolean onMediaPlayerError(Object inObj, int what, int extra) {
- final String TAG = "PlaybackSvc.onErrorLtsn";
- Log.w(TAG, "An error has occured: " + what + " " + extra);
- if (mediaPlayer.getPlayerStatus() == PlayerStatus.PLAYING) {
- mediaPlayer.pause(true, false);
- }
- sendNotificationBroadcast(NOTIFICATION_TYPE_ERROR, what);
- PlaybackPreferences.writeNoMediaPlaying();
- stateManager.stopService();
- return true;
- }
-
- @Override
public void onPostPlayback(@NonNull Playable media, boolean ended, boolean skipped,
boolean playingNext) {
PlaybackService.this.onPostPlayback(media, ended, skipped, playingNext);
@@ -966,7 +906,8 @@ public class PlaybackService extends MediaBrowserServiceCompat {
taskManager.cancelWidgetUpdater();
if (playable != null) {
if (playable instanceof FeedMedia) {
- SyncService.enqueueEpisodePlayed(getApplicationContext(), (FeedMedia) playable, false);
+ SynchronizationQueueSink.enqueueEpisodePlayedIfSynchronizationIsActive(getApplicationContext(),
+ (FeedMedia) playable, false);
}
playable.onPlaybackPause(getApplicationContext());
}
@@ -977,12 +918,67 @@ public class PlaybackService extends MediaBrowserServiceCompat {
return PlaybackService.this.getNextInQueue(currentMedia);
}
+ @Nullable
+ @Override
+ public Playable findMedia(@NonNull String url) {
+ FeedItem item = DBReader.getFeedItemByGuidOrEpisodeUrl(null, url);
+ return item != null ? item.getMedia() : null;
+ }
+
@Override
public void onPlaybackEnded(MediaType mediaType, boolean stopPlaying) {
PlaybackService.this.onPlaybackEnded(mediaType, stopPlaying);
}
+
+ @Override
+ public void ensureMediaInfoLoaded(@NonNull Playable media) {
+ if (media instanceof FeedMedia && ((FeedMedia) media).getItem() == null) {
+ ((FeedMedia) media).setItem(DBReader.getFeedItem(((FeedMedia) media).getItemId()));
+ }
+ }
};
+ @Subscribe(threadMode = ThreadMode.MAIN)
+ @SuppressWarnings("unused")
+ public void playerError(PlayerErrorEvent event) {
+ if (mediaPlayer.getPlayerStatus() == PlayerStatus.PLAYING) {
+ mediaPlayer.pause(true, false);
+ }
+ PlaybackPreferences.writeNoMediaPlaying();
+ stateManager.stopService();
+ }
+
+ @Subscribe(threadMode = ThreadMode.MAIN)
+ @SuppressWarnings("unused")
+ public void bufferUpdate(BufferUpdateEvent event) {
+ if (event.hasEnded()) {
+ Playable playable = getPlayable();
+ if (getPlayable() instanceof FeedMedia
+ && playable.getDuration() <= 0 && mediaPlayer.getDuration() > 0) {
+ // Playable is being streamed and does not have a duration specified in the feed
+ playable.setDuration(mediaPlayer.getDuration());
+ DBWriter.setFeedMedia((FeedMedia) playable);
+ updateNotificationAndMediaSession(playable);
+ }
+ }
+ }
+
+ @Subscribe(threadMode = ThreadMode.MAIN)
+ @SuppressWarnings("unused")
+ public void sleepTimerUpdate(SleepTimerUpdatedEvent event) {
+ if (event.isOver()) {
+ mediaPlayer.pause(true, true);
+ mediaPlayer.setVolume(1.0f, 1.0f);
+ } else if (event.getTimeLeft() < PlaybackServiceTaskManager.SleepTimer.NOTIFICATION_THRESHOLD) {
+ final float[] multiplicators = {0.1f, 0.2f, 0.3f, 0.3f, 0.3f, 0.4f, 0.4f, 0.4f, 0.6f, 0.8f};
+ float multiplicator = multiplicators[Math.max(0, (int) event.getTimeLeft() / 1000)];
+ Log.d(TAG, "onSleepTimerAlmostExpired: " + multiplicator);
+ mediaPlayer.setVolume(multiplicator, multiplicator);
+ } else if (event.isCancelled()) {
+ mediaPlayer.setVolume(1.0f, 1.0f);
+ }
+ }
+
private Playable getNextInQueue(final Playable currentMedia) {
if (!(currentMedia instanceof FeedMedia)) {
Log.d(TAG, "getNextInQueue(), but playable not an instance of FeedMedia, so not proceeding");
@@ -1110,10 +1106,12 @@ public class PlaybackService extends MediaBrowserServiceCompat {
}
if (ended || smartMarkAsPlayed) {
- SyncService.enqueueEpisodePlayed(getApplicationContext(), media, true);
+ SynchronizationQueueSink.enqueueEpisodePlayedIfSynchronizationIsActive(
+ getApplicationContext(), media, true);
media.onPlaybackCompleted(getApplicationContext());
} else {
- SyncService.enqueueEpisodePlayed(getApplicationContext(), media, false);
+ SynchronizationQueueSink.enqueueEpisodePlayedIfSynchronizationIsActive(
+ getApplicationContext(), media, false);
media.onPlaybackPause(getApplicationContext());
}
@@ -1146,12 +1144,10 @@ public class PlaybackService extends MediaBrowserServiceCompat {
public void setSleepTimer(long waitingTime) {
Log.d(TAG, "Setting sleep timer to " + waitingTime + " milliseconds");
taskManager.setSleepTimer(waitingTime);
- sendNotificationBroadcast(NOTIFICATION_TYPE_SLEEPTIMER_UPDATE, 0);
}
public void disableSleepTimer() {
taskManager.disableSleepTimer();
- sendNotificationBroadcast(NOTIFICATION_TYPE_SLEEPTIMER_UPDATE, 0);
}
private void sendNotificationBroadcast(int type, int code) {
@@ -1235,7 +1231,8 @@ public class PlaybackService extends MediaBrowserServiceCompat {
| PlaybackStateCompat.ACTION_PAUSE
| PlaybackStateCompat.ACTION_FAST_FORWARD
| PlaybackStateCompat.ACTION_SKIP_TO_NEXT
- | PlaybackStateCompat.ACTION_SEEK_TO;
+ | PlaybackStateCompat.ACTION_SEEK_TO
+ | PlaybackStateCompat.ACTION_SET_PLAYBACK_SPEED;
if (useSkipToPreviousForRewindInLockscreen()) {
// Workaround to fool Android so that Lockscreen will expose a skip-to-previous button,
@@ -1268,15 +1265,15 @@ public class PlaybackService extends MediaBrowserServiceCompat {
// This would give the PIP of videos a play button
capabilities = capabilities | PlaybackStateCompat.ACTION_PLAY;
if (uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_WATCH) {
- flavorHelper.sessionStateAddActionForWear(sessionState,
+ WearMediaSession.sessionStateAddActionForWear(sessionState,
CUSTOM_ACTION_REWIND,
getString(R.string.rewind_label),
android.R.drawable.ic_media_rew);
- flavorHelper.sessionStateAddActionForWear(sessionState,
+ WearMediaSession.sessionStateAddActionForWear(sessionState,
CUSTOM_ACTION_FAST_FORWARD,
getString(R.string.fast_forward_label),
android.R.drawable.ic_media_ff);
- flavorHelper.mediaSessionSetExtraForWear(mediaSession);
+ WearMediaSession.mediaSessionSetExtraForWear(mediaSession);
}
}
@@ -1320,7 +1317,8 @@ public class PlaybackService extends MediaBrowserServiceCompat {
if (stateManager.hasReceivedValidStartCommand()) {
mediaSession.setSessionActivity(PendingIntent.getActivity(this, R.id.pending_intent_player_activity,
- PlaybackService.getPlayerActivityIntent(this), PendingIntent.FLAG_UPDATE_CURRENT));
+ PlaybackService.getPlayerActivityIntent(this), PendingIntent.FLAG_UPDATE_CURRENT
+ | (Build.VERSION.SDK_INT >= 31 ? PendingIntent.FLAG_MUTABLE : 0)));
try {
mediaSession.setMetadata(builder.build());
} catch (OutOfMemoryError e) {
@@ -1357,7 +1355,6 @@ public class PlaybackService extends MediaBrowserServiceCompat {
notificationBuilder.setPlayable(playable);
notificationBuilder.setMediaSessionToken(mediaSession.getSessionToken());
notificationBuilder.setPlayerStatus(playerStatus);
- notificationBuilder.setCasting(isCasting);
notificationBuilder.updatePosition(getCurrentPosition(), getCurrentPlaybackSpeed());
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
@@ -1566,7 +1563,7 @@ public class PlaybackService extends MediaBrowserServiceCompat {
@Override
public void onReceive(Context context, Intent intent) {
if (TextUtils.equals(intent.getAction(), ACTION_SHUTDOWN_PLAYBACK_SERVICE)) {
- EventBus.getDefault().post(new ServiceEvent(ServiceEvent.Action.SERVICE_SHUT_DOWN));
+ EventBus.getDefault().post(new PlaybackServiceEvent(PlaybackServiceEvent.Action.SERVICE_SHUT_DOWN));
stateManager.stopService();
}
}
@@ -1892,6 +1889,12 @@ public class PlaybackService extends MediaBrowserServiceCompat {
}
@Override
+ public void onSetPlaybackSpeed(float speed) {
+ Log.d(TAG, "onSetPlaybackSpeed()");
+ setSpeed(speed);
+ }
+
+ @Override
public boolean onMediaButtonEvent(final Intent mediaButton) {
Log.d(TAG, "onMediaButtonEvent(" + mediaButton + ")");
if (mediaButton != null) {
@@ -1920,96 +1923,6 @@ public class PlaybackService extends MediaBrowserServiceCompat {
(sharedPreferences, key) -> {
if (UserPreferences.PREF_LOCKSCREEN_BACKGROUND.equals(key)) {
updateNotificationAndMediaSession(getPlayable());
- } else {
- flavorHelper.onSharedPreference(key);
}
};
-
- interface FlavorHelperCallback {
- PlaybackServiceMediaPlayer.PSMPCallback getMediaPlayerCallback();
-
- void setMediaPlayer(PlaybackServiceMediaPlayer mediaPlayer);
-
- PlaybackServiceMediaPlayer getMediaPlayer();
-
- void setIsCasting(boolean isCasting);
-
- void sendNotificationBroadcast(int type, int code);
-
- void saveCurrentPosition(boolean fromMediaPlayer, Playable playable, int position);
-
- void setupNotification(boolean connected, PlaybackServiceMediaPlayer.PSMPInfo info);
-
- MediaSessionCompat getMediaSession();
-
- Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter);
-
- void unregisterReceiver(BroadcastReceiver receiver);
- }
-
- private final FlavorHelperCallback flavorHelperCallback = new FlavorHelperCallback() {
- @Override
- public PlaybackServiceMediaPlayer.PSMPCallback getMediaPlayerCallback() {
- return PlaybackService.this.mediaPlayerCallback;
- }
-
- @Override
- public void setMediaPlayer(PlaybackServiceMediaPlayer mediaPlayer) {
- PlaybackService.this.mediaPlayer = mediaPlayer;
- }
-
- @Override
- public PlaybackServiceMediaPlayer getMediaPlayer() {
- return PlaybackService.this.mediaPlayer;
- }
-
- @Override
- public void setIsCasting(boolean isCasting) {
- PlaybackService.isCasting = isCasting;
- stateManager.validStartCommandWasReceived();
- }
-
- @Override
- public void sendNotificationBroadcast(int type, int code) {
- PlaybackService.this.sendNotificationBroadcast(type, code);
- }
-
- @Override
- public void saveCurrentPosition(boolean fromMediaPlayer, Playable playable, int position) {
- PlaybackService.this.saveCurrentPosition(fromMediaPlayer, playable, position);
- }
-
- @Override
- public void setupNotification(boolean connected, PlaybackServiceMediaPlayer.PSMPInfo info) {
- if (connected) {
- PlaybackService.this.updateNotificationAndMediaSession(info.playable);
- } else {
- PlayerStatus status = info.playerStatus;
- if ((status == PlayerStatus.PLAYING ||
- status == PlayerStatus.SEEKING ||
- status == PlayerStatus.PREPARING ||
- UserPreferences.isPersistNotify()) &&
- android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
- PlaybackService.this.updateNotificationAndMediaSession(info.playable);
- } else if (!UserPreferences.isPersistNotify()) {
- stateManager.stopForeground(true);
- }
- }
- }
-
- @Override
- public MediaSessionCompat getMediaSession() {
- return PlaybackService.this.mediaSession;
- }
-
- @Override
- public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
- return PlaybackService.this.registerReceiver(receiver, filter);
- }
-
- @Override
- public void unregisterReceiver(BroadcastReceiver receiver) {
- PlaybackService.this.unregisterReceiver(receiver);
- }
- };
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceNotificationBuilder.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceNotificationBuilder.java
index e7dea192a..c348f5773 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceNotificationBuilder.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceNotificationBuilder.java
@@ -31,17 +31,17 @@ import de.danoeh.antennapod.model.playback.Playable;
import java.util.ArrayList;
import java.util.concurrent.ExecutionException;
+import de.danoeh.antennapod.playback.base.PlayerStatus;
import org.apache.commons.lang3.ArrayUtils;
public class PlaybackServiceNotificationBuilder {
private static final String TAG = "PlaybackSrvNotification";
private static Bitmap defaultIcon = null;
- private Context context;
+ private final Context context;
private Playable playable;
private MediaSessionCompat.Token mediaSessionToken;
private PlayerStatus playerStatus;
- private boolean isCasting;
private Bitmap icon;
private String position;
@@ -140,7 +140,7 @@ public class PlaybackServiceNotificationBuilder {
if (playable != null) {
notification.setContentTitle(playable.getFeedTitle());
notification.setContentText(playable.getEpisodeTitle());
- addActions(notification, mediaSessionToken, playerStatus, isCasting);
+ addActions(notification, mediaSessionToken, playerStatus);
if (icon != null) {
notification.setLargeIcon(icon);
@@ -170,26 +170,15 @@ public class PlaybackServiceNotificationBuilder {
private PendingIntent getPlayerActivityPendingIntent() {
return PendingIntent.getActivity(context, R.id.pending_intent_player_activity,
- PlaybackService.getPlayerActivityIntent(context), PendingIntent.FLAG_UPDATE_CURRENT);
+ PlaybackService.getPlayerActivityIntent(context), PendingIntent.FLAG_UPDATE_CURRENT
+ | (Build.VERSION.SDK_INT >= 23 ? PendingIntent.FLAG_IMMUTABLE : 0));
}
private void addActions(NotificationCompat.Builder notification, MediaSessionCompat.Token mediaSessionToken,
- PlayerStatus playerStatus, boolean isCasting) {
+ PlayerStatus playerStatus) {
ArrayList<Integer> compactActionList = new ArrayList<>();
int numActions = 0; // we start and 0 and then increment by 1 for each call to addAction
-
- if (isCasting) {
- Intent stopCastingIntent = new Intent(context, PlaybackService.class);
- stopCastingIntent.putExtra(PlaybackService.EXTRA_CAST_DISCONNECT, true);
- PendingIntent stopCastingPendingIntent = PendingIntent.getService(context,
- numActions, stopCastingIntent, PendingIntent.FLAG_UPDATE_CURRENT);
- notification.addAction(R.drawable.ic_notification_cast_off,
- context.getString(R.string.cast_disconnect_label),
- stopCastingPendingIntent);
- numActions++;
- }
-
// always let them rewind
PendingIntent rewindButtonPendingIntent = getPendingIntentForMediaAction(
KeyEvent.KEYCODE_MEDIA_REWIND, numActions);
@@ -252,9 +241,11 @@ public class PlaybackServiceNotificationBuilder {
intent.putExtra(MediaButtonReceiver.EXTRA_KEYCODE, keycodeValue);
if (Build.VERSION.SDK_INT >= 26) {
- return PendingIntent.getForegroundService(context, requestCode, intent, PendingIntent.FLAG_UPDATE_CURRENT);
+ return PendingIntent.getForegroundService(context, requestCode, intent,
+ PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
} else {
- return PendingIntent.getService(context, requestCode, intent, PendingIntent.FLAG_UPDATE_CURRENT);
+ return PendingIntent.getService(context, requestCode, intent, PendingIntent.FLAG_UPDATE_CURRENT
+ | (Build.VERSION.SDK_INT >= 23 ? PendingIntent.FLAG_IMMUTABLE : 0));
}
}
@@ -266,10 +257,6 @@ public class PlaybackServiceNotificationBuilder {
this.playerStatus = playerStatus;
}
- public void setCasting(boolean casting) {
- isCasting = casting;
- }
-
public PlayerStatus getPlayerStatus() {
return playerStatus;
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceTaskManager.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceTaskManager.java
index a14605e5b..9ca7b6647 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceTaskManager.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceTaskManager.java
@@ -7,6 +7,7 @@ import android.os.Vibrator;
import androidx.annotation.NonNull;
import android.util.Log;
+import de.danoeh.antennapod.event.playback.SleepTimerUpdatedEvent;
import de.danoeh.antennapod.core.preferences.SleepTimerPreferences;
import de.danoeh.antennapod.core.util.ChapterUtils;
import de.danoeh.antennapod.core.widget.WidgetUpdater;
@@ -22,10 +23,9 @@ import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
-import de.danoeh.antennapod.core.event.FeedItemEvent;
-import de.danoeh.antennapod.core.event.QueueEvent;
+import de.danoeh.antennapod.event.FeedItemEvent;
+import de.danoeh.antennapod.event.QueueEvent;
import de.danoeh.antennapod.model.feed.FeedItem;
-import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.model.playback.Playable;
import io.reactivex.Completable;
@@ -244,6 +244,7 @@ public class PlaybackServiceTaskManager {
}
sleepTimer = new SleepTimer(waitingTime);
sleepTimerFuture = schedExecutor.schedule(sleepTimer, 0, TimeUnit.MILLISECONDS);
+ EventBus.getDefault().post(SleepTimerUpdatedEvent.justEnabled(waitingTime));
}
/**
@@ -349,10 +350,10 @@ public class PlaybackServiceTaskManager {
* Cancels all tasks and shuts down the internal executor service of the PSTM. The object should not be used after
* execution of this method.
*/
- public synchronized void shutdown() {
+ public void shutdown() {
EventBus.getDefault().unregister(this);
cancelAllTasks();
- schedExecutor.shutdown();
+ schedExecutor.shutdownNow();
}
private Runnable useMainThreadIfNecessary(Runnable runnable) {
@@ -377,27 +378,11 @@ public class PlaybackServiceTaskManager {
private final long waitingTime;
private long timeLeft;
private ShakeListener shakeListener;
- private final Handler handler;
public SleepTimer(long waitingTime) {
super();
this.waitingTime = waitingTime;
this.timeLeft = waitingTime;
-
- if (UserPreferences.useExoplayer() && Looper.myLooper() == Looper.getMainLooper()) {
- // Run callbacks in main thread so they can call ExoPlayer methods themselves
- this.handler = new Handler(Looper.getMainLooper());
- } else {
- this.handler = null;
- }
- }
-
- private void postCallback(Runnable r) {
- if (handler == null) {
- r.run();
- } else {
- handler.post(r);
- }
}
@Override
@@ -417,6 +402,7 @@ public class PlaybackServiceTaskManager {
timeLeft -= now - lastTick;
lastTick = now;
+ EventBus.getDefault().post(SleepTimerUpdatedEvent.updated(timeLeft));
if (timeLeft < NOTIFICATION_THRESHOLD) {
Log.d(TAG, "Sleep timer is about to expire");
if (SleepTimerPreferences.vibrate() && !hasVibrated) {
@@ -429,7 +415,6 @@ public class PlaybackServiceTaskManager {
if (shakeListener == null && SleepTimerPreferences.shakeToReset()) {
shakeListener = new ShakeListener(context, this);
}
- postCallback(() -> callback.onSleepTimerAlmostExpired(timeLeft));
}
if (timeLeft <= 0) {
Log.d(TAG, "Sleep timer expired");
@@ -438,11 +423,6 @@ public class PlaybackServiceTaskManager {
shakeListener = null;
}
hasVibrated = false;
- if (!Thread.currentThread().isInterrupted()) {
- postCallback(callback::onSleepTimerExpired);
- } else {
- Log.d(TAG, "Sleep timer interrupted");
- }
}
}
}
@@ -452,10 +432,8 @@ public class PlaybackServiceTaskManager {
}
public void restart() {
- postCallback(() -> {
- setSleepTimer(waitingTime);
- callback.onSleepTimerReset();
- });
+ EventBus.getDefault().post(SleepTimerUpdatedEvent.cancelled());
+ setSleepTimer(waitingTime);
if (shakeListener != null) {
shakeListener.pause();
shakeListener = null;
@@ -467,19 +445,13 @@ public class PlaybackServiceTaskManager {
if (shakeListener != null) {
shakeListener.pause();
}
- postCallback(callback::onSleepTimerReset);
+ EventBus.getDefault().post(SleepTimerUpdatedEvent.cancelled());
}
}
public interface PSTMCallback {
void positionSaverTick();
- void onSleepTimerAlmostExpired(long timeLeft);
-
- void onSleepTimerExpired();
-
- void onSleepTimerReset();
-
WidgetUpdater.WidgetState requestWidgetState();
void onChapterLoaded(Playable media);
diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackVolumeUpdater.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackVolumeUpdater.java
index edb8bc3a9..43837a473 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackVolumeUpdater.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackVolumeUpdater.java
@@ -4,6 +4,8 @@ import de.danoeh.antennapod.model.feed.FeedMedia;
import de.danoeh.antennapod.model.feed.FeedPreferences;
import de.danoeh.antennapod.model.feed.VolumeAdaptionSetting;
import de.danoeh.antennapod.model.playback.Playable;
+import de.danoeh.antennapod.playback.base.PlaybackServiceMediaPlayer;
+import de.danoeh.antennapod.playback.base.PlayerStatus;
class PlaybackVolumeUpdater {
diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlayerStatus.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlayerStatus.java
deleted file mode 100644
index 4f2ae34f8..000000000
--- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlayerStatus.java
+++ /dev/null
@@ -1,33 +0,0 @@
-package de.danoeh.antennapod.core.service.playback;
-
-public enum PlayerStatus {
- INDETERMINATE(0), // player is currently changing its state, listeners should wait until the player has left this state.
- ERROR(-1),
- PREPARING(19),
- PAUSED(30),
- PLAYING(40),
- STOPPED(5),
- PREPARED(20),
- SEEKING(29),
- INITIALIZING(9), // playback service is loading the Playable's metadata
- INITIALIZED(10); // playback service was started, data source of media player was set.
-
- private final int statusValue;
- private static final PlayerStatus[] fromOrdinalLookup;
-
- static {
- fromOrdinalLookup = PlayerStatus.values();
- }
-
- PlayerStatus(int val) {
- 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/core/src/main/java/de/danoeh/antennapod/core/storage/AutomaticDownloadAlgorithm.java b/core/src/main/java/de/danoeh/antennapod/core/storage/AutomaticDownloadAlgorithm.java
index b5202d79c..cf32eb838 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
@@ -35,7 +35,7 @@ public class AutomaticDownloadAlgorithm {
return () -> {
// true if we should auto download based on network status
- boolean networkShouldAutoDl = NetworkUtils.autodownloadNetworkAvailable()
+ boolean networkShouldAutoDl = NetworkUtils.isAutoDownloadAllowed()
&& UserPreferences.isEnableAutodownload();
// true if we should auto download based on power status
@@ -65,7 +65,7 @@ public class AutomaticDownloadAlgorithm {
Iterator<FeedItem> it = candidates.iterator();
while (it.hasNext()) {
FeedItem item = it.next();
- if (!item.isAutoDownloadable() || FeedItemUtil.isPlaying(item.getMedia())
+ if (!item.isAutoDownloadable(System.currentTimeMillis()) || FeedItemUtil.isPlaying(item.getMedia())
|| item.getFeed().isLocalFeed()) {
it.remove();
}
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 49eca1027..f776fe111 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
@@ -575,7 +575,6 @@ public final class DBReader {
@Nullable
private static FeedItem getFeedItemByGuidOrEpisodeUrl(final String guid, final String episodeUrl,
PodDBAdapter adapter) {
- Log.d(TAG, "Loading feeditem with guid " + guid + " or episode url " + episodeUrl);
try (Cursor cursor = adapter.getFeedItemCursor(guid, episodeUrl)) {
if (!cursor.moveToNext()) {
return null;
@@ -633,8 +632,6 @@ public final class DBReader {
* Does NOT load additional attributes like feed or queue state.
*/
public static FeedItem getFeedItemByGuidOrEpisodeUrl(final String guid, final String episodeUrl) {
- Log.d(TAG, "getFeedItem() called with: " + "guid = [" + guid + "], episodeUrl = [" + episodeUrl + "]");
-
PodDBAdapter adapter = PodDBAdapter.getInstance();
adapter.open();
try {
@@ -881,7 +878,7 @@ public final class DBReader {
int numDownloadedItems = adapter.getNumberOfDownloadedEpisodes();
List<NavDrawerData.DrawerItem> items = new ArrayList<>();
- Map<String, NavDrawerData.FolderDrawerItem> folders = new HashMap<>();
+ 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(),
@@ -890,18 +887,18 @@ public final class DBReader {
items.add(drawerItem);
continue;
}
- NavDrawerData.FolderDrawerItem folder;
+ NavDrawerData.TagDrawerItem folder;
if (folders.containsKey(tag)) {
folder = folders.get(tag);
} else {
- folder = new NavDrawerData.FolderDrawerItem(tag);
+ folder = new NavDrawerData.TagDrawerItem(tag);
folders.put(tag, folder);
}
drawerItem.id |= folder.id;
folder.children.add(drawerItem);
}
}
- List<NavDrawerData.FolderDrawerItem> foldersSorted = new ArrayList<>(folders.values());
+ List<NavDrawerData.TagDrawerItem> foldersSorted = new ArrayList<>(folders.values());
Collections.sort(foldersSorted, (o1, o2) -> o1.getTitle().compareToIgnoreCase(o2.getTitle()));
items.addAll(foldersSorted);
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 a0c1e54ad..412914329 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
@@ -1,5 +1,7 @@
package de.danoeh.antennapod.core.storage;
+import static android.content.Context.MODE_PRIVATE;
+
import android.content.Context;
import android.content.SharedPreferences;
import android.database.Cursor;
@@ -9,22 +11,6 @@ import android.util.Log;
import androidx.annotation.VisibleForTesting;
-import de.danoeh.antennapod.core.R;
-import de.danoeh.antennapod.core.event.FeedItemEvent;
-import de.danoeh.antennapod.core.event.FeedListUpdateEvent;
-import de.danoeh.antennapod.core.event.MessageEvent;
-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.feed.LocalFeedUpdater;
-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.core.sync.SyncService;
-import de.danoeh.antennapod.core.util.DownloadError;
-import de.danoeh.antennapod.core.util.LongList;
-import de.danoeh.antennapod.core.util.comparator.FeedItemPubdateComparator;
-import de.danoeh.antennapod.net.sync.model.EpisodeAction;
import org.greenrobot.eventbus.EventBus;
import java.util.ArrayList;
@@ -41,7 +27,23 @@ import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
import java.util.concurrent.atomic.AtomicBoolean;
-import static android.content.Context.MODE_PRIVATE;
+import de.danoeh.antennapod.core.R;
+import de.danoeh.antennapod.event.FeedItemEvent;
+import de.danoeh.antennapod.event.FeedListUpdateEvent;
+import de.danoeh.antennapod.event.MessageEvent;
+import de.danoeh.antennapod.core.feed.LocalFeedUpdater;
+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.core.sync.SyncService;
+import de.danoeh.antennapod.core.sync.queue.SynchronizationQueueSink;
+import de.danoeh.antennapod.core.util.DownloadError;
+import de.danoeh.antennapod.core.util.LongList;
+import de.danoeh.antennapod.core.util.comparator.FeedItemPubdateComparator;
+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.net.sync.model.EpisodeAction;
/**
* Provides methods for doing common tasks that use DBReader and DBWriter.
@@ -462,7 +464,7 @@ public final class DBTasks {
.position(oldItem.getMedia().getDuration() / 1000)
.total(oldItem.getMedia().getDuration() / 1000)
.build();
- SyncService.enqueueEpisodeAction(context, action);
+ SynchronizationQueueSink.enqueueEpisodeActionIfSynchronizationIsActive(context, action);
}
}
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/DBUpgrader.java b/core/src/main/java/de/danoeh/antennapod/core/storage/DBUpgrader.java
index 46ab7502b..4e0a6aeda 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/storage/DBUpgrader.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/storage/DBUpgrader.java
@@ -73,7 +73,7 @@ class DBUpgrader {
}
if (oldVersion <= 9) {
db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS
- + " ADD COLUMN " + PodDBAdapter.KEY_AUTO_DOWNLOAD
+ + " ADD COLUMN " + PodDBAdapter.KEY_AUTO_DOWNLOAD_ENABLED
+ " INTEGER DEFAULT 1");
}
if (oldVersion <= 10) {
@@ -121,10 +121,10 @@ class DBUpgrader {
}
if (oldVersion <= 14) {
db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEED_ITEMS
- + " ADD COLUMN " + PodDBAdapter.KEY_AUTO_DOWNLOAD + " INTEGER");
+ + " ADD COLUMN " + PodDBAdapter.KEY_AUTO_DOWNLOAD_ATTEMPTS + " INTEGER");
db.execSQL("UPDATE " + PodDBAdapter.TABLE_NAME_FEED_ITEMS
- + " SET " + PodDBAdapter.KEY_AUTO_DOWNLOAD + " = "
- + "(SELECT " + PodDBAdapter.KEY_AUTO_DOWNLOAD
+ + " SET " + PodDBAdapter.KEY_AUTO_DOWNLOAD_ATTEMPTS + " = "
+ + "(SELECT " + PodDBAdapter.KEY_AUTO_DOWNLOAD_ENABLED
+ " FROM " + PodDBAdapter.TABLE_NAME_FEEDS
+ " WHERE " + PodDBAdapter.TABLE_NAME_FEEDS + "." + PodDBAdapter.KEY_ID
+ " = " + PodDBAdapter.TABLE_NAME_FEED_ITEMS + "." + PodDBAdapter.KEY_FEED + ")");
@@ -322,6 +322,10 @@ class DBUpgrader {
db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS
+ " ADD COLUMN " + PodDBAdapter.KEY_FEED_TAGS + " TEXT;");
}
+ if (oldVersion < 2050000) {
+ db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS
+ + " ADD COLUMN " + PodDBAdapter.KEY_MINIMAL_DURATION_FILTER + " INTEGER DEFAULT -1");
+ }
}
}
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 34ea5e207..280a2f118 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
@@ -6,9 +6,8 @@ import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.core.app.NotificationManagerCompat;
-import de.danoeh.antennapod.core.sync.SyncService;
-import de.danoeh.antennapod.net.sync.model.EpisodeAction;
import org.greenrobot.eventbus.EventBus;
import java.io.File;
@@ -25,30 +24,31 @@ import java.util.concurrent.TimeUnit;
import de.danoeh.antennapod.core.R;
import de.danoeh.antennapod.core.event.DownloadLogEvent;
-import de.danoeh.antennapod.core.event.FavoritesEvent;
-import de.danoeh.antennapod.core.event.FeedItemEvent;
-import de.danoeh.antennapod.core.event.FeedListUpdateEvent;
-import de.danoeh.antennapod.core.event.MessageEvent;
-import de.danoeh.antennapod.core.event.PlaybackHistoryEvent;
-import de.danoeh.antennapod.core.event.QueueEvent;
-import de.danoeh.antennapod.core.event.UnreadItemsUpdateEvent;
-import de.danoeh.antennapod.model.feed.Feed;
+import de.danoeh.antennapod.event.FavoritesEvent;
+import de.danoeh.antennapod.event.FeedItemEvent;
+import de.danoeh.antennapod.event.FeedListUpdateEvent;
+import de.danoeh.antennapod.event.MessageEvent;
+import de.danoeh.antennapod.event.playback.PlaybackHistoryEvent;
+import de.danoeh.antennapod.event.QueueEvent;
+import de.danoeh.antennapod.event.UnreadItemsUpdateEvent;
import de.danoeh.antennapod.core.feed.FeedEvent;
-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.preferences.GpodnetPreferences;
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.core.service.playback.PlaybackService;
+import de.danoeh.antennapod.core.sync.queue.SynchronizationQueueSink;
import de.danoeh.antennapod.core.util.FeedItemPermutors;
import de.danoeh.antennapod.core.util.IntentUtils;
import de.danoeh.antennapod.core.util.LongList;
import de.danoeh.antennapod.core.util.Permutor;
+import de.danoeh.antennapod.core.util.playback.PlayableUtils;
+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.feed.SortOrder;
import de.danoeh.antennapod.model.playback.Playable;
-import de.danoeh.antennapod.core.util.playback.PlayableUtils;
+import de.danoeh.antennapod.net.sync.model.EpisodeAction;
/**
* Provides methods for writing data to AntennaPod's database.
@@ -129,16 +129,17 @@ public class DBWriter {
if (media.getId() == PlaybackPreferences.getCurrentlyPlayingFeedMediaId()) {
PlaybackPreferences.writeNoMediaPlaying();
IntentUtils.sendLocalBroadcast(context, PlaybackService.ACTION_SHUTDOWN_PLAYBACK_SERVICE);
+
+ NotificationManagerCompat nm = NotificationManagerCompat.from(context);
+ nm.cancel(R.id.notification_playing);
}
// Gpodder: queue delete action for synchronization
- if (GpodnetPreferences.loggedIn()) {
- FeedItem item = media.getItem();
- EpisodeAction action = new EpisodeAction.Builder(item, EpisodeAction.DELETE)
- .currentTimestamp()
- .build();
- SyncService.enqueueEpisodeAction(context, action);
- }
+ FeedItem item = media.getItem();
+ EpisodeAction action = new EpisodeAction.Builder(item, EpisodeAction.DELETE)
+ .currentTimestamp()
+ .build();
+ SynchronizationQueueSink.enqueueEpisodeActionIfSynchronizationIsActive(context, action);
}
EventBus.getDefault().post(FeedItemEvent.deletedMedia(Collections.singletonList(media.getItem())));
return true;
@@ -152,7 +153,6 @@ public class DBWriter {
*/
public static Future<?> deleteFeed(final Context context, final long feedId) {
return dbExec.submit(() -> {
- DownloadRequester requester = DownloadRequester.getInstance();
final Feed feed = DBReader.getFeed(feedId);
if (feed == null) {
return;
@@ -170,7 +170,9 @@ public class DBWriter {
adapter.removeFeed(feed);
adapter.close();
- SyncService.enqueueFeedRemoved(context, feed.getDownload_url());
+ if (!feed.isLocalFeed()) {
+ SynchronizationQueueSink.enqueueFeedRemovedIfSynchronizationIsActive(context, feed.getDownload_url());
+ }
EventBus.getDefault().post(new FeedListUpdateEvent(feed));
});
}
@@ -782,7 +784,9 @@ public class DBWriter {
adapter.close();
for (Feed feed : feeds) {
- SyncService.enqueueFeedAdded(context, feed.getDownload_url());
+ if (!feed.isLocalFeed()) {
+ SynchronizationQueueSink.enqueueFeedAddedIfSynchronizationIsActive(context, feed.getDownload_url());
+ }
}
BackupManager backupManager = new BackupManager(context);
@@ -953,25 +957,6 @@ public class DBWriter {
});
}
- public static Future<?> saveFeedItemAutoDownloadFailed(final FeedItem feedItem) {
- return dbExec.submit(() -> {
- int failedAttempts = feedItem.getFailedAutoDownloadAttempts() + 1;
- long autoDownload;
- if (!feedItem.getAutoDownload() || failedAttempts >= 10) {
- autoDownload = 0; // giving up, disable auto download
- feedItem.setAutoDownload(false);
- } else {
- long now = System.currentTimeMillis();
- autoDownload = (now / 10) * 10 + failedAttempts;
- }
- final PodDBAdapter adapter = PodDBAdapter.getInstance();
- adapter.open();
- adapter.setFeedItemAutoDownload(feedItem, autoDownload);
- adapter.close();
- EventBus.getDefault().post(new UnreadItemsUpdateEvent());
- });
- }
-
/**
* Set filter of the feed
*
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 7ca90d687..1ec58216a 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
@@ -30,7 +30,7 @@ public class NavDrawerData {
public abstract static class DrawerItem {
public enum Type {
- FOLDER, FEED
+ TAG, FEED
}
public final Type type;
@@ -55,14 +55,14 @@ public class NavDrawerData {
public abstract int getCounter();
}
- public static class FolderDrawerItem extends DrawerItem {
+ public static class TagDrawerItem extends DrawerItem {
public final List<DrawerItem> children = new ArrayList<>();
public final String name;
public boolean isOpen;
- public FolderDrawerItem(String name) {
+ public TagDrawerItem(String name) {
// Keep IDs >0 but make room for many feeds
- super(DrawerItem.Type.FOLDER, Math.abs((long) name.hashCode()) << 20);
+ super(DrawerItem.Type.TAG, Math.abs((long) name.hashCode()) << 20);
this.name = name;
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/PodDBAdapter.java b/core/src/main/java/de/danoeh/antennapod/core/storage/PodDBAdapter.java
index 85ce2dc99..b7e221a33 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/storage/PodDBAdapter.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/storage/PodDBAdapter.java
@@ -44,8 +44,6 @@ import de.danoeh.antennapod.model.feed.SortOrder;
import static de.danoeh.antennapod.model.feed.FeedPreferences.SPEED_USE_GLOBAL;
import static de.danoeh.antennapod.model.feed.SortOrder.toCodeString;
-// TODO Remove media column from feeditem table
-
/**
* Implements methods for accessing the database
*/
@@ -53,7 +51,7 @@ public class PodDBAdapter {
private static final String TAG = "PodDBAdapter";
public static final String DATABASE_NAME = "Antennapod.db";
- public static final int VERSION = 2030000;
+ public static final int VERSION = 2050000;
/**
* Maximum number of arguments for IN-operator.
@@ -97,7 +95,8 @@ public class PodDBAdapter {
public static final String KEY_DOWNLOADSTATUS_TITLE = "title";
public static final String KEY_CHAPTER_TYPE = "type";
public static final String KEY_PLAYBACK_COMPLETION_DATE = "playback_completion_date";
- public static final String KEY_AUTO_DOWNLOAD = "auto_download";
+ public static final String KEY_AUTO_DOWNLOAD_ATTEMPTS = "auto_download";
+ public static final String KEY_AUTO_DOWNLOAD_ENABLED = "auto_download"; // Both tables use the same key
public static final String KEY_KEEP_UPDATED = "keep_updated";
public static final String KEY_AUTO_DELETE_ACTION = "auto_delete_action";
public static final String KEY_FEED_VOLUME_ADAPTION = "feed_volume_adaption";
@@ -113,6 +112,7 @@ public class PodDBAdapter {
public static final String KEY_LAST_PLAYED_TIME = "last_played_time";
public static final String KEY_INCLUDE_FILTER = "include_filter";
public static final String KEY_EXCLUDE_FILTER = "exclude_filter";
+ public static final String KEY_MINIMAL_DURATION_FILTER = "minimal_duration_filter";
public static final String KEY_FEED_PLAYBACK_SPEED = "feed_playback_speed";
public static final String KEY_FEED_SKIP_INTRO = "feed_skip_intro";
public static final String KEY_FEED_SKIP_ENDING = "feed_skip_ending";
@@ -140,11 +140,12 @@ public class PodDBAdapter {
+ KEY_DESCRIPTION + " TEXT," + KEY_PAYMENT_LINK + " TEXT,"
+ KEY_LASTUPDATE + " TEXT," + KEY_LANGUAGE + " TEXT," + KEY_AUTHOR
+ " TEXT," + KEY_IMAGE_URL + " TEXT," + KEY_TYPE + " TEXT,"
- + KEY_FEED_IDENTIFIER + " TEXT," + KEY_AUTO_DOWNLOAD + " INTEGER DEFAULT 1,"
+ + KEY_FEED_IDENTIFIER + " TEXT," + KEY_AUTO_DOWNLOAD_ENABLED + " INTEGER DEFAULT 1,"
+ KEY_USERNAME + " TEXT,"
+ KEY_PASSWORD + " TEXT,"
+ KEY_INCLUDE_FILTER + " TEXT DEFAULT '',"
+ KEY_EXCLUDE_FILTER + " TEXT DEFAULT '',"
+ + KEY_MINIMAL_DURATION_FILTER + " INTEGER DEFAULT -1,"
+ KEY_KEEP_UPDATED + " INTEGER DEFAULT 1,"
+ KEY_IS_PAGED + " INTEGER DEFAULT 0,"
+ KEY_NEXT_PAGE_LINK + " TEXT,"
@@ -167,7 +168,7 @@ public class PodDBAdapter {
+ KEY_MEDIA + " INTEGER," + KEY_FEED + " INTEGER,"
+ KEY_HAS_CHAPTERS + " INTEGER," + KEY_ITEM_IDENTIFIER + " TEXT,"
+ KEY_IMAGE_URL + " TEXT,"
- + KEY_AUTO_DOWNLOAD + " INTEGER)";
+ + KEY_AUTO_DOWNLOAD_ATTEMPTS + " INTEGER)";
private static final String CREATE_TABLE_FEED_MEDIA = "CREATE TABLE "
+ TABLE_NAME_FEED_MEDIA + " (" + TABLE_PRIMARY_KEY + KEY_DURATION
@@ -244,7 +245,7 @@ public class PodDBAdapter {
TABLE_NAME_FEEDS + "." + KEY_IMAGE_URL,
TABLE_NAME_FEEDS + "." + KEY_TYPE,
TABLE_NAME_FEEDS + "." + KEY_FEED_IDENTIFIER,
- TABLE_NAME_FEEDS + "." + KEY_AUTO_DOWNLOAD,
+ TABLE_NAME_FEEDS + "." + KEY_AUTO_DOWNLOAD_ENABLED,
TABLE_NAME_FEEDS + "." + KEY_KEEP_UPDATED,
TABLE_NAME_FEEDS + "." + KEY_IS_PAGED,
TABLE_NAME_FEEDS + "." + KEY_NEXT_PAGE_LINK,
@@ -257,6 +258,7 @@ public class PodDBAdapter {
TABLE_NAME_FEEDS + "." + KEY_FEED_VOLUME_ADAPTION,
TABLE_NAME_FEEDS + "." + KEY_INCLUDE_FILTER,
TABLE_NAME_FEEDS + "." + KEY_EXCLUDE_FILTER,
+ TABLE_NAME_FEEDS + "." + KEY_MINIMAL_DURATION_FILTER,
TABLE_NAME_FEEDS + "." + KEY_FEED_PLAYBACK_SPEED,
TABLE_NAME_FEEDS + "." + KEY_FEED_TAGS,
TABLE_NAME_FEEDS + "." + KEY_FEED_SKIP_INTRO,
@@ -292,7 +294,7 @@ 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;
+ + TABLE_NAME_FEED_ITEMS + "." + KEY_AUTO_DOWNLOAD_ATTEMPTS;
private static final String KEYS_FEED_MEDIA =
TABLE_NAME_FEED_MEDIA + "." + KEY_ID + " AS " + SELECT_KEY_MEDIA_ID + ", "
@@ -442,7 +444,7 @@ public class PodDBAdapter {
throw new IllegalArgumentException("Feed ID of preference must not be null");
}
ContentValues values = new ContentValues();
- values.put(KEY_AUTO_DOWNLOAD, prefs.getAutoDownload());
+ values.put(KEY_AUTO_DOWNLOAD_ENABLED, prefs.getAutoDownload());
values.put(KEY_KEEP_UPDATED, prefs.getKeepUpdated());
values.put(KEY_AUTO_DELETE_ACTION, prefs.getAutoDeleteAction().ordinal());
values.put(KEY_FEED_VOLUME_ADAPTION, prefs.getVolumeAdaptionSetting().toInteger());
@@ -450,6 +452,7 @@ public class PodDBAdapter {
values.put(KEY_PASSWORD, prefs.getPassword());
values.put(KEY_INCLUDE_FILTER, prefs.getFilter().getIncludeFilter());
values.put(KEY_EXCLUDE_FILTER, prefs.getFilter().getExcludeFilter());
+ values.put(KEY_MINIMAL_DURATION_FILTER, prefs.getFilter().getMinimalDurationFilter());
values.put(KEY_FEED_PLAYBACK_SPEED, prefs.getFeedPlaybackSpeed());
values.put(KEY_FEED_TAGS, prefs.getTagsAsString());
values.put(KEY_FEED_SKIP_INTRO, prefs.getFeedSkipIntro());
@@ -645,7 +648,7 @@ public class PodDBAdapter {
}
values.put(KEY_HAS_CHAPTERS, item.getChapters() != null || item.hasChapters());
values.put(KEY_ITEM_IDENTIFIER, item.getItemIdentifier());
- values.put(KEY_AUTO_DOWNLOAD, item.getAutoDownload());
+ values.put(KEY_AUTO_DOWNLOAD_ATTEMPTS, item.getAutoDownloadAttemptsAndTime());
values.put(KEY_IMAGE_URL, item.getImageUrl());
if (item.getId() == 0) {
@@ -761,13 +764,6 @@ public class PodDBAdapter {
return status.getId();
}
- public void setFeedItemAutoDownload(FeedItem feedItem, long autoDownload) {
- ContentValues values = new ContentValues();
- values.put(KEY_AUTO_DOWNLOAD, autoDownload);
- db.update(TABLE_NAME_FEED_ITEMS, values, KEY_ID + "=?",
- new String[]{String.valueOf(feedItem.getId())});
- }
-
public void setFavorites(List<FeedItem> favorites) {
ContentValues values = new ContentValues();
try {
@@ -844,43 +840,32 @@ public class PodDBAdapter {
db.delete(TABLE_NAME_QUEUE, null, null);
}
- private void removeFeedMedia(FeedMedia media) {
- // delete download log entries for feed media
- db.delete(TABLE_NAME_DOWNLOAD_LOG, KEY_FEEDFILE + "=? AND " + KEY_FEEDFILETYPE + "=?",
- new String[]{String.valueOf(media.getId()), String.valueOf(FeedMedia.FEEDFILETYPE_FEEDMEDIA)});
-
- db.delete(TABLE_NAME_FEED_MEDIA, KEY_ID + "=?",
- new String[]{String.valueOf(media.getId())});
- }
-
- private void removeChaptersOfItem(FeedItem item) {
- db.delete(TABLE_NAME_SIMPLECHAPTERS, KEY_FEEDITEM + "=?",
- new String[]{String.valueOf(item.getId())});
- }
-
- /**
- * Remove a FeedItem and its FeedMedia entry.
- */
- private void removeFeedItem(FeedItem item) {
- if (item.getMedia() != null) {
- removeFeedMedia(item.getMedia());
- }
- if (item.hasChapters() || item.getChapters() != null) {
- removeChaptersOfItem(item);
- }
- db.delete(TABLE_NAME_FEED_ITEMS, KEY_ID + "=?",
- new String[]{String.valueOf(item.getId())});
- }
-
/**
* Remove the listed items and their FeedMedia entries.
*/
public void removeFeedItems(@NonNull List<FeedItem> items) {
try {
- db.beginTransactionNonExclusive();
+ StringBuilder mediaIds = new StringBuilder();
+ StringBuilder itemIds = new StringBuilder();
for (FeedItem item : items) {
- removeFeedItem(item);
+ if (item.getMedia() != null) {
+ if (mediaIds.length() != 0) {
+ mediaIds.append(",");
+ }
+ mediaIds.append(item.getMedia().getId());
+ }
+ if (itemIds.length() != 0) {
+ itemIds.append(",");
+ }
+ itemIds.append(item.getId());
}
+
+ db.beginTransactionNonExclusive();
+ db.delete(TABLE_NAME_SIMPLECHAPTERS, KEY_FEEDITEM + " IN (" + itemIds + ")", null);
+ db.delete(TABLE_NAME_DOWNLOAD_LOG, KEY_FEEDFILETYPE + "=" + FeedMedia.FEEDFILETYPE_FEEDMEDIA
+ + " AND " + KEY_FEEDFILE + " IN (" + mediaIds + ")", null);
+ db.delete(TABLE_NAME_FEED_MEDIA, KEY_ID + " IN (" + mediaIds + ")", null);
+ db.delete(TABLE_NAME_FEED_ITEMS, KEY_ID + " IN (" + itemIds + ")", null);
db.setTransactionSuccessful();
} catch (SQLException e) {
Log.e(TAG, Log.getStackTraceString(e));
@@ -896,9 +881,7 @@ public class PodDBAdapter {
try {
db.beginTransactionNonExclusive();
if (feed.getItems() != null) {
- for (FeedItem item : feed.getItems()) {
- removeFeedItem(item);
- }
+ removeFeedItems(feed.getItems());
}
// delete download log entries for feed
db.delete(TABLE_NAME_DOWNLOAD_LOG, KEY_FEEDFILE + "=? AND " + KEY_FEEDFILETYPE + "=?",
@@ -1123,7 +1106,6 @@ public class PodDBAdapter {
+ " INNER JOIN " + TABLE_NAME_FEEDS
+ " ON " + TABLE_NAME_FEED_ITEMS + "." + KEY_FEED + "=" + TABLE_NAME_FEEDS + "." + KEY_ID
+ " WHERE " + whereClauseCondition;
- Log.d(TAG, "SQL: " + query);
return db.rawQuery(query, null);
}
@@ -1362,25 +1344,7 @@ public class PodDBAdapter {
}
/**
- * Select number of items, new items, the date of the latest episode and the number of episodes in progress. The result
- * is sorted by the title of the feed.
- */
- private static final String FEED_STATISTICS_QUERY = "SELECT Feeds.id, num_items, new_items, latest_episode, in_progress FROM " +
- " Feeds LEFT JOIN " +
- "(SELECT feed,count(*) AS num_items," +
- " COUNT(CASE WHEN read=0 THEN 1 END) AS new_items," +
- " MAX(pubDate) AS latest_episode," +
- " COUNT(CASE WHEN position>0 THEN 1 END) AS in_progress," +
- " COUNT(CASE WHEN downloaded=1 THEN 1 END) AS episodes_downloaded " +
- " FROM FeedItems LEFT JOIN FeedMedia ON FeedItems.id=FeedMedia.feeditem GROUP BY FeedItems.feed)" +
- " ON Feeds.id = feed ORDER BY Feeds.title COLLATE NOCASE ASC;";
-
- public Cursor getFeedStatisticsCursor() {
- return db.rawQuery(FEED_STATISTICS_QUERY, null);
- }
-
- /**
- * Insert raw data to the database. *
+ * Insert raw data to the database.
* Call method only for unit tests.
*/
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/mapper/FeedItemCursorMapper.java b/core/src/main/java/de/danoeh/antennapod/core/storage/mapper/FeedItemCursorMapper.java
index 19695ca95..ca0834339 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/storage/mapper/FeedItemCursorMapper.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/storage/mapper/FeedItemCursorMapper.java
@@ -25,7 +25,7 @@ public abstract class FeedItemCursorMapper {
int indexHasChapters = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_HAS_CHAPTERS);
int indexRead = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_READ);
int indexItemIdentifier = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_ITEM_IDENTIFIER);
- int indexAutoDownload = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_AUTO_DOWNLOAD);
+ int indexAutoDownload = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_AUTO_DOWNLOAD_ATTEMPTS);
int indexImageUrl = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_IMAGE_URL);
long id = cursor.getInt(indexId);
diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/mapper/FeedMediaCursorMapper.java b/core/src/main/java/de/danoeh/antennapod/core/storage/mapper/FeedMediaCursorMapper.java
index 608fce5c4..0dc3dc231 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/storage/mapper/FeedMediaCursorMapper.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/storage/mapper/FeedMediaCursorMapper.java
@@ -36,7 +36,7 @@ public abstract class FeedMediaCursorMapper {
}
Boolean hasEmbeddedPicture;
- switch (cursor.getInt(cursor.getColumnIndex(PodDBAdapter.KEY_HAS_EMBEDDED_PICTURE))) {
+ switch (cursor.getInt(cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_HAS_EMBEDDED_PICTURE))) {
case 1:
hasEmbeddedPicture = Boolean.TRUE;
break;
diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/mapper/FeedPreferencesCursorMapper.java b/core/src/main/java/de/danoeh/antennapod/core/storage/mapper/FeedPreferencesCursorMapper.java
index cab6ea618..f062609b6 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/storage/mapper/FeedPreferencesCursorMapper.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/storage/mapper/FeedPreferencesCursorMapper.java
@@ -21,7 +21,7 @@ public abstract class FeedPreferencesCursorMapper {
@NonNull
public static FeedPreferences convert(@NonNull Cursor cursor) {
int indexId = cursor.getColumnIndex(PodDBAdapter.KEY_ID);
- int indexAutoDownload = cursor.getColumnIndex(PodDBAdapter.KEY_AUTO_DOWNLOAD);
+ int indexAutoDownload = cursor.getColumnIndex(PodDBAdapter.KEY_AUTO_DOWNLOAD_ENABLED);
int indexAutoRefresh = cursor.getColumnIndex(PodDBAdapter.KEY_KEEP_UPDATED);
int indexAutoDeleteAction = cursor.getColumnIndex(PodDBAdapter.KEY_AUTO_DELETE_ACTION);
int indexVolumeAdaption = cursor.getColumnIndex(PodDBAdapter.KEY_FEED_VOLUME_ADAPTION);
@@ -29,6 +29,7 @@ public abstract class FeedPreferencesCursorMapper {
int indexPassword = cursor.getColumnIndex(PodDBAdapter.KEY_PASSWORD);
int indexIncludeFilter = cursor.getColumnIndex(PodDBAdapter.KEY_INCLUDE_FILTER);
int indexExcludeFilter = cursor.getColumnIndex(PodDBAdapter.KEY_EXCLUDE_FILTER);
+ int indexMinimalDurationFilter = cursor.getColumnIndex(PodDBAdapter.KEY_MINIMAL_DURATION_FILTER);
int indexFeedPlaybackSpeed = cursor.getColumnIndex(PodDBAdapter.KEY_FEED_PLAYBACK_SPEED);
int indexAutoSkipIntro = cursor.getColumnIndex(PodDBAdapter.KEY_FEED_SKIP_INTRO);
int indexAutoSkipEnding = cursor.getColumnIndex(PodDBAdapter.KEY_FEED_SKIP_ENDING);
@@ -47,6 +48,7 @@ public abstract class FeedPreferencesCursorMapper {
String password = cursor.getString(indexPassword);
String includeFilter = cursor.getString(indexIncludeFilter);
String excludeFilter = cursor.getString(indexExcludeFilter);
+ int minimalDurationFilter = cursor.getInt(indexMinimalDurationFilter);
float feedPlaybackSpeed = cursor.getFloat(indexFeedPlaybackSpeed);
int feedAutoSkipIntro = cursor.getInt(indexAutoSkipIntro);
int feedAutoSkipEnding = cursor.getInt(indexAutoSkipEnding);
@@ -62,7 +64,7 @@ public abstract class FeedPreferencesCursorMapper {
volumeAdaptionSetting,
username,
password,
- new FeedFilter(includeFilter, excludeFilter),
+ new FeedFilter(includeFilter, excludeFilter, minimalDurationFilter),
feedPlaybackSpeed,
feedAutoSkipIntro,
feedAutoSkipEnding,
diff --git a/core/src/main/java/de/danoeh/antennapod/core/sync/EpisodeActionFilter.java b/core/src/main/java/de/danoeh/antennapod/core/sync/EpisodeActionFilter.java
index c74356d98..184f24793 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/sync/EpisodeActionFilter.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/sync/EpisodeActionFilter.java
@@ -22,7 +22,6 @@ public class EpisodeActionFilter {
Map<Pair<String, String>, EpisodeAction> localMostRecentPlayActions =
createUniqueLocalMostRecentPlayActions(queuedEpisodeActions);
for (EpisodeAction remoteAction : remoteActions) {
- Log.d(TAG, "Processing remoteAction: " + remoteAction.toString());
Pair<String, String> key = new Pair<>(remoteAction.getPodcast(), remoteAction.getEpisode());
switch (remoteAction.getAction()) {
case NEW:
diff --git a/core/src/main/java/de/danoeh/antennapod/core/sync/LockingAsyncExecutor.java b/core/src/main/java/de/danoeh/antennapod/core/sync/LockingAsyncExecutor.java
new file mode 100644
index 000000000..e7dbbbd3c
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/sync/LockingAsyncExecutor.java
@@ -0,0 +1,35 @@
+package de.danoeh.antennapod.core.sync;
+
+import java.util.concurrent.locks.ReentrantLock;
+
+import io.reactivex.Completable;
+import io.reactivex.schedulers.Schedulers;
+
+public class LockingAsyncExecutor {
+
+ static final ReentrantLock lock = new ReentrantLock();
+
+ /**
+ * Take the lock and execute runnable (to prevent changes to preferences being lost when enqueueing while sync is
+ * in progress). If the lock is free, the runnable is directly executed in the calling thread to prevent overhead.
+ */
+ public static void executeLockedAsync(Runnable runnable) {
+ if (lock.tryLock()) {
+ try {
+ runnable.run();
+ } finally {
+ lock.unlock();
+ }
+ } else {
+ Completable.fromRunnable(() -> {
+ lock.lock();
+ try {
+ runnable.run();
+ } finally {
+ lock.unlock();
+ }
+ }).subscribeOn(Schedulers.io())
+ .subscribe();
+ }
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/sync/SyncService.java b/core/src/main/java/de/danoeh/antennapod/core/sync/SyncService.java
index 9803a29db..82896382d 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/sync/SyncService.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/sync/SyncService.java
@@ -5,7 +5,7 @@ import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
-import android.content.SharedPreferences;
+import android.os.Build;
import android.util.Log;
import androidx.annotation.NonNull;
@@ -20,12 +20,16 @@ import androidx.work.WorkManager;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
+import org.apache.commons.lang3.StringUtils;
+import org.greenrobot.eventbus.EventBus;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
import de.danoeh.antennapod.core.R;
-import de.danoeh.antennapod.core.event.SyncServiceEvent;
-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.GpodnetPreferences;
+import de.danoeh.antennapod.event.SyncServiceEvent;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.service.download.AntennapodHttpClient;
import de.danoeh.antennapod.core.storage.DBReader;
@@ -33,10 +37,14 @@ import de.danoeh.antennapod.core.storage.DBTasks;
import de.danoeh.antennapod.core.storage.DBWriter;
import de.danoeh.antennapod.core.storage.DownloadRequestException;
import de.danoeh.antennapod.core.storage.DownloadRequester;
+import de.danoeh.antennapod.core.sync.queue.SynchronizationQueueStorage;
import de.danoeh.antennapod.core.util.FeedItemUtil;
import de.danoeh.antennapod.core.util.LongList;
import de.danoeh.antennapod.core.util.URLChecker;
import de.danoeh.antennapod.core.util.gui.NotificationUtils;
+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.net.sync.gpoddernet.GpodnetService;
import de.danoeh.antennapod.net.sync.model.EpisodeAction;
import de.danoeh.antennapod.net.sync.model.EpisodeActionChanges;
@@ -44,162 +52,67 @@ import de.danoeh.antennapod.net.sync.model.ISyncService;
import de.danoeh.antennapod.net.sync.model.SubscriptionChanges;
import de.danoeh.antennapod.net.sync.model.SyncServiceException;
import de.danoeh.antennapod.net.sync.model.UploadChangesResponse;
-import io.reactivex.Completable;
-import io.reactivex.schedulers.Schedulers;
-
-import org.apache.commons.lang3.StringUtils;
-import org.greenrobot.eventbus.EventBus;
-import org.json.JSONArray;
-import org.json.JSONException;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.locks.ReentrantLock;
+import de.danoeh.antennapod.net.sync.nextcloud.NextcloudSyncService;
public class SyncService extends Worker {
- private static final String PREF_NAME = "SyncService";
- private static final String PREF_LAST_SUBSCRIPTION_SYNC_TIMESTAMP = "last_sync_timestamp";
- private static final String PREF_LAST_EPISODE_ACTIONS_SYNC_TIMESTAMP = "last_episode_actions_sync_timestamp";
- private static final String PREF_QUEUED_FEEDS_ADDED = "sync_added";
- private static final String PREF_QUEUED_FEEDS_REMOVED = "sync_removed";
- private static final String PREF_QUEUED_EPISODE_ACTIONS = "sync_queued_episode_actions";
- private static final String PREF_LAST_SYNC_ATTEMPT_TIMESTAMP = "last_sync_attempt_timestamp";
- private static final String PREF_LAST_SYNC_ATTEMPT_SUCCESS = "last_sync_attempt_success";
- private static final String TAG = "SyncService";
+ public static final String TAG = "SyncService";
private static final String WORK_ID_SYNC = "SyncServiceWorkId";
- private static final ReentrantLock lock = new ReentrantLock();
- private ISyncService syncServiceImpl;
+ private static boolean isCurrentlyActive = false;
+ private final SynchronizationQueueStorage synchronizationQueueStorage;
public SyncService(@NonNull Context context, @NonNull WorkerParameters params) {
super(context, params);
+ synchronizationQueueStorage = new SynchronizationQueueStorage(context);
}
@Override
@NonNull
public Result doWork() {
- if (!GpodnetPreferences.loggedIn()) {
+ ISyncService activeSyncProvider = getActiveSyncProvider();
+ if (activeSyncProvider == null) {
return Result.success();
}
- syncServiceImpl = new GpodnetService(AntennapodHttpClient.getHttpClient(),
- GpodnetPreferences.getHosturl(), GpodnetPreferences.getDeviceID(),
- GpodnetPreferences.getUsername(), GpodnetPreferences.getPassword());
- SharedPreferences.Editor prefs = getApplicationContext().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
- .edit();
- prefs.putLong(PREF_LAST_SYNC_ATTEMPT_TIMESTAMP, System.currentTimeMillis()).apply();
+
+ SynchronizationSettings.updateLastSynchronizationAttempt();
+ setCurrentlyActive(true);
try {
- syncServiceImpl.login();
- EventBus.getDefault().postSticky(new SyncServiceEvent(R.string.sync_status_subscriptions));
- syncSubscriptions();
- syncEpisodeActions();
- syncServiceImpl.logout();
+ activeSyncProvider.login();
+ syncSubscriptions(activeSyncProvider);
+ waitForDownloadServiceCompleted();
+ syncEpisodeActions(activeSyncProvider);
+ activeSyncProvider.logout();
clearErrorNotifications();
EventBus.getDefault().postSticky(new SyncServiceEvent(R.string.sync_status_success));
- prefs.putBoolean(PREF_LAST_SYNC_ATTEMPT_SUCCESS, true).apply();
+ SynchronizationSettings.setLastSynchronizationAttemptSuccess(true);
return Result.success();
- } catch (SyncServiceException e) {
+ } catch (Exception e) {
EventBus.getDefault().postSticky(new SyncServiceEvent(R.string.sync_status_error));
- prefs.putBoolean(PREF_LAST_SYNC_ATTEMPT_SUCCESS, false).apply();
+ SynchronizationSettings.setLastSynchronizationAttemptSuccess(false);
Log.e(TAG, Log.getStackTraceString(e));
- if (getRunAttemptCount() % 3 == 2) {
- // Do not spam users with notification and retry before notifying
- updateErrorNotification(e);
- }
- return Result.retry();
- }
- }
-
- public static void clearQueue(Context context) {
- executeLockedAsync(() ->
- context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE).edit()
- .putLong(PREF_LAST_SUBSCRIPTION_SYNC_TIMESTAMP, 0)
- .putLong(PREF_LAST_EPISODE_ACTIONS_SYNC_TIMESTAMP, 0)
- .putLong(PREF_LAST_SYNC_ATTEMPT_TIMESTAMP, 0)
- .putString(PREF_QUEUED_EPISODE_ACTIONS, "[]")
- .putString(PREF_QUEUED_FEEDS_ADDED, "[]")
- .putString(PREF_QUEUED_FEEDS_REMOVED, "[]")
- .apply());
- }
-
- public static void enqueueFeedAdded(Context context, String downloadUrl) {
- if (!GpodnetPreferences.loggedIn()) {
- return;
- }
- executeLockedAsync(() -> {
- try {
- SharedPreferences prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
- String json = prefs.getString(PREF_QUEUED_FEEDS_ADDED, "[]");
- JSONArray queue = new JSONArray(json);
- queue.put(downloadUrl);
- prefs.edit().putString(PREF_QUEUED_FEEDS_ADDED, queue.toString()).apply();
- } catch (JSONException e) {
- e.printStackTrace();
- }
- sync(context);
- });
- }
- public static void enqueueFeedRemoved(Context context, String downloadUrl) {
- if (!GpodnetPreferences.loggedIn()) {
- return;
- }
- executeLockedAsync(() -> {
- try {
- SharedPreferences prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
- String json = prefs.getString(PREF_QUEUED_FEEDS_REMOVED, "[]");
- JSONArray queue = new JSONArray(json);
- queue.put(downloadUrl);
- prefs.edit().putString(PREF_QUEUED_FEEDS_REMOVED, queue.toString()).apply();
- } catch (JSONException e) {
- e.printStackTrace();
+ if (e instanceof SyncServiceException) {
+ if (getRunAttemptCount() % 3 == 2) {
+ // Do not spam users with notification and retry before notifying
+ updateErrorNotification(e);
+ }
+ return Result.retry();
+ } else {
+ updateErrorNotification(e);
+ return Result.failure();
}
- sync(context);
- });
- }
-
- public static void enqueueEpisodeAction(Context context, EpisodeAction action) {
- if (!GpodnetPreferences.loggedIn()) {
- return;
+ } finally {
+ setCurrentlyActive(false);
}
- executeLockedAsync(() -> {
- try {
- SharedPreferences prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
- String json = prefs.getString(PREF_QUEUED_EPISODE_ACTIONS, "[]");
- JSONArray queue = new JSONArray(json);
- queue.put(action.writeToJsonObject());
- prefs.edit().putString(PREF_QUEUED_EPISODE_ACTIONS, queue.toString()).apply();
- } catch (JSONException e) {
- e.printStackTrace();
- }
- sync(context);
- });
}
- public static void enqueueEpisodePlayed(Context context, FeedMedia media, boolean completed) {
- if (!GpodnetPreferences.loggedIn()) {
- return;
- }
- if (media.getItem() == null) {
- return;
- }
- if (media.getStartPosition() < 0 || (!completed && media.getStartPosition() >= media.getPosition())) {
- return;
- }
- EpisodeAction action = new EpisodeAction.Builder(media.getItem(), EpisodeAction.PLAY)
- .currentTimestamp()
- .started(media.getStartPosition() / 1000)
- .position((completed ? media.getDuration() : media.getPosition()) / 1000)
- .total(media.getDuration() / 1000)
- .build();
- SyncService.enqueueEpisodeAction(context, action);
+ private static void setCurrentlyActive(boolean active) {
+ isCurrentlyActive = active;
}
public static void sync(Context context) {
OneTimeWorkRequest workRequest = getWorkRequest().build();
WorkManager.getInstance(context).enqueueUniqueWork(WORK_ID_SYNC, ExistingWorkPolicy.REPLACE, workRequest);
- EventBus.getDefault().postSticky(new SyncServiceEvent(R.string.sync_status_started));
}
public static void syncImmediately(Context context) {
@@ -207,127 +120,27 @@ public class SyncService extends Worker {
.setInitialDelay(0L, TimeUnit.SECONDS)
.build();
WorkManager.getInstance(context).enqueueUniqueWork(WORK_ID_SYNC, ExistingWorkPolicy.REPLACE, workRequest);
- EventBus.getDefault().postSticky(new SyncServiceEvent(R.string.sync_status_started));
}
public static void fullSync(Context context) {
- executeLockedAsync(() -> {
- context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE).edit()
- .putLong(PREF_LAST_SUBSCRIPTION_SYNC_TIMESTAMP, 0)
- .putLong(PREF_LAST_EPISODE_ACTIONS_SYNC_TIMESTAMP, 0)
- .putLong(PREF_LAST_SYNC_ATTEMPT_TIMESTAMP, 0)
- .apply();
-
+ LockingAsyncExecutor.executeLockedAsync(() -> {
+ SynchronizationSettings.resetTimestamps();
OneTimeWorkRequest workRequest = getWorkRequest()
.setInitialDelay(0L, TimeUnit.SECONDS)
.build();
WorkManager.getInstance(context).enqueueUniqueWork(WORK_ID_SYNC, ExistingWorkPolicy.REPLACE, workRequest);
- EventBus.getDefault().postSticky(new SyncServiceEvent(R.string.sync_status_started));
});
}
- private static OneTimeWorkRequest.Builder getWorkRequest() {
- Constraints.Builder constraints = new Constraints.Builder();
- if (UserPreferences.isAllowMobileFeedRefresh()) {
- constraints.setRequiredNetworkType(NetworkType.CONNECTED);
- } else {
- constraints.setRequiredNetworkType(NetworkType.UNMETERED);
- }
-
- return new OneTimeWorkRequest.Builder(SyncService.class)
- .setConstraints(constraints.build())
- .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 10, TimeUnit.MINUTES)
- .setInitialDelay(5L, TimeUnit.SECONDS); // Give it some time, so other actions can be queued
- }
-
- /**
- * Take the lock and execute runnable (to prevent changes to preferences being lost when enqueueing while sync is
- * in progress). If the lock is free, the runnable is directly executed in the calling thread to prevent overhead.
- */
- private static void executeLockedAsync(Runnable runnable) {
- if (lock.tryLock()) {
- try {
- runnable.run();
- } finally {
- lock.unlock();
- }
- } else {
- Completable.fromRunnable(() -> {
- lock.lock();
- try {
- runnable.run();
- } finally {
- lock.unlock();
- }
- }).subscribeOn(Schedulers.io())
- .subscribe();
- }
- }
-
- public static boolean isLastSyncSuccessful(Context context) {
- return context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
- .getBoolean(PREF_LAST_SYNC_ATTEMPT_SUCCESS, false);
- }
-
- public static long getLastSyncAttempt(Context context) {
- return context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
- .getLong(PREF_LAST_SYNC_ATTEMPT_TIMESTAMP, 0);
- }
-
- private List<EpisodeAction> getQueuedEpisodeActions() {
- ArrayList<EpisodeAction> actions = new ArrayList<>();
- try {
- SharedPreferences prefs = getApplicationContext().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
- String json = prefs.getString(PREF_QUEUED_EPISODE_ACTIONS, "[]");
- JSONArray queue = new JSONArray(json);
- for (int i = 0; i < queue.length(); i++) {
- actions.add(EpisodeAction.readFromJsonObject(queue.getJSONObject(i)));
- }
- } catch (JSONException e) {
- e.printStackTrace();
- }
- return actions;
- }
-
- private List<String> getQueuedRemovedFeeds() {
- ArrayList<String> actions = new ArrayList<>();
- try {
- SharedPreferences prefs = getApplicationContext().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
- String json = prefs.getString(PREF_QUEUED_FEEDS_REMOVED, "[]");
- JSONArray queue = new JSONArray(json);
- for (int i = 0; i < queue.length(); i++) {
- actions.add(queue.getString(i));
- }
- } catch (JSONException e) {
- e.printStackTrace();
- }
- return actions;
- }
-
- private List<String> getQueuedAddedFeeds() {
- ArrayList<String> actions = new ArrayList<>();
- try {
- SharedPreferences prefs = getApplicationContext().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
- String json = prefs.getString(PREF_QUEUED_FEEDS_ADDED, "[]");
- JSONArray queue = new JSONArray(json);
- for (int i = 0; i < queue.length(); i++) {
- actions.add(queue.getString(i));
- }
- } catch (JSONException e) {
- e.printStackTrace();
- }
- return actions;
- }
-
- private void syncSubscriptions() throws SyncServiceException {
- final long lastSync = getApplicationContext().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
- .getLong(PREF_LAST_SUBSCRIPTION_SYNC_TIMESTAMP, 0);
+ private void syncSubscriptions(ISyncService syncServiceImpl) throws SyncServiceException {
+ final long lastSync = SynchronizationSettings.getLastSubscriptionSynchronizationTimestamp();
+ EventBus.getDefault().postSticky(new SyncServiceEvent(R.string.sync_status_subscriptions));
final List<String> localSubscriptions = DBReader.getFeedListDownloadUrls();
SubscriptionChanges subscriptionChanges = syncServiceImpl.getSubscriptionChanges(lastSync);
long newTimeStamp = subscriptionChanges.getTimestamp();
- List<String> queuedRemovedFeeds = getQueuedRemovedFeeds();
- List<String> queuedAddedFeeds = getQueuedAddedFeeds();
+ List<String> queuedRemovedFeeds = synchronizationQueueStorage.getQueuedRemovedFeeds();
+ List<String> queuedAddedFeeds = synchronizationQueueStorage.getQueuedAddedFeeds();
Log.d(TAG, "Downloaded subscription changes: " + subscriptionChanges);
for (String downloadUrl : subscriptionChanges.getAdded()) {
@@ -359,26 +172,33 @@ public class SyncService extends Worker {
Log.d(TAG, "Added: " + StringUtils.join(queuedAddedFeeds, ", "));
Log.d(TAG, "Removed: " + StringUtils.join(queuedRemovedFeeds, ", "));
- lock.lock();
+ LockingAsyncExecutor.lock.lock();
try {
UploadChangesResponse uploadResponse = syncServiceImpl
.uploadSubscriptionChanges(queuedAddedFeeds, queuedRemovedFeeds);
- getApplicationContext().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE).edit()
- .putString(PREF_QUEUED_FEEDS_ADDED, "[]").apply();
- getApplicationContext().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE).edit()
- .putString(PREF_QUEUED_FEEDS_REMOVED, "[]").apply();
+ synchronizationQueueStorage.clearFeedQueues();
newTimeStamp = uploadResponse.timestamp;
} finally {
- lock.unlock();
+ LockingAsyncExecutor.lock.unlock();
+ }
+ }
+ SynchronizationSettings.setLastSubscriptionSynchronizationAttemptTimestamp(newTimeStamp);
+ }
+
+ private void waitForDownloadServiceCompleted() {
+ EventBus.getDefault().postSticky(new SyncServiceEvent(R.string.sync_status_wait_for_downloads));
+ try {
+ while (DownloadRequester.getInstance().isDownloadingFeeds()) {
+ //noinspection BusyWait
+ Thread.sleep(1000);
}
+ } catch (InterruptedException e) {
+ e.printStackTrace();
}
- getApplicationContext().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE).edit()
- .putLong(PREF_LAST_SUBSCRIPTION_SYNC_TIMESTAMP, newTimeStamp).apply();
}
- private void syncEpisodeActions() throws SyncServiceException {
- final long lastSync = getApplicationContext().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
- .getLong(PREF_LAST_EPISODE_ACTIONS_SYNC_TIMESTAMP, 0);
+ private void syncEpisodeActions(ISyncService syncServiceImpl) throws SyncServiceException {
+ final long lastSync = SynchronizationSettings.getLastEpisodeActionSynchronizationTimestamp();
EventBus.getDefault().postSticky(new SyncServiceEvent(R.string.sync_status_episodes_download));
EpisodeActionChanges getResponse = syncServiceImpl.getEpisodeActionChanges(lastSync);
long newTimeStamp = getResponse.getTimestamp();
@@ -387,7 +207,7 @@ public class SyncService extends Worker {
// upload local actions
EventBus.getDefault().postSticky(new SyncServiceEvent(R.string.sync_status_episodes_upload));
- List<EpisodeAction> queuedEpisodeActions = getQueuedEpisodeActions();
+ List<EpisodeAction> queuedEpisodeActions = synchronizationQueueStorage.getQueuedEpisodeActions();
if (lastSync == 0) {
EventBus.getDefault().postSticky(new SyncServiceEvent(R.string.sync_status_upload_played));
List<FeedItem> readItems = DBReader.getPlayedItems();
@@ -407,24 +227,21 @@ public class SyncService extends Worker {
}
}
if (queuedEpisodeActions.size() > 0) {
- lock.lock();
+ LockingAsyncExecutor.lock.lock();
try {
Log.d(TAG, "Uploading " + queuedEpisodeActions.size() + " actions: "
+ StringUtils.join(queuedEpisodeActions, ", "));
UploadChangesResponse postResponse = syncServiceImpl.uploadEpisodeActions(queuedEpisodeActions);
newTimeStamp = postResponse.timestamp;
Log.d(TAG, "Upload episode response: " + postResponse);
- getApplicationContext().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE).edit()
- .putString(PREF_QUEUED_EPISODE_ACTIONS, "[]").apply();
+ synchronizationQueueStorage.clearEpisodeActionQueue();
} finally {
- lock.unlock();
+ LockingAsyncExecutor.lock.unlock();
}
}
- getApplicationContext().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE).edit()
- .putLong(PREF_LAST_EPISODE_ACTIONS_SYNC_TIMESTAMP, newTimeStamp).apply();
+ SynchronizationSettings.setLastEpisodeActionSynchronizationAttemptTimestamp(newTimeStamp);
}
-
private synchronized void processEpisodeActions(List<EpisodeAction> remoteActions) {
Log.d(TAG, "Processing " + remoteActions.size() + " actions");
if (remoteActions.size() == 0) {
@@ -432,7 +249,8 @@ public class SyncService extends Worker {
}
Map<Pair<String, String>, EpisodeAction> playActionsToUpdate = EpisodeActionFilter
- .getRemoteActionsOverridingLocalActions(remoteActions, getQueuedEpisodeActions());
+ .getRemoteActionsOverridingLocalActions(remoteActions,
+ synchronizationQueueStorage.getQueuedEpisodeActions());
LongList queueToBeRemoved = new LongList();
List<FeedItem> updatedItems = new ArrayList<>();
for (EpisodeAction action : playActionsToUpdate.values()) {
@@ -442,20 +260,24 @@ public class SyncService extends Worker {
Log.i(TAG, "Unknown feed item: " + action);
continue;
}
+ if (feedItem.getMedia() == null) {
+ Log.i(TAG, "Feed item has no media: " + action);
+ continue;
+ }
if (action.getAction() == EpisodeAction.NEW) {
DBWriter.markItemPlayed(feedItem, FeedItem.UNPLAYED, true);
continue;
}
- Log.d(TAG, "Most recent play action: " + action.toString());
- FeedMedia media = feedItem.getMedia();
- media.setPosition(action.getPosition() * 1000);
+ feedItem.getMedia().setPosition(action.getPosition() * 1000);
if (FeedItemUtil.hasAlmostEnded(feedItem.getMedia())) {
- Log.d(TAG, "Marking as played");
+ Log.d(TAG, "Marking as played: " + action);
feedItem.setPlayed(true);
+ feedItem.getMedia().setPosition(0);
queueToBeRemoved.add(feedItem.getId());
+ } else {
+ Log.d(TAG, "Setting position: " + action);
}
updatedItems.add(feedItem);
-
}
DBWriter.removeQueueItem(getApplicationContext(), false, queueToBeRemoved.toArray());
DBReader.loadAdditionalFeedItemListData(updatedItems);
@@ -469,7 +291,7 @@ public class SyncService extends Worker {
nm.cancel(R.id.notification_gpodnet_sync_autherror);
}
- private void updateErrorNotification(SyncServiceException exception) {
+ private void updateErrorNotification(Exception exception) {
if (!UserPreferences.gpodnetNotificationsEnabled()) {
Log.d(TAG, "Skipping sync error notification because of user setting");
return;
@@ -481,11 +303,13 @@ public class SyncService extends Worker {
Intent intent = getApplicationContext().getPackageManager().getLaunchIntentForPackage(
getApplicationContext().getPackageName());
PendingIntent pendingIntent = PendingIntent.getActivity(getApplicationContext(),
- R.id.pending_intent_sync_error, intent, PendingIntent.FLAG_UPDATE_CURRENT);
+ R.id.pending_intent_sync_error, intent, PendingIntent.FLAG_UPDATE_CURRENT
+ | (Build.VERSION.SDK_INT >= 23 ? PendingIntent.FLAG_IMMUTABLE : 0));
Notification notification = new NotificationCompat.Builder(getApplicationContext(),
NotificationUtils.CHANNEL_ID_SYNC_ERROR)
.setContentTitle(getApplicationContext().getString(R.string.gpodnetsync_error_title))
.setContentText(description)
+ .setStyle(new NotificationCompat.BigTextStyle().bigText(description))
.setContentIntent(pendingIntent)
.setSmallIcon(R.drawable.ic_notification_sync_error)
.setAutoCancel(true)
@@ -495,4 +319,48 @@ public class SyncService extends Worker {
.getSystemService(Context.NOTIFICATION_SERVICE);
nm.notify(R.id.notification_gpodnet_sync_error, notification);
}
+
+ private static OneTimeWorkRequest.Builder getWorkRequest() {
+ Constraints.Builder constraints = new Constraints.Builder();
+ if (UserPreferences.isAllowMobileFeedRefresh()) {
+ constraints.setRequiredNetworkType(NetworkType.CONNECTED);
+ } else {
+ constraints.setRequiredNetworkType(NetworkType.UNMETERED);
+ }
+
+ OneTimeWorkRequest.Builder builder = new OneTimeWorkRequest.Builder(SyncService.class)
+ .setConstraints(constraints.build())
+ .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 10, TimeUnit.MINUTES);
+
+ if (isCurrentlyActive) {
+ // Debounce: don't start sync again immediately after it was finished.
+ builder.setInitialDelay(2L, TimeUnit.MINUTES);
+ } else {
+ // Give it some time, so other possible actions can be queued.
+ builder.setInitialDelay(20L, TimeUnit.SECONDS);
+ EventBus.getDefault().postSticky(new SyncServiceEvent(R.string.sync_status_started));
+ }
+ return builder;
+ }
+
+ private ISyncService getActiveSyncProvider() {
+ String selectedSyncProviderKey = SynchronizationSettings.getSelectedSyncProviderKey();
+ SynchronizationProviderViewData selectedService = SynchronizationProviderViewData
+ .fromIdentifier(selectedSyncProviderKey);
+ if (selectedService == null) {
+ return null;
+ }
+ switch (selectedService) {
+ case GPODDER_NET:
+ return new GpodnetService(AntennapodHttpClient.getHttpClient(),
+ SynchronizationCredentials.getHosturl(), SynchronizationCredentials.getDeviceID(),
+ SynchronizationCredentials.getUsername(), SynchronizationCredentials.getPassword());
+ case NEXTCLOUD_GPODDER:
+ return new NextcloudSyncService(AntennapodHttpClient.getHttpClient(),
+ SynchronizationCredentials.getHosturl(), SynchronizationCredentials.getUsername(),
+ SynchronizationCredentials.getPassword());
+ default:
+ return null;
+ }
+ }
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/sync/SynchronizationCredentials.java b/core/src/main/java/de/danoeh/antennapod/core/sync/SynchronizationCredentials.java
new file mode 100644
index 000000000..e08bc66ad
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/sync/SynchronizationCredentials.java
@@ -0,0 +1,67 @@
+package de.danoeh.antennapod.core.sync;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import de.danoeh.antennapod.core.ClientConfig;
+import de.danoeh.antennapod.core.preferences.UserPreferences;
+import de.danoeh.antennapod.core.sync.queue.SynchronizationQueueSink;
+
+/**
+ * Manages preferences for accessing gpodder.net service and other sync providers
+ */
+public class SynchronizationCredentials {
+
+ private SynchronizationCredentials() {
+ }
+
+ private static final String PREF_NAME = "gpodder.net";
+ private static final String PREF_USERNAME = "de.danoeh.antennapod.preferences.gpoddernet.username";
+ private static final String PREF_PASSWORD = "de.danoeh.antennapod.preferences.gpoddernet.password";
+ private static final String PREF_DEVICEID = "de.danoeh.antennapod.preferences.gpoddernet.deviceID";
+ private static final String PREF_HOSTNAME = "prefGpodnetHostname";
+
+ private static SharedPreferences getPreferences() {
+ return ClientConfig.applicationCallbacks.getApplicationInstance()
+ .getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
+ }
+
+ public static String getUsername() {
+ return getPreferences().getString(PREF_USERNAME, null);
+ }
+
+ public static void setUsername(String username) {
+ getPreferences().edit().putString(PREF_USERNAME, username).apply();
+ }
+
+ public static String getPassword() {
+ return getPreferences().getString(PREF_PASSWORD, null);
+ }
+
+ public static void setPassword(String password) {
+ getPreferences().edit().putString(PREF_PASSWORD, password).apply();
+ }
+
+ public static String getDeviceID() {
+ return getPreferences().getString(PREF_DEVICEID, null);
+ }
+
+ public static void setDeviceID(String deviceID) {
+ getPreferences().edit().putString(PREF_DEVICEID, deviceID).apply();
+ }
+
+ public static String getHosturl() {
+ return getPreferences().getString(PREF_HOSTNAME, null);
+ }
+
+ public static void setHosturl(String value) {
+ getPreferences().edit().putString(PREF_HOSTNAME, value).apply();
+ }
+
+ public static synchronized void clear(Context context) {
+ setUsername(null);
+ setPassword(null);
+ setDeviceID(null);
+ SynchronizationQueueSink.clearQueue(context);
+ UserPreferences.setGpodnetNotificationsEnabled();
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/sync/SynchronizationProviderViewData.java b/core/src/main/java/de/danoeh/antennapod/core/sync/SynchronizationProviderViewData.java
new file mode 100644
index 000000000..cba713f60
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/sync/SynchronizationProviderViewData.java
@@ -0,0 +1,47 @@
+package de.danoeh.antennapod.core.sync;
+
+import de.danoeh.antennapod.core.R;
+
+public enum SynchronizationProviderViewData {
+ GPODDER_NET(
+ "GPODDER_NET",
+ R.string.gpodnet_description,
+ R.drawable.gpodder_icon
+ ),
+ NEXTCLOUD_GPODDER(
+ "NEXTCLOUD_GPODDER",
+ R.string.synchronization_summary_nextcloud,
+ R.drawable.nextcloud_logo
+ );
+
+ public static SynchronizationProviderViewData fromIdentifier(String provider) {
+ for (SynchronizationProviderViewData synchronizationProvider : SynchronizationProviderViewData.values()) {
+ if (synchronizationProvider.getIdentifier().equals(provider)) {
+ return synchronizationProvider;
+ }
+ }
+ return null;
+ }
+
+ private final String identifier;
+ private final int iconResource;
+ private final int summaryResource;
+
+ SynchronizationProviderViewData(String identifier, int summaryResource, int iconResource) {
+ this.identifier = identifier;
+ this.iconResource = iconResource;
+ this.summaryResource = summaryResource;
+ }
+
+ public String getIdentifier() {
+ return identifier;
+ }
+
+ public int getIconResource() {
+ return iconResource;
+ }
+
+ public int getSummaryResource() {
+ return summaryResource;
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/sync/SynchronizationSettings.java b/core/src/main/java/de/danoeh/antennapod/core/sync/SynchronizationSettings.java
new file mode 100644
index 000000000..1a53ac0fb
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/sync/SynchronizationSettings.java
@@ -0,0 +1,83 @@
+package de.danoeh.antennapod.core.sync;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+
+import de.danoeh.antennapod.core.ClientConfig;
+
+public class SynchronizationSettings {
+
+ public static final String LAST_SYNC_ATTEMPT_TIMESTAMP = "last_sync_attempt_timestamp";
+ private static final String NAME = "synchronization";
+ private static final String SELECTED_SYNC_PROVIDER = "selected_sync_provider";
+ private static final String LAST_SYNC_ATTEMPT_SUCCESS = "last_sync_attempt_success";
+ private static final String LAST_EPISODE_ACTIONS_SYNC_TIMESTAMP = "last_episode_actions_sync_timestamp";
+ private static final String LAST_SUBSCRIPTION_SYNC_TIMESTAMP = "last_sync_timestamp";
+
+ public static boolean isProviderConnected() {
+ return getSelectedSyncProviderKey() != null;
+ }
+
+ public static void resetTimestamps() {
+ getSharedPreferences().edit()
+ .putLong(LAST_SUBSCRIPTION_SYNC_TIMESTAMP, 0)
+ .putLong(LAST_EPISODE_ACTIONS_SYNC_TIMESTAMP, 0)
+ .putLong(LAST_SYNC_ATTEMPT_TIMESTAMP, 0)
+ .apply();
+ }
+
+ public static boolean isLastSyncSuccessful() {
+ return getSharedPreferences().getBoolean(LAST_SYNC_ATTEMPT_SUCCESS, false);
+ }
+
+ public static long getLastSyncAttempt() {
+ return getSharedPreferences().getLong(LAST_SYNC_ATTEMPT_TIMESTAMP, 0);
+ }
+
+ public static void setSelectedSyncProvider(SynchronizationProviderViewData provider) {
+ getSharedPreferences()
+ .edit()
+ .putString(SELECTED_SYNC_PROVIDER, provider == null ? null : provider.getIdentifier())
+ .apply();
+ }
+
+ public static String getSelectedSyncProviderKey() {
+ return getSharedPreferences().getString(SELECTED_SYNC_PROVIDER, null);
+ }
+
+ public static void updateLastSynchronizationAttempt() {
+ getSharedPreferences().edit()
+ .putLong(LAST_SYNC_ATTEMPT_TIMESTAMP, System.currentTimeMillis())
+ .apply();
+ }
+
+ public static void setLastSynchronizationAttemptSuccess(boolean isSuccess) {
+ getSharedPreferences().edit()
+ .putBoolean(LAST_SYNC_ATTEMPT_SUCCESS, isSuccess)
+ .apply();
+ }
+
+ public static long getLastSubscriptionSynchronizationTimestamp() {
+ return getSharedPreferences().getLong(LAST_SUBSCRIPTION_SYNC_TIMESTAMP, 0);
+ }
+
+ public static void setLastSubscriptionSynchronizationAttemptTimestamp(long newTimeStamp) {
+ getSharedPreferences().edit()
+ .putLong(LAST_SUBSCRIPTION_SYNC_TIMESTAMP, newTimeStamp).apply();
+ }
+
+ public static long getLastEpisodeActionSynchronizationTimestamp() {
+ return getSharedPreferences()
+ .getLong(LAST_EPISODE_ACTIONS_SYNC_TIMESTAMP, 0);
+ }
+
+ public static void setLastEpisodeActionSynchronizationAttemptTimestamp(long timestamp) {
+ getSharedPreferences().edit()
+ .putLong(LAST_EPISODE_ACTIONS_SYNC_TIMESTAMP, timestamp).apply();
+ }
+
+ private static SharedPreferences getSharedPreferences() {
+ return ClientConfig.applicationCallbacks.getApplicationInstance()
+ .getSharedPreferences(NAME, Context.MODE_PRIVATE);
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/sync/queue/SynchronizationQueueSink.java b/core/src/main/java/de/danoeh/antennapod/core/sync/queue/SynchronizationQueueSink.java
new file mode 100644
index 000000000..445faf60f
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/sync/queue/SynchronizationQueueSink.java
@@ -0,0 +1,67 @@
+package de.danoeh.antennapod.core.sync.queue;
+
+import android.content.Context;
+
+import de.danoeh.antennapod.core.sync.LockingAsyncExecutor;
+import de.danoeh.antennapod.core.sync.SyncService;
+import de.danoeh.antennapod.core.sync.SynchronizationSettings;
+import de.danoeh.antennapod.model.feed.FeedMedia;
+import de.danoeh.antennapod.net.sync.model.EpisodeAction;
+
+public class SynchronizationQueueSink {
+
+ public static void clearQueue(Context context) {
+ LockingAsyncExecutor.executeLockedAsync(new SynchronizationQueueStorage(context)::clearQueue);
+ }
+
+ public static void enqueueFeedAddedIfSynchronizationIsActive(Context context, String downloadUrl) {
+ if (!SynchronizationSettings.isProviderConnected()) {
+ return;
+ }
+ LockingAsyncExecutor.executeLockedAsync(() -> {
+ new SynchronizationQueueStorage(context).enqueueFeedAdded(downloadUrl);
+ SyncService.sync(context);
+ });
+ }
+
+ public static void enqueueFeedRemovedIfSynchronizationIsActive(Context context, String downloadUrl) {
+ if (!SynchronizationSettings.isProviderConnected()) {
+ return;
+ }
+ LockingAsyncExecutor.executeLockedAsync(() -> {
+ new SynchronizationQueueStorage(context).enqueueFeedRemoved(downloadUrl);
+ SyncService.sync(context);
+ });
+ }
+
+ public static void enqueueEpisodeActionIfSynchronizationIsActive(Context context, EpisodeAction action) {
+ if (!SynchronizationSettings.isProviderConnected()) {
+ return;
+ }
+ LockingAsyncExecutor.executeLockedAsync(() -> {
+ new SynchronizationQueueStorage(context).enqueueEpisodeAction(action);
+ SyncService.sync(context);
+ });
+ }
+
+ public static void enqueueEpisodePlayedIfSynchronizationIsActive(Context context, FeedMedia media,
+ boolean completed) {
+ if (!SynchronizationSettings.isProviderConnected()) {
+ return;
+ }
+ if (media.getItem() == null) {
+ return;
+ }
+ if (media.getStartPosition() < 0 || (!completed && media.getStartPosition() >= media.getPosition())) {
+ return;
+ }
+ EpisodeAction action = new EpisodeAction.Builder(media.getItem(), EpisodeAction.PLAY)
+ .currentTimestamp()
+ .started(media.getStartPosition() / 1000)
+ .position((completed ? media.getDuration() : media.getPosition()) / 1000)
+ .total(media.getDuration() / 1000)
+ .build();
+ enqueueEpisodeActionIfSynchronizationIsActive(context, action);
+ }
+
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/sync/queue/SynchronizationQueueStorage.java b/core/src/main/java/de/danoeh/antennapod/core/sync/queue/SynchronizationQueueStorage.java
new file mode 100644
index 000000000..5c6d58fe3
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/sync/queue/SynchronizationQueueStorage.java
@@ -0,0 +1,140 @@
+package de.danoeh.antennapod.core.sync.queue;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+
+import java.util.ArrayList;
+
+import de.danoeh.antennapod.core.sync.SynchronizationSettings;
+import de.danoeh.antennapod.net.sync.model.EpisodeAction;
+
+public class SynchronizationQueueStorage {
+
+ private static final String NAME = "synchronization";
+ private static final String QUEUED_EPISODE_ACTIONS = "sync_queued_episode_actions";
+ private static final String QUEUED_FEEDS_REMOVED = "sync_removed";
+ private static final String QUEUED_FEEDS_ADDED = "sync_added";
+ private final SharedPreferences sharedPreferences;
+
+ public SynchronizationQueueStorage(Context context) {
+ this.sharedPreferences = context.getSharedPreferences(NAME, Context.MODE_PRIVATE);
+ }
+
+ public ArrayList<EpisodeAction> getQueuedEpisodeActions() {
+ ArrayList<EpisodeAction> actions = new ArrayList<>();
+ try {
+ String json = getSharedPreferences()
+ .getString(QUEUED_EPISODE_ACTIONS, "[]");
+ JSONArray queue = new JSONArray(json);
+ for (int i = 0; i < queue.length(); i++) {
+ actions.add(EpisodeAction.readFromJsonObject(queue.getJSONObject(i)));
+ }
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ return actions;
+ }
+
+ public ArrayList<String> getQueuedRemovedFeeds() {
+ ArrayList<String> removedFeedUrls = new ArrayList<>();
+ try {
+ String json = getSharedPreferences()
+ .getString(QUEUED_FEEDS_REMOVED, "[]");
+ JSONArray queue = new JSONArray(json);
+ for (int i = 0; i < queue.length(); i++) {
+ removedFeedUrls.add(queue.getString(i));
+ }
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ return removedFeedUrls;
+
+ }
+
+ public ArrayList<String> getQueuedAddedFeeds() {
+ ArrayList<String> addedFeedUrls = new ArrayList<>();
+ try {
+ String json = getSharedPreferences()
+ .getString(QUEUED_FEEDS_ADDED, "[]");
+ JSONArray queue = new JSONArray(json);
+ for (int i = 0; i < queue.length(); i++) {
+ addedFeedUrls.add(queue.getString(i));
+ }
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ return addedFeedUrls;
+ }
+
+ public void clearEpisodeActionQueue() {
+ getSharedPreferences().edit()
+ .putString(QUEUED_EPISODE_ACTIONS, "[]").apply();
+
+ }
+
+ public void clearFeedQueues() {
+ getSharedPreferences().edit()
+ .putString(QUEUED_FEEDS_ADDED, "[]")
+ .putString(QUEUED_FEEDS_REMOVED, "[]")
+ .apply();
+ }
+
+ protected void clearQueue() {
+ SynchronizationSettings.resetTimestamps();
+ getSharedPreferences().edit()
+ .putString(QUEUED_EPISODE_ACTIONS, "[]")
+ .putString(QUEUED_FEEDS_ADDED, "[]")
+ .putString(QUEUED_FEEDS_REMOVED, "[]")
+ .apply();
+
+ }
+
+ protected void enqueueFeedAdded(String downloadUrl) {
+ SharedPreferences sharedPreferences = getSharedPreferences();
+ String json = sharedPreferences
+ .getString(QUEUED_FEEDS_ADDED, "[]");
+ try {
+ JSONArray queue = new JSONArray(json);
+ queue.put(downloadUrl);
+ sharedPreferences
+ .edit().putString(QUEUED_FEEDS_ADDED, queue.toString()).apply();
+
+ } catch (JSONException jsonException) {
+ jsonException.printStackTrace();
+ }
+ }
+
+ protected void enqueueFeedRemoved(String downloadUrl) {
+ SharedPreferences sharedPreferences = getSharedPreferences();
+ String json = sharedPreferences.getString(QUEUED_FEEDS_REMOVED, "[]");
+ try {
+ JSONArray queue = new JSONArray(json);
+ queue.put(downloadUrl);
+ sharedPreferences.edit().putString(QUEUED_FEEDS_REMOVED, queue.toString())
+ .apply();
+ } catch (JSONException jsonException) {
+ jsonException.printStackTrace();
+ }
+ }
+
+ protected void enqueueEpisodeAction(EpisodeAction action) {
+ SharedPreferences sharedPreferences = getSharedPreferences();
+ String json = sharedPreferences.getString(QUEUED_EPISODE_ACTIONS, "[]");
+ try {
+ JSONArray queue = new JSONArray(json);
+ queue.put(action.writeToJsonObject());
+ sharedPreferences.edit().putString(
+ QUEUED_EPISODE_ACTIONS, queue.toString()
+ ).apply();
+ } catch (JSONException jsonException) {
+ jsonException.printStackTrace();
+ }
+ }
+
+ private SharedPreferences getSharedPreferences() {
+ return sharedPreferences;
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/FeedItemPermutors.java b/core/src/main/java/de/danoeh/antennapod/core/util/FeedItemPermutors.java
index e5f60d64b..09161ca7b 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/util/FeedItemPermutors.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/FeedItemPermutors.java
@@ -9,6 +9,7 @@ import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
import de.danoeh.antennapod.model.feed.FeedItem;
@@ -77,25 +78,22 @@ public class FeedItemPermutors {
@NonNull
private static Date pubDate(@Nullable FeedItem item) {
- return (item != null && item.getPubDate() != null) ?
- item.getPubDate() : new Date(0);
+ return (item != null && item.getPubDate() != null) ? item.getPubDate() : new Date(0);
}
@NonNull
private static String itemTitle(@Nullable FeedItem item) {
- return (item != null && item.getTitle() != null) ?
- item.getTitle() : "";
+ return (item != null && item.getTitle() != null) ? item.getTitle().toLowerCase(Locale.getDefault()) : "";
}
private static int duration(@Nullable FeedItem item) {
- return (item != null && item.getMedia() != null) ?
- item.getMedia().getDuration() : 0;
+ return (item != null && item.getMedia() != null) ? item.getMedia().getDuration() : 0;
}
@NonNull
private static String feedTitle(@Nullable FeedItem item) {
- return (item != null && item.getFeed() != null && item.getFeed().getTitle() != null) ?
- item.getFeed().getTitle() : "";
+ return (item != null && item.getFeed() != null && item.getFeed().getTitle() != null)
+ ? item.getFeed().getTitle().toLowerCase(Locale.getDefault()) : "";
}
/**
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 12f1e98f9..efc7845a4 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
@@ -8,7 +8,6 @@ import android.net.NetworkInfo;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.os.Build;
-import androidx.core.net.ConnectivityManagerCompat;
import android.text.TextUtils;
import android.util.Log;
@@ -16,7 +15,11 @@ import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import de.danoeh.antennapod.core.storage.DBTasks;
+import de.danoeh.antennapod.core.storage.DownloadRequester;
import de.danoeh.antennapod.model.feed.FeedMedia;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.service.download.AntennapodHttpClient;
@@ -30,6 +33,8 @@ import okhttp3.Request;
import okhttp3.Response;
public class NetworkUtils {
+ private static final String REGEX_PATTERN_IP_ADDRESS = "([0-9]{1,3}[\\.]){3}[0-9]{1,3}";
+
private NetworkUtils(){}
private static final String TAG = NetworkUtils.class.getSimpleName();
@@ -40,56 +45,23 @@ public class NetworkUtils {
NetworkUtils.context = context;
}
- /**
- * Returns true if the device is connected to Wi-Fi and the Wi-Fi filter for
- * automatic downloads is disabled or the device is connected to a Wi-Fi
- * network that is on the 'selected networks' list of the Wi-Fi filter for
- * automatic downloads and false otherwise.
- * */
- public static boolean autodownloadNetworkAvailable() {
- ConnectivityManager cm = (ConnectivityManager) context
- .getSystemService(Context.CONNECTIVITY_SERVICE);
+ public static boolean isAutoDownloadAllowed() {
+ ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo networkInfo = cm.getActiveNetworkInfo();
- if (networkInfo != null) {
- if (networkInfo.getType() == ConnectivityManager.TYPE_WIFI) {
- Log.d(TAG, "Device is connected to Wi-Fi");
- if (networkInfo.isConnected()) {
- if (!UserPreferences.isEnableAutodownloadWifiFilter()) {
- Log.d(TAG, "Auto-dl filter is disabled");
- return true;
- } else {
- WifiManager wm = (WifiManager) context.getApplicationContext()
- .getSystemService(Context.WIFI_SERVICE);
- WifiInfo wifiInfo = wm.getConnectionInfo();
- List<String> selectedNetworks = Arrays
- .asList(UserPreferences
- .getAutodownloadSelectedNetworks());
- if (selectedNetworks.contains(Integer.toString(wifiInfo
- .getNetworkId()))) {
- Log.d(TAG, "Current network is on the selected networks list");
- return true;
- }
- }
- }
- } else if (networkInfo.getType() == ConnectivityManager.TYPE_ETHERNET) {
- Log.d(TAG, "Device is connected to Ethernet");
- if (networkInfo.isConnected()) {
- return true;
- }
+ if (networkInfo == null) {
+ return false;
+ }
+ if (networkInfo.getType() == ConnectivityManager.TYPE_WIFI) {
+ if (UserPreferences.isEnableAutodownloadWifiFilter()) {
+ return isInAllowedWifiNetwork();
} else {
- if (!UserPreferences.isAllowMobileAutoDownload()) {
- Log.d(TAG, "Auto Download not enabled on Mobile");
- return false;
- }
- if (networkInfo.isRoaming()) {
- Log.d(TAG, "Roaming on foreign network");
- return false;
- }
- return true;
+ return !isNetworkMetered();
}
+ } else if (networkInfo.getType() == ConnectivityManager.TYPE_ETHERNET) {
+ return true;
+ } else {
+ return UserPreferences.isAllowMobileAutoDownload() || !NetworkUtils.isNetworkRestricted();
}
- Log.d(TAG, "Network for auto-dl is not available");
- return false;
}
public static boolean networkAvailable() {
@@ -126,7 +98,18 @@ public class NetworkUtils {
private static boolean isNetworkMetered() {
ConnectivityManager connManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
- return ConnectivityManagerCompat.isActiveNetworkMetered(connManager);
+
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
+ NetworkCapabilities capabilities = connManager.getNetworkCapabilities(
+ connManager.getActiveNetwork());
+
+ if (capabilities != null
+ && capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
+ && capabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN)) {
+ return false;
+ }
+ }
+ return connManager.isActiveNetworkMetered();
}
private static boolean isNetworkCellular() {
@@ -157,6 +140,12 @@ public class NetworkUtils {
}
}
+ private static boolean isInAllowedWifiNetwork() {
+ WifiManager wm = (WifiManager) context.getApplicationContext().getSystemService(Context.WIFI_SERVICE);
+ List<String> selectedNetworks = Arrays.asList(UserPreferences.getAutodownloadSelectedNetworks());
+ return selectedNetworks.contains(Integer.toString(wm.getConnectionInfo().getNetworkId()));
+ }
+
/**
* Returns the SSID of the wifi connection, or <code>null</code> if there is no wifi.
*/
@@ -169,6 +158,22 @@ public class NetworkUtils {
return null;
}
+ public static boolean wasDownloadBlocked(Throwable throwable) {
+ String message = throwable.getMessage();
+ if (message != null) {
+ Pattern pattern = Pattern.compile(REGEX_PATTERN_IP_ADDRESS);
+ Matcher matcher = pattern.matcher(message);
+ if (matcher.find()) {
+ String ip = matcher.group();
+ return ip.startsWith("127.") || ip.startsWith("0.");
+ }
+ }
+ if (throwable.getCause() != null) {
+ return wasDownloadBlocked(throwable.getCause());
+ }
+ return false;
+ }
+
public static Single<Long> getFeedMediaSizeObservable(FeedMedia media) {
return Single.create((SingleOnSubscribe<Long>) emitter -> {
if (!NetworkUtils.isEpisodeHeadDownloadAllowed()) {
@@ -225,4 +230,16 @@ public class NetworkUtils {
.observeOn(AndroidSchedulers.mainThread());
}
+ public static void networkChangedDetected() {
+ if (NetworkUtils.isAutoDownloadAllowed()) {
+ Log.d(TAG, "auto-dl network available, starting auto-download");
+ DBTasks.autodownloadUndownloadedItems(context);
+ } else { // if new network is Wi-Fi, finish ongoing downloads,
+ // otherwise cancel all downloads
+ if (NetworkUtils.isNetworkRestricted()) {
+ Log.i(TAG, "Device is no longer connected to Wi-Fi. Cancelling ongoing downloads");
+ DownloadRequester.getInstance().cancelAllDownloads(context);
+ }
+ }
+ }
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/ShareUtils.java b/core/src/main/java/de/danoeh/antennapod/core/util/ShareUtils.java
index c1c48f70d..34b9d294d 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
@@ -5,10 +5,12 @@ import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.net.Uri;
-import android.os.Build;
-import androidx.core.content.FileProvider;
import android.util.Log;
+import androidx.annotation.NonNull;
+import androidx.core.app.ShareCompat;
+import androidx.core.content.FileProvider;
+
import java.io.File;
import java.util.List;
@@ -24,11 +26,13 @@ public class ShareUtils {
private ShareUtils() {
}
- public static void shareLink(Context context, String text) {
- Intent i = new Intent(Intent.ACTION_SEND);
- i.setType("text/plain");
- i.putExtra(Intent.EXTRA_TEXT, text);
- context.startActivity(Intent.createChooser(i, context.getString(R.string.share_url_label)));
+ public static void shareLink(@NonNull Context context, @NonNull String text) {
+ Intent intent = new ShareCompat.IntentBuilder(context)
+ .setType("text/plain")
+ .setText(text)
+ .setChooserTitle(R.string.share_url_label)
+ .createChooserIntent();
+ context.startActivity(intent);
}
public static void shareFeedlink(Context context, Feed feed) {
@@ -75,21 +79,20 @@ public class ShareUtils {
}
public static void shareFeedItemFile(Context context, FeedMedia media) {
- Intent i = new Intent(Intent.ACTION_SEND);
- i.setType(media.getMime_type());
+ 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()));
- i.putExtra(Intent.EXTRA_STREAM, fileUri);
- i.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
- if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) {
- List<ResolveInfo> resInfoList = context.getPackageManager()
- .queryIntentActivities(i, PackageManager.MATCH_DEFAULT_ONLY);
- for (ResolveInfo resolveInfo : resInfoList) {
- String packageName = resolveInfo.activityInfo.packageName;
- context.grantUriPermission(packageName, fileUri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
- }
+ 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(Intent.createChooser(i, context.getString(R.string.share_file_label)));
+ context.startActivity(chooserIntent);
Log.e(TAG, "shareFeedItemFile called");
}
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/StorageUtils.java b/core/src/main/java/de/danoeh/antennapod/core/util/StorageUtils.java
index 414b5c781..cf049ed80 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/util/StorageUtils.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/StorageUtils.java
@@ -1,7 +1,6 @@
package de.danoeh.antennapod.core.util;
import android.app.Activity;
-import android.os.Build;
import android.os.StatFs;
import android.util.Log;
@@ -63,29 +62,15 @@ public class StorageUtils {
*/
public static long getFreeSpaceAvailable(String path) {
StatFs stat = new StatFs(path);
- long availableBlocks;
- long blockSize;
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
- availableBlocks = stat.getAvailableBlocksLong();
- blockSize = stat.getBlockSizeLong();
- } else {
- availableBlocks = stat.getAvailableBlocks();
- blockSize = stat.getBlockSize();
- }
+ long availableBlocks = stat.getAvailableBlocksLong();
+ long blockSize = stat.getBlockSizeLong();
return availableBlocks * blockSize;
}
public static long getTotalSpaceAvailable(String path) {
StatFs stat = new StatFs(path);
- long blockCount;
- long blockSize;
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
- blockCount = stat.getBlockCountLong();
- blockSize = stat.getBlockSizeLong();
- } else {
- blockCount = stat.getBlockCount();
- blockSize = stat.getBlockSize();
- }
+ long blockCount = stat.getBlockCountLong();
+ long blockSize = stat.getBlockSizeLong();
return blockCount * blockSize;
}
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/comparator/CompareCompat.java b/core/src/main/java/de/danoeh/antennapod/core/util/comparator/CompareCompat.java
deleted file mode 100644
index c189f2389..000000000
--- a/core/src/main/java/de/danoeh/antennapod/core/util/comparator/CompareCompat.java
+++ /dev/null
@@ -1,29 +0,0 @@
-package de.danoeh.antennapod.core.util.comparator;
-
-/**
- * Some compare() methods are not available before API 19.
- * This class provides fallbacks
- */
-public class CompareCompat {
-
- private CompareCompat() {
- // Must not be instantiated
- }
-
- /**
- * Compares two {@code long} values. Long.compare() is not available before API 19
- *
- * @return 0 if long1 = long2, less than 0 if long1 &lt; long2,
- * and greater than 0 if long1 &gt; long2.
- */
- public static int compareLong(long long1, long long2) {
- //noinspection UseCompareMethod
- if (long1 > long2) {
- return -1;
- } else if (long1 < long2) {
- return 1;
- } else {
- return 0;
- }
- }
-}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/download/AutoUpdateManager.java b/core/src/main/java/de/danoeh/antennapod/core/util/download/AutoUpdateManager.java
index bb0a71744..dbad1f63e 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/util/download/AutoUpdateManager.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/download/AutoUpdateManager.java
@@ -1,9 +1,10 @@
package de.danoeh.antennapod.core.util.download;
import android.content.Context;
-import androidx.annotation.NonNull;
import android.util.Log;
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.AlertDialog;
import androidx.work.Constraints;
import androidx.work.Data;
import androidx.work.ExistingPeriodicWorkPolicy;
@@ -17,6 +18,7 @@ import java.util.Arrays;
import java.util.Calendar;
import java.util.concurrent.TimeUnit;
+import de.danoeh.antennapod.core.R;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.service.FeedUpdateWorker;
import de.danoeh.antennapod.core.storage.DBTasks;
@@ -70,7 +72,7 @@ public class AutoUpdateManager {
Log.d(TAG, "Restarting update alarm.");
Calendar now = Calendar.getInstance();
- Calendar alarm = (Calendar)now.clone();
+ Calendar alarm = (Calendar) now.clone();
alarm.set(Calendar.HOUR_OF_DAY, hoursOfDay);
alarm.set(Calendar.MINUTE, minute);
if (alarm.before(now) || alarm.equals(now)) {
@@ -121,8 +123,24 @@ public class AutoUpdateManager {
Log.d(TAG, "Run auto update immediately in background.");
if (!NetworkUtils.networkAvailable()) {
Log.d(TAG, "Ignoring: No network connection.");
- return;
+ } else if (NetworkUtils.isEpisodeDownloadAllowed()) {
+ startRefreshAllFeeds(context);
+ } else {
+ confirmMobileAllFeedsRefresh(context);
}
+ }
+
+ private static void confirmMobileAllFeedsRefresh(final Context context) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(context)
+ .setTitle(R.string.feed_refresh_title)
+ .setMessage(R.string.confirm_mobile_feed_refresh_dialog_message)
+ .setPositiveButton(R.string.yes,
+ (dialog, which) -> startRefreshAllFeeds(context))
+ .setNegativeButton(R.string.no, null);
+ builder.show();
+ }
+
+ private static void startRefreshAllFeeds(final Context context) {
new Thread(() -> DBTasks.refreshAllFeeds(
context.getApplicationContext(), true), "ManualRefreshAllFeeds").start();
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/download/ConnectionStateMonitor.java b/core/src/main/java/de/danoeh/antennapod/core/util/download/ConnectionStateMonitor.java
new file mode 100644
index 000000000..5c543bf5a
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/download/ConnectionStateMonitor.java
@@ -0,0 +1,46 @@
+package de.danoeh.antennapod.core.util.download;
+
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
+import android.os.Build;
+import android.util.Log;
+
+import androidx.annotation.RequiresApi;
+import de.danoeh.antennapod.core.util.NetworkUtils;
+
+@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
+public class ConnectionStateMonitor
+ extends ConnectivityManager.NetworkCallback
+ implements ConnectivityManager.OnNetworkActiveListener {
+ private static final String TAG = "ConnectionStateMonitor";
+ final NetworkRequest networkRequest;
+
+ public ConnectionStateMonitor() {
+ networkRequest = new NetworkRequest.Builder()
+ .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
+ .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
+ .build();
+ }
+
+ @Override
+ public void onNetworkActive() {
+ Log.d(TAG, "ConnectionStateMonitor::onNetworkActive network connection changed");
+ NetworkUtils.networkChangedDetected();
+ }
+
+ public void enable(Context context) {
+ ConnectivityManager connectivityManager =
+ (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ connectivityManager.registerNetworkCallback(networkRequest, this);
+ connectivityManager.addDefaultNetworkActiveListener(this);
+ }
+
+ public void disable(Context context) {
+ ConnectivityManager connectivityManager =
+ (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ connectivityManager.unregisterNetworkCallback(this);
+ connectivityManager.removeDefaultNetworkActiveListener(this);
+ }
+} \ No newline at end of file
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 7f4c1ceaf..549171c76 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
@@ -7,23 +7,24 @@ import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection;
-import android.media.MediaPlayer;
import android.os.IBinder;
import android.util.Log;
import android.util.Pair;
import android.view.SurfaceHolder;
import androidx.annotation.NonNull;
-import de.danoeh.antennapod.core.R;
-import de.danoeh.antennapod.core.event.MessageEvent;
-import de.danoeh.antennapod.core.event.ServiceEvent;
+import de.danoeh.antennapod.core.storage.DBWriter;
+import de.danoeh.antennapod.event.playback.PlaybackPositionEvent;
+import de.danoeh.antennapod.model.feed.FeedMedia;
+import de.danoeh.antennapod.event.playback.PlaybackServiceEvent;
+import de.danoeh.antennapod.event.playback.SpeedChangedEvent;
import de.danoeh.antennapod.model.playback.MediaType;
import de.danoeh.antennapod.core.feed.util.PlaybackSpeedUtils;
import de.danoeh.antennapod.core.preferences.PlaybackPreferences;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.service.playback.PlaybackService;
-import de.danoeh.antennapod.core.service.playback.PlaybackServiceMediaPlayer;
-import de.danoeh.antennapod.core.service.playback.PlayerStatus;
import de.danoeh.antennapod.model.playback.Playable;
+import de.danoeh.antennapod.playback.base.PlaybackServiceMediaPlayer;
+import de.danoeh.antennapod.playback.base.PlayerStatus;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
@@ -71,8 +72,8 @@ public abstract class PlaybackController {
}
@Subscribe(threadMode = ThreadMode.MAIN)
- public void onEventMainThread(ServiceEvent event) {
- if (event.action == ServiceEvent.Action.SERVICE_STARTED) {
+ public void onEventMainThread(PlaybackServiceEvent event) {
+ if (event.action == PlaybackServiceEvent.Action.SERVICE_STARTED) {
init();
}
}
@@ -209,13 +210,6 @@ public abstract class PlaybackController {
return;
}
switch (type) {
- case PlaybackService.NOTIFICATION_TYPE_ERROR:
- handleError(code);
- break;
- case PlaybackService.NOTIFICATION_TYPE_BUFFER_UPDATE:
- float progress = ((float) code) / 100;
- onBufferUpdate(progress);
- break;
case PlaybackService.NOTIFICATION_TYPE_RELOAD:
if (playbackService == null && PlaybackService.isRunning) {
bindToService();
@@ -226,21 +220,9 @@ public abstract class PlaybackController {
onReloadNotification(intent.getIntExtra(
PlaybackService.EXTRA_NOTIFICATION_CODE, -1));
break;
- case PlaybackService.NOTIFICATION_TYPE_SLEEPTIMER_UPDATE:
- onSleepTimerUpdate();
- break;
- case PlaybackService.NOTIFICATION_TYPE_BUFFER_START:
- onBufferStart();
- break;
- case PlaybackService.NOTIFICATION_TYPE_BUFFER_END:
- onBufferEnd();
- break;
case PlaybackService.NOTIFICATION_TYPE_PLAYBACK_END:
onPlaybackEnd();
break;
- case PlaybackService.NOTIFICATION_TYPE_PLAYBACK_SPEED_CHANGE:
- onPlaybackSpeedChange();
- break;
}
}
@@ -248,24 +230,11 @@ public abstract class PlaybackController {
public void onPositionObserverUpdate() {}
-
- public void onPlaybackSpeedChange() {}
-
/**
* Called when the currently displayed information should be refreshed.
*/
public void onReloadNotification(int code) {}
- public void onBufferStart() {}
-
- public void onBufferEnd() {}
-
- public void onBufferUpdate(float progress) {}
-
- public void onSleepTimerUpdate() {}
-
- public void handleError(int code) {}
-
public void onPlaybackEnd() {}
/**
@@ -276,10 +245,6 @@ public abstract class PlaybackController {
Log.d(TAG, "status: " + status.toString());
checkMediaInfoLoaded();
switch (status) {
- case ERROR:
- EventBus.getDefault().post(new MessageEvent(activity.getString(R.string.player_error_msg)));
- handleError(MediaPlayer.MEDIA_ERROR_UNKNOWN);
- break;
case PAUSED:
onPositionObserverUpdate();
updatePlayButtonShowsPlay(true);
@@ -458,6 +423,11 @@ public abstract class PlaybackController {
public void seekTo(int time) {
if (playbackService != null) {
playbackService.seekTo(time);
+ } else if (getMedia() instanceof FeedMedia) {
+ FeedMedia media = (FeedMedia) getMedia();
+ media.setPosition(time);
+ DBWriter.setFeedItem(media.getItem());
+ EventBus.getDefault().post(new PlaybackPositionEvent(time, getMedia().getDuration()));
}
}
@@ -482,7 +452,7 @@ public abstract class PlaybackController {
if (playbackService != null) {
playbackService.setSpeed(speed);
} else {
- onPlaybackSpeedChange();
+ EventBus.getDefault().post(new SpeedChangedEvent(speed));
}
}
@@ -555,20 +525,6 @@ public abstract class PlaybackController {
}
}
- /**
- * Move service into INITIALIZED state if it's paused to save bandwidth
- */
- public void reinitServiceIfPaused() {
- if (playbackService != null
- && playbackService.isStreaming()
- && !PlaybackService.isCasting()
- && (playbackService.getStatus() == PlayerStatus.PAUSED ||
- (playbackService.getStatus() == PlayerStatus.PREPARING &&
- !playbackService.isStartWhenPrepared()))) {
- playbackService.reinit();
- }
- }
-
public boolean isStreaming() {
return playbackService != null && playbackService.isStreaming();
}
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 cecd4b3b6..2762fb9fe 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
@@ -7,6 +7,7 @@ import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.Bitmap;
+import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.view.KeyEvent;
@@ -19,15 +20,16 @@ import com.bumptech.glide.request.RequestOptions;
import java.util.concurrent.TimeUnit;
import de.danoeh.antennapod.core.R;
+import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.model.playback.MediaType;
import de.danoeh.antennapod.core.glide.ApGlideSettings;
import de.danoeh.antennapod.core.receiver.MediaButtonReceiver;
import de.danoeh.antennapod.core.receiver.PlayerWidget;
-import de.danoeh.antennapod.core.service.playback.PlayerStatus;
import de.danoeh.antennapod.core.util.Converter;
import de.danoeh.antennapod.core.feed.util.ImageResourceUtils;
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.VideoPlayerActivityStarter;
@@ -67,9 +69,6 @@ public abstract class WidgetUpdater {
if (!PlayerWidget.isEnabled(context) || widgetState == null) {
return;
}
- ComponentName playerWidget = new ComponentName(context, PlayerWidget.class);
- AppWidgetManager manager = AppWidgetManager.getInstance(context);
- int[] widgetIds = manager.getAppWidgetIds(playerWidget);
PendingIntent startMediaPlayer;
if (widgetState.media != null && widgetState.media.getMediaType() == MediaType.VIDEO
@@ -156,36 +155,36 @@ public abstract class WidgetUpdater {
views.setImageViewResource(R.id.butPlayExtended, R.drawable.ic_widget_play);
}
- if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) {
- for (int id : widgetIds) {
- Bundle options = manager.getAppWidgetOptions(id);
- SharedPreferences prefs = context.getSharedPreferences(PlayerWidget.PREFS_NAME, Context.MODE_PRIVATE);
- int minWidth = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH);
- int columns = getCellsForSize(minWidth);
- if (columns < 3) {
- views.setViewVisibility(R.id.layout_center, View.INVISIBLE);
- } else {
- views.setViewVisibility(R.id.layout_center, View.VISIBLE);
- }
- 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) {
- views.setInt(R.id.extendedButtonsContainer, "setVisibility", View.VISIBLE);
- views.setInt(R.id.butPlay, "setVisibility", 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);
- }
-
- int backgroundColor = prefs.getInt(PlayerWidget.KEY_WIDGET_COLOR + id, PlayerWidget.DEFAULT_COLOR);
- views.setInt(R.id.widgetLayout, "setBackgroundColor", backgroundColor);
+ ComponentName playerWidget = new ComponentName(context, PlayerWidget.class);
+ AppWidgetManager manager = AppWidgetManager.getInstance(context);
+ int[] widgetIds = manager.getAppWidgetIds(playerWidget);
- manager.updateAppWidget(id, views);
+ for (int id : widgetIds) {
+ Bundle options = manager.getAppWidgetOptions(id);
+ SharedPreferences prefs = context.getSharedPreferences(PlayerWidget.PREFS_NAME, Context.MODE_PRIVATE);
+ int minWidth = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH);
+ int columns = getCellsForSize(minWidth);
+ if (columns < 3) {
+ views.setViewVisibility(R.id.layout_center, View.INVISIBLE);
+ } else {
+ views.setViewVisibility(R.id.layout_center, View.VISIBLE);
}
- } else {
- manager.updateAppWidget(playerWidget, views);
+ 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) {
+ views.setInt(R.id.extendedButtonsContainer, "setVisibility", View.VISIBLE);
+ views.setInt(R.id.butPlay, "setVisibility", 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);
+ }
+
+ int backgroundColor = prefs.getInt(PlayerWidget.KEY_WIDGET_COLOR + id, PlayerWidget.DEFAULT_COLOR);
+ views.setInt(R.id.widgetLayout, "setBackgroundColor", backgroundColor);
+
+ manager.updateAppWidget(id, views);
}
}
@@ -212,18 +211,21 @@ public abstract class WidgetUpdater {
startingIntent.setAction(MediaButtonReceiver.NOTIFY_BUTTON_RECEIVER);
startingIntent.putExtra(Intent.EXTRA_KEY_EVENT, event);
- return PendingIntent.getBroadcast(context, eventCode, startingIntent, 0);
+ return PendingIntent.getBroadcast(context, eventCode, startingIntent,
+ (Build.VERSION.SDK_INT >= 23 ? PendingIntent.FLAG_IMMUTABLE : 0));
}
private static String getProgressString(int position, int duration, float speed) {
- if (position >= 0 && duration > 0) {
- TimeSpeedConverter converter = new TimeSpeedConverter(speed);
- position = converter.convert(position);
- duration = converter.convert(duration);
- return Converter.getDurationStringLong(position) + " / "
- + Converter.getDurationStringLong(duration);
- } else {
+ if (position < 0 || duration <= 0) {
return null;
}
+ TimeSpeedConverter converter = new TimeSpeedConverter(speed);
+ if (UserPreferences.shouldShowRemainingTime()) {
+ return Converter.getDurationStringLong(converter.convert(position)) + " / -"
+ + Converter.getDurationStringLong(converter.convert(Math.max(0, duration - position)));
+ } else {
+ return Converter.getDurationStringLong(converter.convert(position)) + " / "
+ + Converter.getDurationStringLong(converter.convert(duration));
+ }
}
}
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
index b14fb3b0b..325c508c5 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/widget/WidgetUpdaterJobService.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/widget/WidgetUpdaterJobService.java
@@ -6,9 +6,9 @@ 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.core.service.playback.PlayerStatus;
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;
diff --git a/core/src/main/res/drawable-nodpi/nextcloud_logo.png b/core/src/main/res/drawable-nodpi/nextcloud_logo.png
new file mode 100644
index 000000000..2164e37fb
--- /dev/null
+++ b/core/src/main/res/drawable-nodpi/nextcloud_logo.png
Binary files differ
diff --git a/core/src/main/res/drawable/ic_download_black.xml b/core/src/main/res/drawable/ic_download_black.xml
new file mode 100644
index 000000000..eba137a59
--- /dev/null
+++ b/core/src/main/res/drawable/ic_download_black.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="#000000"
+ android:pathData="M18,15v3H6v-3H4v3c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2v-3H18zM17,11l-1.41,-1.41L13,12.17V4h-2v8.17L8.41,9.59L7,11l5,5L17,11z"/>
+</vector>
diff --git a/core/src/main/res/drawable/ic_tag.xml b/core/src/main/res/drawable/ic_tag.xml
new file mode 100644
index 000000000..95db04e93
--- /dev/null
+++ b/core/src/main/res/drawable/ic_tag.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="?attr/action_icon_color"
+ android:pathData="M21.41,11.58L12.41,2.58A2,2 0,0 0,11 2H4A2,2 0,0 0,2 4V11A2,2 0,0 0,2.59 12.42L11.59,21.42A2,2 0,0 0,13 22A2,2 0,0 0,14.41 21.41L21.41,14.41A2,2 0,0 0,22 13A2,2 0,0 0,21.41 11.58M13,20L4,11V4H11L20,13M6.5,5A1.5,1.5 0,1 1,5 6.5A1.5,1.5 0,0 1,6.5 5Z"/>
+</vector>
diff --git a/core/src/main/res/layout/player_widget.xml b/core/src/main/res/layout/player_widget.xml
index a70e98f0f..60d40e6b5 100644
--- a/core/src/main/res/layout/player_widget.xml
+++ b/core/src/main/res/layout/player_widget.xml
@@ -19,7 +19,6 @@
android:layout_width="@android:dimen/app_icon_size"
android:layout_height="match_parent"
android:contentDescription="@string/play_label"
- android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:layout_margin="12dp"
android:background="?android:attr/selectableItemBackground"
@@ -31,9 +30,7 @@
android:id="@+id/layout_left"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
- android:layout_toLeftOf="@id/butPlay"
android:layout_toStartOf="@id/butPlay"
android:background="@android:color/transparent"
android:gravity="fill_horizontal"
@@ -97,7 +94,6 @@
android:layout_height="36dp"
android:background="?android:attr/selectableItemBackground"
android:contentDescription="@string/rewind_label"
- android:layout_marginRight="2dp"
android:layout_marginEnd="2dp"
android:scaleType="fitXY"
android:src="@drawable/ic_widget_fast_rewind" />
@@ -108,7 +104,6 @@
android:layout_height="36dp"
android:background="?android:attr/selectableItemBackground"
android:contentDescription="@string/play_label"
- android:layout_marginRight="2dp"
android:layout_marginEnd="2dp"
android:scaleType="fitXY"
android:src="@drawable/ic_widget_play" />
@@ -119,7 +114,6 @@
android:layout_height="36dp"
android:background="?android:attr/selectableItemBackground"
android:contentDescription="@string/fast_forward_label"
- android:layout_marginRight="2dp"
android:layout_marginEnd="2dp"
android:scaleType="fitXY"
android:src="@drawable/ic_widget_fast_forward" />
@@ -130,7 +124,6 @@
android:layout_height="36dp"
android:background="?android:attr/selectableItemBackground"
android:contentDescription="@string/skip_episode_label"
- android:layout_marginRight="2dp"
android:layout_marginEnd="2dp"
android:scaleType="fitXY"
android:src="@drawable/ic_widget_skip" />
diff --git a/core/src/main/res/values-land/dimens.xml b/core/src/main/res/values-land/dimens.xml
deleted file mode 100644
index 73b2b2e98..000000000
--- a/core/src/main/res/values-land/dimens.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<resources>
- <dimen name="media_router_controller_playback_control_start_padding">@dimen/media_router_controller_playback_control_horizontal_spacing</dimen>
-</resources>
diff --git a/core/src/main/res/values-v21/styles.xml b/core/src/main/res/values-v21/styles.xml
index 996b16f5e..349ca3213 100644
--- a/core/src/main/res/values-v21/styles.xml
+++ b/core/src/main/res/values-v21/styles.xml
@@ -4,14 +4,17 @@
<item name="android:windowContentTransitions">true</item>
<!-- To make icons visible -->
<item name="android:statusBarColor">@color/grey600</item>
+ <item name="android:navigationBarColor">@color/grey600</item>
</style>
<style name="Theme.AntennaPod.Dark" parent="Theme.Base.AntennaPod.Dark">
<item name="android:windowContentTransitions">true</item>
<item name="android:statusBarColor">@color/background_darktheme</item>
+ <item name="android:navigationBarColor">@color/background_darktheme</item>
</style>
<style name="Theme.AntennaPod.TrueBlack" parent="Theme.Base.AntennaPod.TrueBlack">
<item name="android:statusBarColor">@color/black</item>
+ <item name="android:navigationBarColor">@color/black</item>
</style>
</resources> \ No newline at end of file
diff --git a/core/src/main/res/values-v23/styles.xml b/core/src/main/res/values-v23/styles.xml
index fd339a071..dde8e41ae 100644
--- a/core/src/main/res/values-v23/styles.xml
+++ b/core/src/main/res/values-v23/styles.xml
@@ -4,15 +4,18 @@
<item name="android:windowContentTransitions">true</item>
<item name="android:statusBarColor">@color/background_light</item>
<item name="android:windowLightStatusBar">true</item>
+ <item name="android:navigationBarColor">@color/background_light</item>
</style>
<style name="Theme.AntennaPod.Dark" parent="Theme.Base.AntennaPod.Dark">
<item name="android:windowContentTransitions">true</item>
<item name="android:statusBarColor">@color/background_darktheme</item>
<item name="android:windowLightStatusBar">false</item>
+ <item name="android:navigationBarColor">@color/background_darktheme</item>
</style>
<style name="Theme.AntennaPod.TrueBlack" parent="Theme.Base.AntennaPod.TrueBlack">
<item name="android:statusBarColor">@color/black</item>
+ <item name="android:navigationBarColor">@color/black</item>
</style>
</resources> \ No newline at end of file
diff --git a/core/src/main/res/values-v27/styles.xml b/core/src/main/res/values-v27/styles.xml
new file mode 100644
index 000000000..a28090155
--- /dev/null
+++ b/core/src/main/res/values-v27/styles.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <style name="Theme.AntennaPod.Light" parent="Theme.Base.AntennaPod.Light">
+ <item name="android:windowContentTransitions">true</item>
+ <item name="android:statusBarColor">@color/background_light</item>
+ <item name="android:windowLightStatusBar">true</item>
+ <item name="android:navigationBarColor">@color/background_light</item>
+ <item name="android:navigationBarDividerColor">@color/navigation_bar_divider_light</item>
+ <item name="android:windowLightNavigationBar">true</item>
+ </style>
+</resources> \ No newline at end of file
diff --git a/core/src/main/res/values/arrays.xml b/core/src/main/res/values/arrays.xml
index 97b677362..ba4d48219 100644
--- a/core/src/main/res/values/arrays.xml
+++ b/core/src/main/res/values/arrays.xml
@@ -135,58 +135,6 @@
<item>-2</item>
</string-array>
- <string-array name="playback_speed_values">
- <item>0.50</item>
- <item>0.60</item>
- <item>0.70</item>
- <item>0.75</item>
- <item>0.80</item>
- <item>0.85</item>
- <item>0.90</item>
- <item>0.95</item>
- <item>1.00</item>
- <item>1.05</item>
- <item>1.10</item>
- <item>1.15</item>
- <item>1.20</item>
- <item>1.25</item>
- <item>1.30</item>
- <item>1.35</item>
- <item>1.40</item>
- <item>1.45</item>
- <item>1.50</item>
- <item>1.55</item>
- <item>1.60</item>
- <item>1.65</item>
- <item>1.70</item>
- <item>1.75</item>
- <item>1.80</item>
- <item>1.85</item>
- <item>1.90</item>
- <item>1.95</item>
- <item>2.00</item>
- <item>2.10</item>
- <item>2.20</item>
- <item>2.30</item>
- <item>2.40</item>
- <item>2.50</item>
- <item>2.60</item>
- <item>2.70</item>
- <item>2.80</item>
- <item>2.90</item>
- <item>3.00</item>
- <item>3.10</item>
- <item>3.20</item>
- <item>3.30</item>
- <item>3.40</item>
- <item>3.50</item>
- <item>3.60</item>
- <item>3.70</item>
- <item>3.80</item>
- <item>3.90</item>
- <item>4.00</item>
- </string-array>
-
<string-array name="theme_options">
<item>@string/pref_theme_title_use_system</item>
<item>@string/pref_theme_title_light</item>
diff --git a/core/src/main/res/values/colors.xml b/core/src/main/res/values/colors.xml
index 760044854..859b64367 100644
--- a/core/src/main/res/values/colors.xml
+++ b/core/src/main/res/values/colors.xml
@@ -25,6 +25,7 @@
<color name="non_square_icon_background">#22777777</color>
<color name="seek_background_light">#90000000</color>
<color name="seek_background_dark">#905B5B5B</color>
+ <color name="navigation_bar_divider_light">#1F000000</color>
<color name="accent_light">#0078C2</color>
<color name="accent_dark">#3D8BFF</color>
diff --git a/core/src/main/res/values/dimens.xml b/core/src/main/res/values/dimens.xml
index d1e200d1d..4b2247492 100644
--- a/core/src/main/res/values/dimens.xml
+++ b/core/src/main/res/values/dimens.xml
@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
-
<dimen name="widget_margin">0dp</dimen>
<dimen name="external_player_height">64dp</dimen>
<dimen name="text_size_micro">12sp</dimen>
@@ -28,11 +27,5 @@
<dimen name="audioplayer_playercontrols_length_big">64dp</dimen>
<dimen name="audioplayer_playercontrols_margin">12dp</dimen>
- <dimen name="media_router_controller_playback_control_vertical_padding">16dp</dimen>
- <dimen name="media_router_controller_playback_control_horizontal_spacing">12dp</dimen>
- <dimen name="media_router_controller_playback_control_start_padding">24dp</dimen>
- <dimen name="media_router_controller_bottom_margin">8dp</dimen>
-
<dimen name="nav_drawer_max_screen_size">480dp</dimen>
-
</resources>
diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml
index e407b700a..d6915b76b 100644
--- a/core/src/main/res/values/strings.xml
+++ b/core/src/main/res/values/strings.xml
@@ -45,7 +45,6 @@
<!-- Statistics fragment -->
<string name="total_time_listened_to_podcasts">Total time of episodes played:</string>
- <string name="statistics_details_dialog">%1$d out of %2$d episodes started.\n\nPlayed %3$s out of %4$s.</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>
@@ -187,7 +186,9 @@
<item quantity="one">%d subscription updated.</item>
<item quantity="other">%d subscriptions updated.</item>
</plurals>
- <string name="add_to_folder">Add to folder</string>
+ <string name="edit_tags">Edit tags</string>
+ <string name="rename_tag_label">Rename tag</string>
+ <string name="confirm_mobile_feed_refresh_dialog_message">Downloading episodes over mobile data connection is disabled in the settings.\n\nDo you still want to refresh all podcasts over mobile data?</string>
<!-- actions on feeditems -->
<string name="download_label">Download</string>
@@ -202,8 +203,8 @@
<string name="delete_failed">Unable to delete file. Rebooting the device could help.</string>
<string name="delete_episode_label">Delete Episode</string>
<plurals name="deleted_multi_episode_batch_label">
- <item quantity="one">%d episode selected, %d download deleted.</item>
- <item quantity="other">%d episodes selected, %d download(s) deleted.</item>
+ <item quantity="one">1 downloaded episode deleted.</item>
+ <item quantity="other">%d downloaded episodes deleted.</item>
</plurals>
<string name="remove_new_flag_label">Remove \"new\" flag</string>
<string name="removed_new_flag_label">Removed \"new\" flag</string>
@@ -263,8 +264,8 @@
<string name="download_error_forbidden">The podcast host\'s server refuses to respond.</string>
<string name="download_canceled_msg">Download canceled</string>
<string name="download_error_wrong_size">The server connection was lost before completing the download</string>
- <string name="download_error_blocked">The download was blocked by another app on your device.</string>
- <string name="download_error_certificate">Unable to establish a secure connection. This can mean that another app on your device blocked the download, or that something is wrong with the server certificates.</string>
+ <string name="download_error_blocked">The download was blocked by another app on your device (like a VPN or ad blocker).</string>
+ <string name="download_error_certificate">Unable to establish a secure connection. This can mean that another app on your device (like a VPN or an ad blocker) blocked the download, or that something is wrong with the server certificates.</string>
<string name="download_report_title">Downloads completed with error(s)</string>
<string name="auto_download_report_title">Auto-downloads completed</string>
<string name="download_error_io_error">IO Error</string>
@@ -294,7 +295,6 @@
<string name="confirm_mobile_download_dialog_enable_temporarily">Allow temporarily</string>
<!-- Mediaplayer messages -->
- <string name="player_error_msg">Error!</string>
<string name="playback_error_server_died">Server died</string>
<string name="playback_error_unsupported">Unsupported media type</string>
<string name="playback_error_timeout">Operation timed out</string>
@@ -356,7 +356,7 @@
<string name="storage_sum">Episode auto delete, Import, Export</string>
<string name="project_pref">Project</string>
<string name="synchronization_pref">Synchronization</string>
- <string name="synchronization_sum">Synchronize with other devices using gpodder.net</string>
+ <string name="synchronization_sum">Synchronize with other devices</string>
<string name="automation">Automation</string>
<string name="download_pref_details">Details</string>
<string name="import_export_pref">Import/Export</string>
@@ -447,17 +447,20 @@
<string name="pref_theme_title_dark">Dark</string>
<string name="pref_theme_title_trueblack">Black (AMOLED ready)</string>
<string name="pref_episode_cache_unlimited">Unlimited</string>
- <string name="pref_gpodnet_authenticate_title">Login</string>
- <string name="pref_gpodnet_authenticate_sum">Login with your gpodder.net account in order to sync your subscriptions.</string>
- <string name="pref_gpodnet_logout_title">Logout</string>
- <string name="pref_gpodnet_logout_toast">Logout was successful</string>
+ <string name="synchronization_logout">Logout</string>
+ <string name="pref_synchronization_logout_toast">Logout was successful</string>
<string name="pref_gpodnet_setlogin_information_title">Change login information</string>
<string name="pref_gpodnet_setlogin_information_sum">Change the login information for your gpodder.net account.</string>
- <string name="pref_gpodnet_sync_changes_title">Synchronize now</string>
- <string name="pref_gpodnet_sync_changes_sum">Sync subscription and episode state changes with gpodder.net.</string>
- <string name="pref_gpodnet_full_sync_title">Force full synchronization</string>
- <string name="pref_gpodnet_full_sync_sum">Sync all subscriptions and episode states with gpodder.net.</string>
- <string name="pref_gpodnet_login_status"><![CDATA[Logged in as <i>%1$s</i> with device <i>%2$s</i>]]></string>
+ <string name="synchronization_sync_changes_title">Synchronize now</string>
+ <string name="synchronization_full_sync_title">Force full synchronization</string>
+ <string name="synchronization_login_status"><![CDATA[Logged in as <i>%1$s</i> on <i>%2$s</i>. <br/><br/>You can choose your synchronization provider again once you have logged out]]></string>
+ <string name="synchronization_summary_unchoosen">You can choose from multiple providers to synchronize your subscriptions and episode play state with</string>
+ <string name="synchronization_summary_nextcloud">Gpoddersync is an open-source Nextcloud app that you can easily install on your own server. The app is independent of the AntennaPod project.</string>
+ <string name="synchronization_nextcloud_authenticate_browser">Grant access using the opened web browser and come back to AntennaPod.</string>
+ <string name="synchronization_choose_title">Choose synchronization provider</string>
+ <string name="synchronization_force_sync_summary">Re-synchronize all subscriptions and episode states</string>
+ <string name="synchronization_sync_summary">Synchronize subscription and episode state changes</string>
+ <string name="dialog_choose_sync_service_title">Choose synchronization provider</string>
<string name="pref_playback_speed_sum">Customize the speeds available for variable speed playback</string>
<string name="pref_feed_playback_speed_sum">The speed to use when starting audio playback for episodes in this podcast</string>
<string name="pref_feed_skip">Auto Skip</string>
@@ -500,9 +503,6 @@
<string name="pref_proxy_title">Proxy</string>
<string name="pref_proxy_sum">Set a network proxy</string>
<string name="pref_no_browser_found">No web browser found.</string>
- <string name="pref_cast_title">Chromecast support</string>
- <string name="pref_cast_message_play_flavor">Enable support for remote media playback on Cast devices (such as Chromecast, Audio Speakers or Android TV)</string>
- <string name="pref_cast_message_free_flavor" tools:ignore="UnusedResources">Chromecast requires third party proprietary libraries that are disabled in this version of AntennaPod</string>
<string name="pref_enqueue_downloaded_title">Enqueue Downloaded</string>
<string name="pref_enqueue_downloaded_summary">Add downloaded episodes to the queue</string>
<string name="media_player_builtin">Built-in Android player (deprecated) </string>
@@ -540,6 +540,8 @@
<string name="new_episode_notification_disabled">Notification disabled</string>
<string name="pref_feed_settings_dialog_msg">This setting is unique to each podcast. You can change it by opening the podcast page.</string>
<string name="pref_contribute">Contribute</string>
+ <string name="pref_show_subscription_title">Show Subscription Title</string>
+ <string name="pref_show_subscription_title_summary">Display the subscription title below the cover image.</string>
<!-- About screen -->
<string name="about_pref">About</string>
@@ -565,6 +567,7 @@
<string name="sync_status_episodes_download">Downloading episode changes…</string>
<string name="sync_status_upload_played">Uploading played status…</string>
<string name="sync_status_subscriptions">Synchronizing subscriptions…</string>
+ <string name="sync_status_wait_for_downloads">Waiting for downloads to complete…</string>
<string name="sync_status_success">Synchronization successful</string>
<string name="sync_status_error">Synchronization failed</string>
@@ -594,7 +597,6 @@
<string name="export_success_title">Export successful</string>
<string name="export_success_sum">The exported file was written to:\n\n%1$s</string>
<string name="opml_import_ask_read_permission">Access to external storage is required to read the OPML file</string>
- <string name="import_select_file">Select file to import</string>
<string name="successful_import_label">Import successful</string>
<string name="import_ok">Please press OK to restart AntennaPod</string>
<string name="import_no_downgrade">This database was exported with a newer version of AntennaPod. Your current installation does not yet know how to handle this file.</string>
@@ -662,7 +664,6 @@
<string name="pref_pausePlaybackForFocusLoss_title">Pause for Interruptions</string>
<string name="pref_resumeAfterCall_sum">Resume playback after a phone call completes</string>
<string name="pref_resumeAfterCall_title">Resume after Call</string>
- <string name="pref_restart_required">AntennaPod has to be restarted for this change to take effect.</string>
<!-- Online feed view -->
<string name="subscribe_label">Subscribe</string>
@@ -671,6 +672,7 @@
<string name="stop_preview">Stop preview</string>
<!-- Content descriptions for image buttons -->
+ <string name="toolbar_back_button_content_description">Back</string>
<string name="rewind_label">Rewind</string>
<string name="fast_forward_label">Fast forward</string>
<string name="increase_speed">Increase speed</string>
@@ -689,21 +691,26 @@
<!-- Feed settings/information screen -->
<string name="authentication_label">Authentication</string>
<string name="authentication_descr">Change your username and password for this podcast and its episodes.</string>
- <string name="feed_folders_label">Folders</string>
- <string name="feed_folders_summary">Change the folders in which this podcast is displayed.</string>
+ <string name="feed_tags_label">Tags</string>
+ <string name="feed_tags_summary">Change the tags of this podcast to help organize your subscriptions</string>
<string name="feed_folders_include_root">Show in main list</string>
+ <string name="multi_feed_common_tags_info">{fa-info-circle} Only common tags from all selected subscriptions are shown. Other tags stay unaffected.</string>
<string name="auto_download_settings_label">Auto Download Settings</string>
<string name="episode_filters_label">Episode Filter</string>
<string name="episode_filters_description">List of terms used to decide if an episode should be included or excluded when auto downloading</string>
<string name="episode_filters_include">Include</string>
<string name="episode_filters_exclude">Exclude</string>
+ <string name="episode_filters_duration">Minimal Duration (in minutes)</string>
<string name="episode_filters_hint">Single words \n\"Multiple Words\"</string>
<string name="keep_updated">Keep Updated</string>
<string name="keep_updated_summary">Include this podcast when (auto-)refreshing all podcasts</string>
<string name="auto_download_disabled_globally">Auto download is disabled in the main AntennaPod settings</string>
- <string name="statistics_listened_for">Listened for:</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>
<string name="statistics_view_all">View for all podcasts »</string>
<!-- AntennaPodSP -->
@@ -800,21 +807,6 @@
<!-- Subscriptions fragment -->
<string name="subscription_num_columns">Number of columns</string>
- <!-- Casting -->
- <string name="cast_media_route_menu_title">Play on&#8230;</string>
- <string name="cast_disconnect_label">Disconnect the cast session</string>
- <string name="cast_not_castable">Media selected is not compatible with cast device</string>
- <string name="cast_failed_to_play">Failed to start the playback of media</string>
- <string name="cast_failed_to_stop">Failed to stop the playback of media</string>
- <string name="cast_failed_to_pause">Failed to pause the playback of media</string>
- <string name="cast_failed_setting_volume">Failed to set the volume</string>
- <string name="cast_failed_no_connection">No connection to the cast device is present</string>
- <string name="cast_failed_no_connection_trans">Connection to the cast device has been lost. Application is trying to re-establish the connection, if possible. Please wait for a few seconds and try again.</string>
- <string name="cast_failed_status_request">Failed to sync up with the cast device</string>
- <string name="cast_failed_seek">Failed to seek to the new position on the cast device</string>
- <string name="cast_failed_receiver_player_error">Receiver player has encountered a severe error</string>
- <string name="cast_failed_media_error_skipping">Error playing media. Skipping&#8230;</string>
-
<!-- Notification channels -->
<string name="notification_group_errors">Errors</string>
<string name="notification_group_news">News</string>
@@ -842,4 +834,8 @@
<string name="on_demand_config_setting_changed">Setting updated successfully.</string>
<string name="on_demand_config_stream_text">Looks like you stream a lot. Do you want episode lists to show stream buttons?</string>
<string name="on_demand_config_download_text">Looks like you download a lot. Do you want episode lists to show download buttons?</string>
+
+ <string name="shortcut_subscription_label">Subscription shortcut</string>
+ <string name="shortcut_select_subscription">Select subscription</string>
+ <string name="add_shortcut">Add Shortcut</string>
</resources>
diff --git a/core/src/play/java/de/danoeh/antennapod/core/CastCallbacks.java b/core/src/play/java/de/danoeh/antennapod/core/CastCallbacks.java
deleted file mode 100644
index 27f985a4c..000000000
--- a/core/src/play/java/de/danoeh/antennapod/core/CastCallbacks.java
+++ /dev/null
@@ -1,12 +0,0 @@
-package de.danoeh.antennapod.core;
-
-import androidx.annotation.Nullable;
-import androidx.mediarouter.app.MediaRouteDialogFactory;
-
-/**
- * Callbacks for Chromecast support on the core module
- */
-public interface CastCallbacks {
-
- @Nullable MediaRouteDialogFactory getMediaRouterDialogFactory();
-}
diff --git a/core/src/play/java/de/danoeh/antennapod/core/ClientConfig.java b/core/src/play/java/de/danoeh/antennapod/core/ClientConfig.java
deleted file mode 100644
index 48de7c6e1..000000000
--- a/core/src/play/java/de/danoeh/antennapod/core/ClientConfig.java
+++ /dev/null
@@ -1,64 +0,0 @@
-package de.danoeh.antennapod.core;
-
-import android.content.Context;
-import android.util.Log;
-import de.danoeh.antennapod.core.cast.CastManager;
-import de.danoeh.antennapod.core.preferences.PlaybackPreferences;
-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.net.ssl.SslProviderInstaller;
-
-import java.io.File;
-
-/**
- * Stores callbacks for core classes like Services, DB classes etc. and other configuration variables.
- * Apps using the core module of AntennaPod should register implementations of all interfaces here.
- */
-public class ClientConfig {
- private static final String TAG = "ClientConfig";
-
- private ClientConfig(){}
-
- /**
- * Should be used when setting User-Agent header for HTTP-requests.
- */
- public static String USER_AGENT;
-
- public static ApplicationCallbacks applicationCallbacks;
-
- public static DownloadServiceCallbacks downloadServiceCallbacks;
-
- public static CastCallbacks castCallbacks;
-
- private static boolean initialized = false;
-
- public static synchronized void initialize(Context context) {
- if (initialized) {
- return;
- }
- PodDBAdapter.init(context);
- UserPreferences.init(context);
- UsageStatistics.init(context);
- PlaybackPreferences.init(context);
- SslProviderInstaller.install(context);
- NetworkUtils.init(context);
- // Don't initialize Cast-related logic unless it is enabled, to avoid the unnecessary
- // Google Play Service usage.
- // Down side: when the user decides to enable casting, AntennaPod needs to be restarted
- // for it to take effect.
- if (UserPreferences.isCastEnabled()) {
- CastManager.init(context);
- } else {
- Log.v(TAG, "Cast is disabled. All Cast-related initialization will be skipped.");
- }
- AntennapodHttpClient.setCacheDirectory(new File(context.getCacheDir(), "okhttp"));
- SleepTimerPreferences.init(context);
- NotificationUtils.createChannels(context);
- initialized = true;
- }
-}
diff --git a/core/src/play/java/de/danoeh/antennapod/core/cast/CastButtonVisibilityManager.java b/core/src/play/java/de/danoeh/antennapod/core/cast/CastButtonVisibilityManager.java
deleted file mode 100644
index 8d0e40116..000000000
--- a/core/src/play/java/de/danoeh/antennapod/core/cast/CastButtonVisibilityManager.java
+++ /dev/null
@@ -1,120 +0,0 @@
-package de.danoeh.antennapod.core.cast;
-
-import android.util.Log;
-import android.view.Menu;
-import android.view.MenuItem;
-
-import de.danoeh.antennapod.core.R;
-
-public class CastButtonVisibilityManager {
- private static final String TAG = "CastBtnVisibilityMgr";
- private final CastManager castManager;
- private volatile boolean prefEnabled = false;
- private volatile boolean viewRequested = false;
- private volatile boolean resumed = false;
- private volatile boolean connected = false;
- private volatile int showAsAction = MenuItem.SHOW_AS_ACTION_IF_ROOM;
- private Menu menu;
- public SwitchableMediaRouteActionProvider mediaRouteActionProvider;
-
- public CastButtonVisibilityManager(CastManager castManager) {
- this.castManager = castManager;
- }
-
- public synchronized void setPrefEnabled(boolean newValue) {
- if (prefEnabled != newValue && resumed && (viewRequested || connected)) {
- if (newValue) {
- castManager.incrementUiCounter();
- } else {
- castManager.decrementUiCounter();
- }
- }
- prefEnabled = newValue;
- if (mediaRouteActionProvider != null) {
- mediaRouteActionProvider.setEnabled(prefEnabled && (viewRequested || connected));
- }
- }
-
- public synchronized void setResumed(boolean newValue) {
- if (resumed == newValue) {
- Log.e(TAG, "resumed should never change to the same value");
- return;
- }
- resumed = newValue;
- if (prefEnabled && (viewRequested || connected)) {
- if (resumed) {
- castManager.incrementUiCounter();
- } else {
- castManager.decrementUiCounter();
- }
- }
- }
-
- public synchronized void setViewRequested(boolean newValue) {
- if (viewRequested != newValue && resumed && prefEnabled && !connected) {
- if (newValue) {
- castManager.incrementUiCounter();
- } else {
- castManager.decrementUiCounter();
- }
- }
- viewRequested = newValue;
- if (mediaRouteActionProvider != null) {
- mediaRouteActionProvider.setEnabled(prefEnabled && (viewRequested || connected));
- }
- }
-
- public synchronized void setConnected(boolean newValue) {
- if (connected != newValue && resumed && prefEnabled && !prefEnabled) {
- if (newValue) {
- castManager.incrementUiCounter();
- } else {
- castManager.decrementUiCounter();
- }
- }
- connected = newValue;
- if (mediaRouteActionProvider != null) {
- mediaRouteActionProvider.setEnabled(prefEnabled && (viewRequested || connected));
- }
- }
-
- public synchronized boolean shouldEnable() {
- return prefEnabled && viewRequested;
- }
-
- public void setMenu(Menu menu) {
- setViewRequested(false);
- showAsAction = MenuItem.SHOW_AS_ACTION_IF_ROOM;
- this.menu = menu;
- setShowAsAction();
- }
-
- public void requestCastButton(int showAsAction) {
- setViewRequested(true);
- this.showAsAction = showAsAction;
- setShowAsAction();
- }
-
- public void onConnected() {
- setConnected(true);
- setShowAsAction();
- }
-
- public void onDisconnected() {
- setConnected(false);
- setShowAsAction();
- }
-
- private void setShowAsAction() {
- if (menu == null) {
- Log.d(TAG, "setShowAsAction() without a menu");
- return;
- }
- MenuItem item = menu.findItem(R.id.media_route_menu_item);
- if (item == null) {
- Log.e(TAG, "setShowAsAction(), but cast button not inflated");
- return;
- }
- item.setShowAsAction(connected ? MenuItem.SHOW_AS_ACTION_ALWAYS : showAsAction);
- }
-}
diff --git a/core/src/play/java/de/danoeh/antennapod/core/cast/CastConsumer.java b/core/src/play/java/de/danoeh/antennapod/core/cast/CastConsumer.java
deleted file mode 100644
index 213dd1875..000000000
--- a/core/src/play/java/de/danoeh/antennapod/core/cast/CastConsumer.java
+++ /dev/null
@@ -1,11 +0,0 @@
-package de.danoeh.antennapod.core.cast;
-
-import com.google.android.libraries.cast.companionlibrary.cast.callbacks.VideoCastConsumer;
-
-public interface CastConsumer extends VideoCastConsumer{
-
- /**
- * Called when the stream's volume is changed.
- */
- void onStreamVolumeChanged(double value, boolean isMute);
-}
diff --git a/core/src/play/java/de/danoeh/antennapod/core/cast/CastManager.java b/core/src/play/java/de/danoeh/antennapod/core/cast/CastManager.java
deleted file mode 100644
index dd07b9cd8..000000000
--- a/core/src/play/java/de/danoeh/antennapod/core/cast/CastManager.java
+++ /dev/null
@@ -1,1091 +0,0 @@
-/*
- * Copyright (C) 2015 Google Inc. All Rights Reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- * ------------------------------------------------------------------------
- *
- * Changes made by Domingos Lopes <domingos86lopes@gmail.com>
- *
- * original can be found at http://www.github.com/googlecast/CastCompanionLibrary-android
- */
-
-package de.danoeh.antennapod.core.cast;
-
-import android.content.Context;
-import androidx.annotation.NonNull;
-import androidx.core.view.ActionProvider;
-import androidx.core.view.MenuItemCompat;
-import androidx.mediarouter.media.MediaRouter;
-import android.util.Log;
-import android.view.MenuItem;
-
-import com.google.android.gms.cast.ApplicationMetadata;
-import com.google.android.gms.cast.Cast;
-import com.google.android.gms.cast.CastDevice;
-import com.google.android.gms.cast.CastMediaControlIntent;
-import com.google.android.gms.cast.CastStatusCodes;
-import com.google.android.gms.cast.MediaInfo;
-import com.google.android.gms.cast.MediaMetadata;
-import com.google.android.gms.cast.MediaQueueItem;
-import com.google.android.gms.cast.MediaStatus;
-import com.google.android.gms.cast.RemoteMediaPlayer;
-import com.google.android.gms.common.ConnectionResult;
-import com.google.android.gms.common.GoogleApiAvailability;
-import com.google.android.libraries.cast.companionlibrary.cast.BaseCastManager;
-import com.google.android.libraries.cast.companionlibrary.cast.CastConfiguration;
-import com.google.android.libraries.cast.companionlibrary.cast.exceptions.CastException;
-import com.google.android.libraries.cast.companionlibrary.cast.exceptions.NoConnectionException;
-import com.google.android.libraries.cast.companionlibrary.cast.exceptions.OnFailedListener;
-import com.google.android.libraries.cast.companionlibrary.cast.exceptions.TransientNetworkDisconnectionException;
-
-import org.json.JSONObject;
-
-import java.io.IOException;
-import java.util.List;
-import java.util.Locale;
-import java.util.Set;
-import java.util.concurrent.CopyOnWriteArraySet;
-
-import de.danoeh.antennapod.core.ClientConfig;
-import de.danoeh.antennapod.core.R;
-
-import static com.google.android.gms.cast.RemoteMediaPlayer.RESUME_STATE_PLAY;
-import static com.google.android.gms.cast.RemoteMediaPlayer.RESUME_STATE_UNCHANGED;
-
-/**
- * A subclass of {@link BaseCastManager} that is suitable for casting video contents (it
- * also provides a single custom data channel/namespace if an out-of-band communication is
- * needed).
- * <p>
- * Clients need to initialize this class by calling
- * {@link #init(android.content.Context)} in the Application's
- * {@code onCreate()} method. To access the (singleton) instance of this class, clients
- * need to call {@link #getInstance()}.
- * <p>This
- * class manages various states of the remote cast device. Client applications, however, can
- * complement the default behavior of this class by hooking into various callbacks that it provides
- * (see {@link CastConsumer}).
- * Since the number of these callbacks is usually much larger than what a single application might
- * be interested in, there is a no-op implementation of this interface (see
- * {@link DefaultCastConsumer}) that applications can subclass to override only those methods that
- * they are interested in. Since this library depends on the cast functionalities provided by the
- * Google Play services, the library checks to ensure that the right version of that service is
- * installed. It also provides a simple static method {@code checkGooglePlayServices()} that clients
- * can call at an early stage of their applications to provide a dialog for users if they need to
- * update/activate their Google Play Services library.
- *
- * @see CastConfiguration
- */
-public class CastManager extends BaseCastManager implements OnFailedListener {
- public static final String TAG = "CastManager";
-
- public static final String CAST_APP_ID = CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID;
-
- private MediaStatus mediaStatus;
- private static CastManager INSTANCE;
- private RemoteMediaPlayer remoteMediaPlayer;
- private int state = MediaStatus.PLAYER_STATE_IDLE;
- private final Set<CastConsumer> castConsumers = new CopyOnWriteArraySet<>();
-
- public static final int QUEUE_OPERATION_LOAD = 1;
- public static final int QUEUE_OPERATION_APPEND = 9;
-
- private CastManager(Context context, CastConfiguration castConfiguration) {
- super(context, castConfiguration);
- Log.d(TAG, "CastManager is instantiated");
- }
-
- public static synchronized CastManager init(Context context) {
- if (INSTANCE == null) {
- CastConfiguration castConfiguration = new CastConfiguration.Builder(CAST_APP_ID)
- .enableDebug()
- .enableAutoReconnect()
- .enableWifiReconnection()
- .setLaunchOptions(true, Locale.getDefault())
- .setMediaRouteDialogFactory(ClientConfig.castCallbacks.getMediaRouterDialogFactory())
- .build();
- Log.d(TAG, "New instance of CastManager is created");
- if (ConnectionResult.SUCCESS != GoogleApiAvailability.getInstance()
- .isGooglePlayServicesAvailable(context)) {
- Log.e(TAG, "Couldn't find the appropriate version of Google Play Services");
- }
- INSTANCE = new CastManager(context, castConfiguration);
- }
- return INSTANCE;
- }
-
- /**
- * Returns a (singleton) instance of this class. Clients should call this method in order to
- * get a hold of this singleton instance, only after it is initialized. If it is not initialized
- * yet, an {@link IllegalStateException} will be thrown.
- *
- */
- public static CastManager getInstance() {
- if (INSTANCE == null) {
- String msg = "No CastManager instance was found, did you forget to initialize it?";
- Log.e(TAG, msg);
- throw new IllegalStateException(msg);
- }
- return INSTANCE;
- }
-
- public static boolean isInitialized() {
- return INSTANCE != null;
- }
-
- /**
- * Returns the active {@link RemoteMediaPlayer} instance. Since there are a number of media
- * control APIs that this library do not provide a wrapper for, client applications can call
- * those methods directly after obtaining an instance of the active {@link RemoteMediaPlayer}.
- */
- public final RemoteMediaPlayer getRemoteMediaPlayer() {
- return remoteMediaPlayer;
- }
-
- /*
- * A simple check to make sure remoteMediaPlayer is not null
- */
- private void checkRemoteMediaPlayerAvailable() throws NoConnectionException {
- if (remoteMediaPlayer == null) {
- throw new NoConnectionException();
- }
- }
-
- /**
- * Indicates if the remote media is currently playing (or buffering).
- *
- * @throws NoConnectionException
- * @throws TransientNetworkDisconnectionException
- */
- public boolean isRemoteMediaPlaying() throws TransientNetworkDisconnectionException,
- NoConnectionException {
- checkConnectivity();
- return state == MediaStatus.PLAYER_STATE_BUFFERING
- || state == MediaStatus.PLAYER_STATE_PLAYING;
- }
-
- /**
- * Returns <code>true</code> if the remote connected device is playing a movie.
- *
- * @throws NoConnectionException
- * @throws TransientNetworkDisconnectionException
- */
- public boolean isRemoteMediaPaused() throws TransientNetworkDisconnectionException,
- NoConnectionException {
- checkConnectivity();
- return state == MediaStatus.PLAYER_STATE_PAUSED;
- }
-
- /**
- * Returns <code>true</code> only if there is a media on the remote being played, paused or
- * buffered.
- *
- * @throws NoConnectionException
- * @throws TransientNetworkDisconnectionException
- */
- public boolean isRemoteMediaLoaded() throws TransientNetworkDisconnectionException,
- NoConnectionException {
- checkConnectivity();
- return isRemoteMediaPaused() || isRemoteMediaPlaying();
- }
-
- /**
- * Gets the remote's system volume. It internally detects what type of volume is used.
- *
- * @throws NoConnectionException If no connectivity to the device exists
- * @throws TransientNetworkDisconnectionException If framework is still trying to recover from
- * a possibly transient loss of network
- */
- public double getStreamVolume() throws TransientNetworkDisconnectionException, NoConnectionException {
- checkConnectivity();
- checkRemoteMediaPlayerAvailable();
- return remoteMediaPlayer.getMediaStatus().getStreamVolume();
- }
-
- /**
- * Sets the stream volume.
- *
- * @param volume Should be a value between 0 and 1, inclusive.
- * @throws NoConnectionException
- * @throws TransientNetworkDisconnectionException
- * @throws CastException If setting system volume fails
- */
- public void setStreamVolume(double volume) throws CastException,
- TransientNetworkDisconnectionException, NoConnectionException {
- checkConnectivity();
- if (volume > 1.0) {
- volume = 1.0;
- } else if (volume < 0) {
- volume = 0.0;
- }
-
- RemoteMediaPlayer mediaPlayer = getRemoteMediaPlayer();
- if (mediaPlayer == null) {
- throw new NoConnectionException();
- }
- mediaPlayer.setStreamVolume(mApiClient, volume).setResultCallback(
- (result) -> {
- if (!result.getStatus().isSuccess()) {
- onFailed(R.string.cast_failed_setting_volume,
- result.getStatus().getStatusCode());
- } else {
- CastManager.this.onStreamVolumeChanged();
- }
- });
- }
-
- /**
- * Returns <code>true</code> if remote Stream is muted.
- *
- * @throws NoConnectionException
- * @throws TransientNetworkDisconnectionException
- */
- public boolean isStreamMute() throws TransientNetworkDisconnectionException, NoConnectionException {
- checkConnectivity();
- checkRemoteMediaPlayerAvailable();
- return remoteMediaPlayer.getMediaStatus().isMute();
- }
-
- /**
- * Returns the duration of the media that is loaded, in milliseconds.
- *
- * @throws NoConnectionException
- * @throws TransientNetworkDisconnectionException
- */
- public long getMediaDuration() throws TransientNetworkDisconnectionException,
- NoConnectionException {
- checkConnectivity();
- checkRemoteMediaPlayerAvailable();
- return remoteMediaPlayer.getStreamDuration();
- }
-
- /**
- * Returns the current (approximate) position of the current media, in milliseconds.
- *
- * @throws NoConnectionException
- * @throws TransientNetworkDisconnectionException
- */
- public long getCurrentMediaPosition() throws TransientNetworkDisconnectionException,
- NoConnectionException {
- checkConnectivity();
- checkRemoteMediaPlayerAvailable();
- return remoteMediaPlayer.getApproximateStreamPosition();
- }
-
- public int getApplicationStandbyState() throws IllegalStateException {
- Log.d(TAG, "getApplicationStandbyState()");
- return Cast.CastApi.getStandbyState(mApiClient);
- }
-
- private void onApplicationDisconnected(int errorCode) {
- Log.d(TAG, "onApplicationDisconnected() reached with error code: " + errorCode);
- mApplicationErrorCode = errorCode;
- for (CastConsumer consumer : castConsumers) {
- consumer.onApplicationDisconnected(errorCode);
- }
- if (mMediaRouter != null) {
- Log.d(TAG, "onApplicationDisconnected(): Cached RouteInfo: " + getRouteInfo());
- Log.d(TAG, "onApplicationDisconnected(): Selected RouteInfo: "
- + mMediaRouter.getSelectedRoute());
- if (getRouteInfo() == null || mMediaRouter.getSelectedRoute().equals(getRouteInfo())) {
- Log.d(TAG, "onApplicationDisconnected(): Setting route to default");
- mMediaRouter.selectRoute(mMediaRouter.getDefaultRoute());
- }
- }
- onDeviceSelected(null /* CastDevice */, null /* RouteInfo */);
- }
-
- private void onApplicationStatusChanged() {
- if (!isConnected()) {
- return;
- }
- try {
- String appStatus = Cast.CastApi.getApplicationStatus(mApiClient);
- Log.d(TAG, "onApplicationStatusChanged() reached: " + appStatus);
- for (CastConsumer consumer : castConsumers) {
- consumer.onApplicationStatusChanged(appStatus);
- }
- } catch (IllegalStateException e) {
- Log.e(TAG, "onApplicationStatusChanged()", e);
- }
- }
-
- private void onDeviceVolumeChanged() {
- Log.d(TAG, "onDeviceVolumeChanged() reached");
- double volume;
- try {
- volume = getDeviceVolume();
- boolean isMute = isDeviceMute();
- for (CastConsumer consumer : castConsumers) {
- consumer.onVolumeChanged(volume, isMute);
- }
- } catch (TransientNetworkDisconnectionException | NoConnectionException e) {
- Log.e(TAG, "Failed to get volume", e);
- }
-
- }
-
- private void onStreamVolumeChanged() {
- Log.d(TAG, "onStreamVolumeChanged() reached");
- double volume;
- try {
- volume = getStreamVolume();
- boolean isMute = isStreamMute();
- for (CastConsumer consumer : castConsumers) {
- consumer.onStreamVolumeChanged(volume, isMute);
- }
- } catch (TransientNetworkDisconnectionException | NoConnectionException e) {
- Log.e(TAG, "Failed to get volume", e);
- }
- }
-
- @Override
- protected void onApplicationConnected(ApplicationMetadata appMetadata,
- String applicationStatus, String sessionId, boolean wasLaunched) {
- Log.d(TAG, "onApplicationConnected() reached with sessionId: " + sessionId
- + ", and mReconnectionStatus=" + mReconnectionStatus);
- mApplicationErrorCode = NO_APPLICATION_ERROR;
- if (mReconnectionStatus == RECONNECTION_STATUS_IN_PROGRESS) {
- // we have tried to reconnect and successfully launched the app, so
- // it is time to select the route and make the cast icon happy :-)
- List<MediaRouter.RouteInfo> routes = mMediaRouter.getRoutes();
- if (routes != null) {
- String routeId = mPreferenceAccessor.getStringFromPreference(PREFS_KEY_ROUTE_ID);
- for (MediaRouter.RouteInfo routeInfo : routes) {
- if (routeId.equals(routeInfo.getId())) {
- // found the right route
- Log.d(TAG, "Found the correct route during reconnection attempt");
- mReconnectionStatus = RECONNECTION_STATUS_FINALIZED;
- mMediaRouter.selectRoute(routeInfo);
- break;
- }
- }
- }
- }
- try {
- //attachDataChannel();
- attachMediaChannel();
- mSessionId = sessionId;
- // saving device for future retrieval; we only save the last session info
- mPreferenceAccessor.saveStringToPreference(PREFS_KEY_SESSION_ID, mSessionId);
- remoteMediaPlayer.requestStatus(mApiClient).setResultCallback(result -> {
- if (!result.getStatus().isSuccess()) {
- onFailed(R.string.cast_failed_status_request,
- result.getStatus().getStatusCode());
- }
- });
- for (CastConsumer consumer : castConsumers) {
- consumer.onApplicationConnected(appMetadata, mSessionId, wasLaunched);
- }
- } catch (TransientNetworkDisconnectionException e) {
- Log.e(TAG, "Failed to attach media/data channel due to network issues", e);
- onFailed(R.string.cast_failed_no_connection_trans, NO_STATUS_CODE);
- } catch (NoConnectionException e) {
- Log.e(TAG, "Failed to attach media/data channel due to network issues", e);
- onFailed(R.string.cast_failed_no_connection, NO_STATUS_CODE);
- }
- }
-
- /*
- * (non-Javadoc)
- * @see com.google.android.libraries.cast.companionlibrary.cast.BaseCastManager
- * #onConnectivityRecovered()
- */
- @Override
- public void onConnectivityRecovered() {
- reattachMediaChannel();
- //reattachDataChannel();
- super.onConnectivityRecovered();
- }
-
- /*
- * (non-Javadoc)
- * @see com.google.android.gms.cast.CastClient.Listener#onApplicationStopFailed (int)
- */
- @Override
- public void onApplicationStopFailed(int errorCode) {
- for (CastConsumer consumer : castConsumers) {
- consumer.onApplicationStopFailed(errorCode);
- }
- }
-
- @Override
- public void onApplicationConnectionFailed(int errorCode) {
- Log.d(TAG, "onApplicationConnectionFailed() reached with errorCode: " + errorCode);
- mApplicationErrorCode = errorCode;
- if (mReconnectionStatus == RECONNECTION_STATUS_IN_PROGRESS) {
- if (errorCode == CastStatusCodes.APPLICATION_NOT_RUNNING) {
- // while trying to re-establish session, we found out that the app is not running
- // so we need to disconnect
- mReconnectionStatus = RECONNECTION_STATUS_INACTIVE;
- onDeviceSelected(null /* CastDevice */, null /* RouteInfo */);
- }
- } else {
- for (CastConsumer consumer : castConsumers) {
- consumer.onApplicationConnectionFailed(errorCode);
- }
- onDeviceSelected(null /* CastDevice */, null /* RouteInfo */);
- if (mMediaRouter != null) {
- Log.d(TAG, "onApplicationConnectionFailed(): Setting route to default");
- mMediaRouter.selectRoute(mMediaRouter.getDefaultRoute());
- }
- }
- }
-
- /**
- * Loads a media. For this to succeed, you need to have successfully launched the application.
- *
- * @param media The media to be loaded
- * @param autoPlay If <code>true</code>, playback starts after load
- * @param position Where to start the playback (only used if autoPlay is <code>true</code>.
- * Units is milliseconds.
- * @throws NoConnectionException
- * @throws TransientNetworkDisconnectionException
- */
- public void loadMedia(MediaInfo media, boolean autoPlay, int position)
- throws TransientNetworkDisconnectionException, NoConnectionException {
- loadMedia(media, autoPlay, position, null);
- }
-
- /**
- * Loads a media. For this to succeed, you need to have successfully launched the application.
- *
- * @param media The media to be loaded
- * @param autoPlay If <code>true</code>, playback starts after load
- * @param position Where to start the playback (only used if autoPlay is <code>true</code>).
- * Units is milliseconds.
- * @param customData Optional {@link JSONObject} data to be passed to the cast device
- * @throws NoConnectionException
- * @throws TransientNetworkDisconnectionException
- */
- public void loadMedia(MediaInfo media, boolean autoPlay, int position, JSONObject customData)
- throws TransientNetworkDisconnectionException, NoConnectionException {
- loadMedia(media, null, autoPlay, position, customData);
- }
-
- /**
- * Loads a media. For this to succeed, you need to have successfully launched the application.
- *
- * @param media The media to be loaded
- * @param activeTracks An array containing the list of track IDs to be set active for this
- * media upon a successful load
- * @param autoPlay If <code>true</code>, playback starts after load
- * @param position Where to start the playback (only used if autoPlay is <code>true</code>).
- * Units is milliseconds.
- * @param customData Optional {@link JSONObject} data to be passed to the cast device
- * @throws NoConnectionException
- * @throws TransientNetworkDisconnectionException
- */
- public void loadMedia(MediaInfo media, final long[] activeTracks, boolean autoPlay,
- int position, JSONObject customData)
- throws TransientNetworkDisconnectionException, NoConnectionException {
- Log.d(TAG, "loadMedia");
- checkConnectivity();
- if (media == null) {
- return;
- }
- if (remoteMediaPlayer == null) {
- Log.e(TAG, "Trying to load a video with no active media session");
- throw new NoConnectionException();
- }
-
- Log.d(TAG, "remoteMediaPlayer.load() with media=" + media.getMetadata().getString(MediaMetadata.KEY_TITLE)
- + ", position=" + position + ", autoplay=" + autoPlay);
- remoteMediaPlayer.load(mApiClient, media, autoPlay, position, activeTracks, customData)
- .setResultCallback(result -> {
- for (CastConsumer consumer : castConsumers) {
- consumer.onMediaLoadResult(result.getStatus().getStatusCode());
- }
- });
- }
-
- /**
- * Loads and optionally starts playback of a new queue of media items.
- *
- * @param items Array of items to load, in the order that they should be played. Must not be
- * {@code null} or empty.
- * @param startIndex The array index of the item in the {@code items} array that should be
- * played first (i.e., it will become the currentItem).If {@code repeatMode}
- * is {@link MediaStatus#REPEAT_MODE_REPEAT_OFF} playback will end when the
- * last item in the array is played.
- * <p>
- * This may be useful for continuation scenarios where the user was already
- * using the sender application and in the middle decides to cast. This lets
- * the sender application avoid mapping between the local and remote queue
- * positions and/or avoid issuing an extra request to update the queue.
- * <p>
- * This value must be less than the length of {@code items}.
- * @param repeatMode The repeat playback mode for the queue. One of
- * {@link MediaStatus#REPEAT_MODE_REPEAT_OFF},
- * {@link MediaStatus#REPEAT_MODE_REPEAT_ALL},
- * {@link MediaStatus#REPEAT_MODE_REPEAT_SINGLE} and
- * {@link MediaStatus#REPEAT_MODE_REPEAT_ALL_AND_SHUFFLE}.
- * @param customData Custom application-specific data to pass along with the request, may be
- * {@code null}.
- * @throws TransientNetworkDisconnectionException
- * @throws NoConnectionException
- */
- public void queueLoad(final MediaQueueItem[] items, final int startIndex, final int repeatMode,
- final JSONObject customData)
- throws TransientNetworkDisconnectionException, NoConnectionException {
- Log.d(TAG, "queueLoad");
- checkConnectivity();
- if (items == null || items.length == 0) {
- return;
- }
- if (remoteMediaPlayer == null) {
- Log.e(TAG, "Trying to queue one or more videos with no active media session");
- throw new NoConnectionException();
- }
- Log.d(TAG, "remoteMediaPlayer.queueLoad() with " + items.length + "items, starting at "
- + startIndex);
- remoteMediaPlayer
- .queueLoad(mApiClient, items, startIndex, repeatMode, customData)
- .setResultCallback(result -> {
- for (CastConsumer consumer : castConsumers) {
- consumer.onMediaQueueOperationResult(QUEUE_OPERATION_LOAD,
- result.getStatus().getStatusCode());
- }
- });
- }
-
- /**
- * Plays the loaded media.
- *
- * @param position Where to start the playback. Units is milliseconds.
- * @throws NoConnectionException
- * @throws TransientNetworkDisconnectionException
- */
- public void play(int position) throws TransientNetworkDisconnectionException,
- NoConnectionException {
- checkConnectivity();
- Log.d(TAG, "attempting to play media at position " + position + " seconds");
- if (remoteMediaPlayer == null) {
- Log.e(TAG, "Trying to play a video with no active media session");
- throw new NoConnectionException();
- }
- seekAndPlay(position);
- }
-
- /**
- * Resumes the playback from where it was left (can be the beginning).
- *
- * @param customData Optional {@link JSONObject} data to be passed to the cast device
- * @throws NoConnectionException
- * @throws TransientNetworkDisconnectionException
- */
- public void play(JSONObject customData) throws
- TransientNetworkDisconnectionException, NoConnectionException {
- Log.d(TAG, "play(customData)");
- checkConnectivity();
- if (remoteMediaPlayer == null) {
- Log.e(TAG, "Trying to play a video with no active media session");
- throw new NoConnectionException();
- }
- remoteMediaPlayer.play(mApiClient, customData)
- .setResultCallback(result -> {
- if (!result.getStatus().isSuccess()) {
- onFailed(R.string.cast_failed_to_play,
- result.getStatus().getStatusCode());
- }
- });
- }
-
- /**
- * Resumes the playback from where it was left (can be the beginning).
- *
- * @throws CastException
- * @throws NoConnectionException
- * @throws TransientNetworkDisconnectionException
- */
- public void play() throws CastException, TransientNetworkDisconnectionException,
- NoConnectionException {
- play(null);
- }
-
- /**
- * Stops the playback of media/stream
- *
- * @param customData Optional {@link JSONObject}
- * @throws TransientNetworkDisconnectionException
- * @throws NoConnectionException
- */
- public void stop(JSONObject customData) throws
- TransientNetworkDisconnectionException, NoConnectionException {
- Log.d(TAG, "stop()");
- checkConnectivity();
- if (remoteMediaPlayer == null) {
- Log.e(TAG, "Trying to stop a stream with no active media session");
- throw new NoConnectionException();
- }
- remoteMediaPlayer.stop(mApiClient, customData).setResultCallback(
- result -> {
- if (!result.getStatus().isSuccess()) {
- onFailed(R.string.cast_failed_to_stop,
- result.getStatus().getStatusCode());
- }
- }
- );
- }
-
- /**
- * Stops the playback of media/stream
- *
- * @throws CastException
- * @throws TransientNetworkDisconnectionException
- * @throws NoConnectionException
- */
- public void stop() throws CastException,
- TransientNetworkDisconnectionException, NoConnectionException {
- stop(null);
- }
-
- /**
- * Pauses the playback.
- *
- * @throws CastException
- * @throws NoConnectionException
- * @throws TransientNetworkDisconnectionException
- */
- public void pause() throws CastException, TransientNetworkDisconnectionException,
- NoConnectionException {
- pause(null);
- }
-
- /**
- * Pauses the playback.
- *
- * @param customData Optional {@link JSONObject} data to be passed to the cast device
- * @throws NoConnectionException
- * @throws TransientNetworkDisconnectionException
- */
- public void pause(JSONObject customData) throws
- TransientNetworkDisconnectionException, NoConnectionException {
- Log.d(TAG, "attempting to pause media");
- checkConnectivity();
- if (remoteMediaPlayer == null) {
- Log.e(TAG, "Trying to pause a video with no active media session");
- throw new NoConnectionException();
- }
- remoteMediaPlayer.pause(mApiClient, customData)
- .setResultCallback(result -> {
- if (!result.getStatus().isSuccess()) {
- onFailed(R.string.cast_failed_to_pause,
- result.getStatus().getStatusCode());
- }
- });
- }
-
- /**
- * Seeks to the given point without changing the state of the player, i.e. after seek is
- * completed, it resumes what it was doing before the start of seek.
- *
- * @param position in milliseconds
- * @throws NoConnectionException
- * @throws TransientNetworkDisconnectionException
- */
- public void seek(int position) throws TransientNetworkDisconnectionException,
- NoConnectionException {
- Log.d(TAG, "attempting to seek media");
- checkConnectivity();
- if (remoteMediaPlayer == null) {
- Log.e(TAG, "Trying to seek a video with no active media session");
- throw new NoConnectionException();
- }
- Log.d(TAG, "remoteMediaPlayer.seek() to position " + position);
- remoteMediaPlayer.seek(mApiClient, position, RESUME_STATE_UNCHANGED).setResultCallback(result -> {
- if (!result.getStatus().isSuccess()) {
- onFailed(R.string.cast_failed_seek, result.getStatus().getStatusCode());
- }
- });
- }
-
- /**
- * Fast forwards the media by the given amount. If {@code lengthInMillis} is negative, it
- * rewinds the media.
- *
- * @param lengthInMillis The amount to fast forward the media, given in milliseconds
- * @throws TransientNetworkDisconnectionException
- * @throws NoConnectionException
- */
- public void forward(int lengthInMillis) throws TransientNetworkDisconnectionException,
- NoConnectionException {
- Log.d(TAG, "forward(): attempting to forward media by " + lengthInMillis);
- checkConnectivity();
- if (remoteMediaPlayer == null) {
- Log.e(TAG, "Trying to seek a video with no active media session");
- throw new NoConnectionException();
- }
- long position = remoteMediaPlayer.getApproximateStreamPosition() + lengthInMillis;
- seek((int) position);
- }
-
- /**
- * Seeks to the given point and starts playback regardless of the starting state.
- *
- * @param position in milliseconds
- * @throws NoConnectionException
- * @throws TransientNetworkDisconnectionException
- */
- public void seekAndPlay(int position) throws TransientNetworkDisconnectionException,
- NoConnectionException {
- Log.d(TAG, "attempting to seek media");
- checkConnectivity();
- if (remoteMediaPlayer == null) {
- Log.e(TAG, "Trying to seekAndPlay a video with no active media session");
- throw new NoConnectionException();
- }
- Log.d(TAG, "remoteMediaPlayer.seek() to position " + position + "and play");
- remoteMediaPlayer.seek(mApiClient, position, RESUME_STATE_PLAY)
- .setResultCallback(result -> {
- if (!result.getStatus().isSuccess()) {
- onFailed(R.string.cast_failed_seek, result.getStatus().getStatusCode());
- }
- });
- }
-
- private void attachMediaChannel() throws TransientNetworkDisconnectionException,
- NoConnectionException {
- Log.d(TAG, "attachMediaChannel()");
- checkConnectivity();
- if (remoteMediaPlayer == null) {
- remoteMediaPlayer = new RemoteMediaPlayer();
-
- remoteMediaPlayer.setOnStatusUpdatedListener(
- () -> {
- Log.d(TAG, "RemoteMediaPlayer::onStatusUpdated() is reached");
- CastManager.this.onRemoteMediaPlayerStatusUpdated();
- }
- );
-
- remoteMediaPlayer.setOnPreloadStatusUpdatedListener(
- () -> {
- Log.d(TAG, "RemoteMediaPlayer::onPreloadStatusUpdated() is reached");
- CastManager.this.onRemoteMediaPreloadStatusUpdated();
- });
-
-
- remoteMediaPlayer.setOnMetadataUpdatedListener(
- () -> {
- Log.d(TAG, "RemoteMediaPlayer::onMetadataUpdated() is reached");
- CastManager.this.onRemoteMediaPlayerMetadataUpdated();
- }
- );
-
- remoteMediaPlayer.setOnQueueStatusUpdatedListener(
- () -> {
- Log.d(TAG, "RemoteMediaPlayer::onQueueStatusUpdated() is reached");
- mediaStatus = remoteMediaPlayer.getMediaStatus();
- if (mediaStatus != null
- && mediaStatus.getQueueItems() != null) {
- List<MediaQueueItem> queueItems = mediaStatus
- .getQueueItems();
- int itemId = mediaStatus.getCurrentItemId();
- MediaQueueItem item = mediaStatus
- .getQueueItemById(itemId);
- int repeatMode = mediaStatus.getQueueRepeatMode();
- onQueueUpdated(queueItems, item, repeatMode, false);
- } else {
- onQueueUpdated(null, null,
- MediaStatus.REPEAT_MODE_REPEAT_OFF, false);
- }
- });
-
- }
- try {
- Log.d(TAG, "Registering MediaChannel namespace");
- Cast.CastApi.setMessageReceivedCallbacks(mApiClient, remoteMediaPlayer.getNamespace(),
- remoteMediaPlayer);
- } catch (IOException | IllegalStateException e) {
- Log.e(TAG, "attachMediaChannel()", e);
- }
- }
-
- private void reattachMediaChannel() {
- if (remoteMediaPlayer != null && mApiClient != null) {
- try {
- Log.d(TAG, "Registering MediaChannel namespace");
- Cast.CastApi.setMessageReceivedCallbacks(mApiClient,
- remoteMediaPlayer.getNamespace(), remoteMediaPlayer);
- } catch (IOException | IllegalStateException e) {
- Log.e(TAG, "reattachMediaChannel()", e);
- }
- }
- }
-
- private void detachMediaChannel() {
- Log.d(TAG, "trying to detach media channel");
- if (remoteMediaPlayer != null) {
- try {
- Cast.CastApi.removeMessageReceivedCallbacks(mApiClient,
- remoteMediaPlayer.getNamespace());
- } catch (IOException | IllegalStateException e) {
- Log.e(TAG, "detachMediaChannel()", e);
- }
- remoteMediaPlayer = null;
- }
- }
-
- /**
- * Returns the latest retrieved value for the {@link MediaStatus}. This value is updated
- * whenever the onStatusUpdated callback is called.
- */
- public final MediaStatus getMediaStatus() {
- return mediaStatus;
- }
-
- /*
- * This is called by onStatusUpdated() of the RemoteMediaPlayer
- */
- private void onRemoteMediaPlayerStatusUpdated() {
- Log.d(TAG, "onRemoteMediaPlayerStatusUpdated() reached");
- if (mApiClient == null || remoteMediaPlayer == null) {
- Log.d(TAG, "mApiClient or remoteMediaPlayer is null, so will not proceed");
- return;
- }
- mediaStatus = remoteMediaPlayer.getMediaStatus();
- if (mediaStatus == null) {
- Log.d(TAG, "MediaStatus is null, so will not proceed");
- return;
- } else {
- List<MediaQueueItem> queueItems = mediaStatus.getQueueItems();
- if (queueItems != null) {
- int itemId = mediaStatus.getCurrentItemId();
- MediaQueueItem item = mediaStatus.getQueueItemById(itemId);
- int repeatMode = mediaStatus.getQueueRepeatMode();
- onQueueUpdated(queueItems, item, repeatMode, false);
- } else {
- onQueueUpdated(null, null, MediaStatus.REPEAT_MODE_REPEAT_OFF, false);
- }
- state = mediaStatus.getPlayerState();
- int idleReason = mediaStatus.getIdleReason();
-
- if (state == MediaStatus.PLAYER_STATE_PLAYING) {
- Log.d(TAG, "onRemoteMediaPlayerStatusUpdated(): Player status = playing");
- } else if (state == MediaStatus.PLAYER_STATE_PAUSED) {
- Log.d(TAG, "onRemoteMediaPlayerStatusUpdated(): Player status = paused");
- } else if (state == MediaStatus.PLAYER_STATE_IDLE) {
- Log.d(TAG, "onRemoteMediaPlayerStatusUpdated(): Player status = IDLE with reason: "
- + idleReason);
- if (idleReason == MediaStatus.IDLE_REASON_ERROR) {
- // something bad happened on the cast device
- Log.d(TAG, "onRemoteMediaPlayerStatusUpdated(): IDLE reason = ERROR");
- onFailed(R.string.cast_failed_receiver_player_error, NO_STATUS_CODE);
- }
- } else if (state == MediaStatus.PLAYER_STATE_BUFFERING) {
- Log.d(TAG, "onRemoteMediaPlayerStatusUpdated(): Player status = buffering");
- } else {
- Log.d(TAG, "onRemoteMediaPlayerStatusUpdated(): Player status = unknown");
- }
- }
- for (CastConsumer consumer : castConsumers) {
- consumer.onRemoteMediaPlayerStatusUpdated();
- }
- if (mediaStatus != null) {
- double volume = mediaStatus.getStreamVolume();
- boolean isMute = mediaStatus.isMute();
- for (CastConsumer consumer : castConsumers) {
- consumer.onStreamVolumeChanged(volume, isMute);
- }
- }
- }
-
- private void onRemoteMediaPreloadStatusUpdated() {
- MediaQueueItem item = null;
- mediaStatus = remoteMediaPlayer.getMediaStatus();
- if (mediaStatus != null) {
- item = mediaStatus.getQueueItemById(mediaStatus.getPreloadedItemId());
- }
- Log.d(TAG, "onRemoteMediaPreloadStatusUpdated() " + item);
- for (CastConsumer consumer : castConsumers) {
- consumer.onRemoteMediaPreloadStatusUpdated(item);
- }
- }
-
- /*
- * This is called by onQueueStatusUpdated() of RemoteMediaPlayer
- */
- private void onQueueUpdated(List<MediaQueueItem> queueItems, MediaQueueItem item,
- int repeatMode, boolean shuffle) {
- Log.d(TAG, "onQueueUpdated() reached");
- Log.d(TAG, String.format(Locale.US, "Queue Items size: %d, Item: %s, Repeat Mode: %d, Shuffle: %s",
- queueItems == null ? 0 : queueItems.size(), item, repeatMode, shuffle));
- for (CastConsumer consumer : castConsumers) {
- consumer.onMediaQueueUpdated(queueItems, item, repeatMode, shuffle);
- }
- }
-
- /*
- * This is called by onMetadataUpdated() of RemoteMediaPlayer
- */
- public void onRemoteMediaPlayerMetadataUpdated() {
- Log.d(TAG, "onRemoteMediaPlayerMetadataUpdated() reached");
- for (CastConsumer consumer : castConsumers) {
- consumer.onRemoteMediaPlayerMetadataUpdated();
- }
- }
-
- /**
- * Registers a {@link CastConsumer} interface with this class.
- * Registered listeners will be notified of changes to a variety of
- * lifecycle and media status changes through the callbacks that the interface provides.
- *
- * @see DefaultCastConsumer
- */
- public synchronized void addCastConsumer(CastConsumer listener) {
- if (listener != null) {
- addBaseCastConsumer(listener);
- castConsumers.add(listener);
- Log.d(TAG, "Successfully added the new CastConsumer listener " + listener);
- }
- }
-
- /**
- * Unregisters a {@link CastConsumer}.
- */
- public synchronized void removeCastConsumer(CastConsumer listener) {
- if (listener != null) {
- removeBaseCastConsumer(listener);
- castConsumers.remove(listener);
- }
- }
-
- @Override
- protected void onDeviceUnselected() {
- detachMediaChannel();
- //removeDataChannel();
- state = MediaStatus.PLAYER_STATE_IDLE;
- mediaStatus = null;
- }
-
- @Override
- protected Cast.CastOptions.Builder getCastOptionBuilder(CastDevice device) {
- Cast.CastOptions.Builder builder = new Cast.CastOptions.Builder(mSelectedCastDevice, new CastListener());
- if (isFeatureEnabled(CastConfiguration.FEATURE_DEBUGGING)) {
- builder.setVerboseLoggingEnabled(true);
- }
- return builder;
- }
-
- @Override
- public void onConnectionFailed(ConnectionResult result) {
- super.onConnectionFailed(result);
- state = MediaStatus.PLAYER_STATE_IDLE;
- mediaStatus = null;
- }
-
- @Override
- public void onDisconnected(boolean stopAppOnExit, boolean clearPersistedConnectionData,
- boolean setDefaultRoute) {
- super.onDisconnected(stopAppOnExit, clearPersistedConnectionData, setDefaultRoute);
- state = MediaStatus.PLAYER_STATE_IDLE;
- mediaStatus = null;
- }
-
- class CastListener extends Cast.Listener {
-
- /*
- * (non-Javadoc)
- * @see com.google.android.gms.cast.Cast.Listener#onApplicationDisconnected (int)
- */
- @Override
- public void onApplicationDisconnected(int statusCode) {
- CastManager.this.onApplicationDisconnected(statusCode);
- }
-
- /*
- * (non-Javadoc)
- * @see com.google.android.gms.cast.Cast.Listener#onApplicationStatusChanged ()
- */
- @Override
- public void onApplicationStatusChanged() {
- CastManager.this.onApplicationStatusChanged();
- }
-
- @Override
- public void onVolumeChanged() {
- CastManager.this.onDeviceVolumeChanged();
- }
- }
-
- @Override
- public void onFailed(int resourceId, int statusCode) {
- Log.d(TAG, "onFailed: " + mContext.getString(resourceId) + ", code: " + statusCode);
- super.onFailed(resourceId, statusCode);
- }
-
- /**
- * Checks whether the selected Cast Device has the specified audio or video capabilities.
- *
- * @param capability capability from:
- * <ul>
- * <li>{@link CastDevice#CAPABILITY_AUDIO_IN}</li>
- * <li>{@link CastDevice#CAPABILITY_AUDIO_OUT}</li>
- * <li>{@link CastDevice#CAPABILITY_VIDEO_IN}</li>
- * <li>{@link CastDevice#CAPABILITY_VIDEO_OUT}</li>
- * </ul>
- * @param defaultVal value to return whenever there's no device selected.
- * @return {@code true} if the selected device has the specified capability,
- * {@code false} otherwise.
- */
- public boolean hasCapability(final int capability, final boolean defaultVal) {
- if (mSelectedCastDevice != null) {
- return mSelectedCastDevice.hasCapability(capability);
- } else {
- return defaultVal;
- }
- }
-
- /**
- * Adds and wires up the Switchable Media Router cast button. It returns a reference to the
- * {@link SwitchableMediaRouteActionProvider} associated with the button if the caller needs
- * such reference. It is assumed that the enclosing
- * {@link android.app.Activity} inherits (directly or indirectly) from
- * {@link androidx.appcompat.app.AppCompatActivity}.
- *
- * @param menuItem MenuItem of the Media Router cast button.
- */
- public final SwitchableMediaRouteActionProvider addMediaRouterButton(@NonNull MenuItem menuItem) {
- ActionProvider actionProvider = MenuItemCompat.getActionProvider(menuItem);
- if (!(actionProvider instanceof SwitchableMediaRouteActionProvider)) {
- Log.wtf(TAG, "MenuItem provided to addMediaRouterButton() is not compatible with " +
- "SwitchableMediaRouteActionProvider." +
- ((actionProvider == null) ? " Its action provider is null!" : ""),
- new ClassCastException());
- return null;
- }
- SwitchableMediaRouteActionProvider mediaRouteActionProvider =
- (SwitchableMediaRouteActionProvider) actionProvider;
- mediaRouteActionProvider.setRouteSelector(mMediaRouteSelector);
- if (mCastConfiguration.getMediaRouteDialogFactory() != null) {
- mediaRouteActionProvider.setDialogFactory(mCastConfiguration.getMediaRouteDialogFactory());
- }
- return mediaRouteActionProvider;
- }
-
- /* (non-Javadoc)
- * These methods startReconnectionService and stopReconnectionService simply override the ones
- * from BaseCastManager with empty implementations because we handle the service ourselves, but
- * need to allow BaseCastManager to save current network information.
- */
- @Override
- protected void startReconnectionService(long mediaDurationLeft) {
- // Do nothing
- }
-
- @Override
- protected void stopReconnectionService() {
- // Do nothing
- }
-}
diff --git a/core/src/play/java/de/danoeh/antennapod/core/cast/CastUtils.java b/core/src/play/java/de/danoeh/antennapod/core/cast/CastUtils.java
deleted file mode 100644
index e1f52aa9f..000000000
--- a/core/src/play/java/de/danoeh/antennapod/core/cast/CastUtils.java
+++ /dev/null
@@ -1,303 +0,0 @@
-package de.danoeh.antennapod.core.cast;
-
-import android.content.ContentResolver;
-import android.net.Uri;
-import android.text.TextUtils;
-import android.util.Log;
-
-import com.google.android.gms.cast.CastDevice;
-import com.google.android.gms.cast.MediaInfo;
-import com.google.android.gms.cast.MediaMetadata;
-import com.google.android.gms.common.images.WebImage;
-
-import java.util.Calendar;
-import java.util.List;
-
-import de.danoeh.antennapod.model.feed.Feed;
-import de.danoeh.antennapod.model.feed.FeedItem;
-import de.danoeh.antennapod.model.feed.FeedMedia;
-import de.danoeh.antennapod.model.playback.Playable;
-import de.danoeh.antennapod.model.playback.RemoteMedia;
-import de.danoeh.antennapod.core.storage.DBReader;
-
-/**
- * Helper functions for Cast support.
- */
-public class CastUtils {
- private CastUtils(){}
-
- private static final String TAG = "CastUtils";
-
- public static final String KEY_MEDIA_ID = "de.danoeh.antennapod.core.cast.MediaId";
-
- public static final String KEY_EPISODE_IDENTIFIER = "de.danoeh.antennapod.core.cast.EpisodeId";
- public static final String KEY_EPISODE_LINK = "de.danoeh.antennapod.core.cast.EpisodeLink";
- public static final String KEY_FEED_URL = "de.danoeh.antennapod.core.cast.FeedUrl";
- public static final String KEY_FEED_WEBSITE = "de.danoeh.antennapod.core.cast.FeedWebsite";
- public static final String KEY_EPISODE_NOTES = "de.danoeh.antennapod.core.cast.EpisodeNotes";
-
- /**
- * The field <code>AntennaPod.FormatVersion</code> specifies which version of MediaMetaData
- * fields we're using. Future implementations should try to be backwards compatible with earlier
- * versions, and earlier versions should be forward compatible until the version indicated by
- * <code>MAX_VERSION_FORWARD_COMPATIBILITY</code>. If an update makes the format unreadable for
- * an earlier version, then its version number should be greater than the
- * <code>MAX_VERSION_FORWARD_COMPATIBILITY</code> value set on the earlier one, so that it
- * doesn't try to parse the object.
- */
- public static final String KEY_FORMAT_VERSION = "de.danoeh.antennapod.core.cast.FormatVersion";
- public static final int FORMAT_VERSION_VALUE = 1;
- public static final int MAX_VERSION_FORWARD_COMPATIBILITY = 9999;
-
- public static boolean isCastable(Playable media) {
- if (media == null) {
- return false;
- }
- if (media instanceof FeedMedia || media instanceof RemoteMedia) {
- String url = media.getStreamUrl();
- if (url == null || url.isEmpty()) {
- return false;
- }
- if (url.startsWith(ContentResolver.SCHEME_CONTENT)) {
- return false; // Local feed
- }
- switch (media.getMediaType()) {
- case UNKNOWN:
- return false;
- case AUDIO:
- return CastManager.getInstance().hasCapability(CastDevice.CAPABILITY_AUDIO_OUT, true);
- case VIDEO:
- return CastManager.getInstance().hasCapability(CastDevice.CAPABILITY_VIDEO_OUT, true);
- }
- }
- return false;
- }
-
- /**
- * Converts {@link FeedMedia} objects into a format suitable for sending to a Cast Device.
- * Before using this method, one should make sure {@link #isCastable(Playable)} returns
- * {@code true}. This method should not run on the main thread.
- *
- * @param media The {@link FeedMedia} object to be converted.
- * @return {@link MediaInfo} object in a format proper for casting.
- */
- public static MediaInfo convertFromFeedMedia(FeedMedia media){
- if (media == null) {
- return null;
- }
- MediaMetadata metadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_GENERIC);
- if (media.getItem() == null) {
- media.setItem(DBReader.getFeedItem(media.getItemId()));
- }
- FeedItem feedItem = media.getItem();
- if (feedItem != null) {
- metadata.putString(MediaMetadata.KEY_TITLE, media.getEpisodeTitle());
- String subtitle = media.getFeedTitle();
- if (subtitle != null) {
- metadata.putString(MediaMetadata.KEY_SUBTITLE, subtitle);
- }
-
- if (!TextUtils.isEmpty(feedItem.getImageLocation())) {
- metadata.addImage(new WebImage(Uri.parse(feedItem.getImageLocation())));
- }
- Calendar calendar = Calendar.getInstance();
- calendar.setTime(media.getItem().getPubDate());
- metadata.putDate(MediaMetadata.KEY_RELEASE_DATE, calendar);
- Feed feed = feedItem.getFeed();
- if (feed != null) {
- if (!TextUtils.isEmpty(feed.getAuthor())) {
- metadata.putString(MediaMetadata.KEY_ARTIST, feed.getAuthor());
- }
- if (!TextUtils.isEmpty(feed.getDownload_url())) {
- metadata.putString(KEY_FEED_URL, feed.getDownload_url());
- }
- if (!TextUtils.isEmpty(feed.getLink())) {
- metadata.putString(KEY_FEED_WEBSITE, feed.getLink());
- }
- }
- if (!TextUtils.isEmpty(feedItem.getItemIdentifier())) {
- metadata.putString(KEY_EPISODE_IDENTIFIER, feedItem.getItemIdentifier());
- } else {
- metadata.putString(KEY_EPISODE_IDENTIFIER, media.getStreamUrl());
- }
- if (!TextUtils.isEmpty(feedItem.getLink())) {
- metadata.putString(KEY_EPISODE_LINK, feedItem.getLink());
- }
- try {
- DBReader.loadDescriptionOfFeedItem(feedItem);
- metadata.putString(KEY_EPISODE_NOTES, feedItem.getDescription());
- } catch (Exception e) {
- Log.e(TAG, "Unable to load FeedMedia notes", e);
- }
- }
- // This field only identifies the id on the device that has the original version.
- // Idea is to perhaps, on a first approach, check if the version on the local DB with the
- // same id matches the remote object, and if not then search for episode and feed identifiers.
- // This at least should make media recognition for a single device much quicker.
- metadata.putInt(KEY_MEDIA_ID, ((Long) media.getIdentifier()).intValue());
- // A way to identify different casting media formats in case we change it in the future and
- // senders with different versions share a casting device.
- metadata.putInt(KEY_FORMAT_VERSION, FORMAT_VERSION_VALUE);
-
- MediaInfo.Builder builder = new MediaInfo.Builder(media.getStreamUrl())
- .setContentType(media.getMime_type())
- .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
- .setMetadata(metadata);
- if (media.getDuration() > 0) {
- builder.setStreamDuration(media.getDuration());
- }
- return builder.build();
- }
-
- //TODO make unit tests for all the conversion methods
- /**
- * Converts {@link MediaInfo} objects into the appropriate implementation of {@link Playable}.
- *
- * Unless <code>searchFeedMedia</code> is set to <code>false</code>, this method should not run
- * on the GUI thread.
- *
- * @param media The {@link MediaInfo} object to be converted.
- * @param searchFeedMedia If set to <code>true</code>, the database will be queried to find a
- * {@link FeedMedia} instance that matches {@param media}.
- * @return {@link Playable} object in a format proper for casting.
- */
- public static Playable getPlayable(MediaInfo media, boolean searchFeedMedia) {
- Log.d(TAG, "getPlayable called with searchFeedMedia=" + searchFeedMedia);
- if (media == null) {
- Log.d(TAG, "MediaInfo object provided is null, not converting to any Playable instance");
- return null;
- }
- MediaMetadata metadata = media.getMetadata();
- int version = metadata.getInt(KEY_FORMAT_VERSION);
- if (version <= 0 || version > MAX_VERSION_FORWARD_COMPATIBILITY) {
- Log.w(TAG, "MediaInfo object obtained from the cast device is not compatible with this" +
- "version of AntennaPod CastUtils, curVer=" + FORMAT_VERSION_VALUE +
- ", object version=" + version);
- return null;
- }
- Playable result = null;
- if (searchFeedMedia) {
- long mediaId = metadata.getInt(KEY_MEDIA_ID);
- if (mediaId > 0) {
- FeedMedia fMedia = DBReader.getFeedMedia(mediaId);
- if (fMedia != null) {
- if (matches(media, fMedia)) {
- result = fMedia;
- Log.d(TAG, "FeedMedia object obtained matches the MediaInfo provided. id=" + mediaId);
- } else {
- Log.d(TAG, "FeedMedia object obtained does NOT match the MediaInfo provided. id=" + mediaId);
- }
- } else {
- Log.d(TAG, "Unable to find in database a FeedMedia with id=" + mediaId);
- }
- }
- if (result == null) {
- FeedItem feedItem = DBReader.getFeedItemByGuidOrEpisodeUrl(null,
- metadata.getString(KEY_EPISODE_IDENTIFIER));
- if (feedItem != null) {
- result = feedItem.getMedia();
- Log.d(TAG, "Found episode that matches the MediaInfo provided. Using its media, if existing.");
- }
- }
- }
- if (result == null) {
- List<WebImage> imageList = metadata.getImages();
- String imageUrl = null;
- if (!imageList.isEmpty()) {
- imageUrl = imageList.get(0).getUrl().toString();
- }
- String notes = metadata.getString(KEY_EPISODE_NOTES);
- result = new RemoteMedia(media.getContentId(),
- metadata.getString(KEY_EPISODE_IDENTIFIER),
- metadata.getString(KEY_FEED_URL),
- metadata.getString(MediaMetadata.KEY_SUBTITLE),
- metadata.getString(MediaMetadata.KEY_TITLE),
- metadata.getString(KEY_EPISODE_LINK),
- metadata.getString(MediaMetadata.KEY_ARTIST),
- imageUrl,
- metadata.getString(KEY_FEED_WEBSITE),
- media.getContentType(),
- metadata.getDate(MediaMetadata.KEY_RELEASE_DATE).getTime(),
- notes);
- Log.d(TAG, "Converted MediaInfo into RemoteMedia");
- }
- if (result.getDuration() == 0 && media.getStreamDuration() > 0) {
- result.setDuration((int) media.getStreamDuration());
- }
- return result;
- }
-
- /**
- * Compares a {@link MediaInfo} instance with a {@link FeedMedia} one and evaluates whether they
- * represent the same podcast episode.
- *
- * @param info the {@link MediaInfo} object to be compared.
- * @param media the {@link FeedMedia} object to be compared.
- * @return <true>true</true> if there's a match, <code>false</code> otherwise.
- *
- * @see RemoteMedia#equals(Object)
- */
- public static boolean matches(MediaInfo info, FeedMedia media) {
- if (info == null || media == null) {
- return false;
- }
- if (!TextUtils.equals(info.getContentId(), media.getStreamUrl())) {
- return false;
- }
- MediaMetadata metadata = info.getMetadata();
- FeedItem fi = media.getItem();
- if (fi == null || metadata == null ||
- !TextUtils.equals(metadata.getString(KEY_EPISODE_IDENTIFIER), fi.getItemIdentifier())) {
- return false;
- }
- Feed feed = fi.getFeed();
- return feed != null && TextUtils.equals(metadata.getString(KEY_FEED_URL), feed.getDownload_url());
- }
-
- /**
- * Compares a {@link MediaInfo} instance with a {@link RemoteMedia} one and evaluates whether they
- * represent the same podcast episode.
- *
- * @param info the {@link MediaInfo} object to be compared.
- * @param media the {@link RemoteMedia} object to be compared.
- * @return <true>true</true> if there's a match, <code>false</code> otherwise.
- *
- * @see RemoteMedia#equals(Object)
- */
- public static boolean matches(MediaInfo info, RemoteMedia media) {
- if (info == null || media == null) {
- return false;
- }
- if (!TextUtils.equals(info.getContentId(), media.getStreamUrl())) {
- return false;
- }
- MediaMetadata metadata = info.getMetadata();
- return metadata != null &&
- TextUtils.equals(metadata.getString(KEY_EPISODE_IDENTIFIER), media.getEpisodeIdentifier()) &&
- TextUtils.equals(metadata.getString(KEY_FEED_URL), media.getFeedUrl());
- }
-
- /**
- * Compares a {@link MediaInfo} instance with a {@link Playable} and evaluates whether they
- * represent the same podcast episode. Useful every time we get a MediaInfo from the Cast Device
- * and want to avoid unnecessary conversions.
- *
- * @param info the {@link MediaInfo} object to be compared.
- * @param media the {@link Playable} object to be compared.
- * @return <true>true</true> if there's a match, <code>false</code> otherwise.
- *
- * @see RemoteMedia#equals(Object)
- */
- public static boolean matches(MediaInfo info, Playable media) {
- if (info == null || media == null) {
- return false;
- }
- if (media instanceof RemoteMedia) {
- return matches(info, (RemoteMedia) media);
- }
- return media instanceof FeedMedia && matches(info, (FeedMedia) media);
- }
-
-
- //TODO Queue handling perhaps
-}
diff --git a/core/src/play/java/de/danoeh/antennapod/core/cast/DefaultCastConsumer.java b/core/src/play/java/de/danoeh/antennapod/core/cast/DefaultCastConsumer.java
deleted file mode 100644
index fe4183d54..000000000
--- a/core/src/play/java/de/danoeh/antennapod/core/cast/DefaultCastConsumer.java
+++ /dev/null
@@ -1,10 +0,0 @@
-package de.danoeh.antennapod.core.cast;
-
-import com.google.android.libraries.cast.companionlibrary.cast.callbacks.VideoCastConsumerImpl;
-
-public class DefaultCastConsumer extends VideoCastConsumerImpl implements CastConsumer {
- @Override
- public void onStreamVolumeChanged(double value, boolean isMute) {
- // no-op
- }
-}
diff --git a/core/src/play/java/de/danoeh/antennapod/core/cast/MediaInfoCreator.java b/core/src/play/java/de/danoeh/antennapod/core/cast/MediaInfoCreator.java
deleted file mode 100644
index 00011ef05..000000000
--- a/core/src/play/java/de/danoeh/antennapod/core/cast/MediaInfoCreator.java
+++ /dev/null
@@ -1,57 +0,0 @@
-package de.danoeh.antennapod.core.cast;
-
-import android.net.Uri;
-import android.text.TextUtils;
-import com.google.android.gms.cast.MediaInfo;
-import com.google.android.gms.cast.MediaMetadata;
-import com.google.android.gms.common.images.WebImage;
-import de.danoeh.antennapod.model.playback.RemoteMedia;
-import java.util.Calendar;
-
-public class MediaInfoCreator {
- public static MediaInfo from(RemoteMedia media) {
- MediaMetadata metadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_GENERIC);
-
- metadata.putString(MediaMetadata.KEY_TITLE, media.getEpisodeTitle());
- metadata.putString(MediaMetadata.KEY_SUBTITLE, media.getFeedTitle());
- if (!TextUtils.isEmpty(media.getImageLocation())) {
- metadata.addImage(new WebImage(Uri.parse(media.getImageLocation())));
- }
- Calendar calendar = Calendar.getInstance();
- calendar.setTime(media.getPubDate());
- metadata.putDate(MediaMetadata.KEY_RELEASE_DATE, calendar);
- if (!TextUtils.isEmpty(media.getFeedAuthor())) {
- metadata.putString(MediaMetadata.KEY_ARTIST, media.getFeedAuthor());
- }
- if (!TextUtils.isEmpty(media.getFeedUrl())) {
- metadata.putString(CastUtils.KEY_FEED_URL, media.getFeedUrl());
- }
- if (!TextUtils.isEmpty(media.getFeedLink())) {
- metadata.putString(CastUtils.KEY_FEED_WEBSITE, media.getFeedLink());
- }
- if (!TextUtils.isEmpty(media.getEpisodeIdentifier())) {
- metadata.putString(CastUtils.KEY_EPISODE_IDENTIFIER, media.getEpisodeIdentifier());
- } else {
- metadata.putString(CastUtils.KEY_EPISODE_IDENTIFIER, media.getDownloadUrl());
- }
- if (!TextUtils.isEmpty(media.getEpisodeLink())) {
- metadata.putString(CastUtils.KEY_EPISODE_LINK, media.getEpisodeLink());
- }
- String notes = media.getNotes();
- if (notes != null) {
- metadata.putString(CastUtils.KEY_EPISODE_NOTES, notes);
- }
- // Default id value
- metadata.putInt(CastUtils.KEY_MEDIA_ID, 0);
- metadata.putInt(CastUtils.KEY_FORMAT_VERSION, CastUtils.FORMAT_VERSION_VALUE);
-
- MediaInfo.Builder builder = new MediaInfo.Builder(media.getDownloadUrl())
- .setContentType(media.getMimeType())
- .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
- .setMetadata(metadata);
- if (media.getDuration() > 0) {
- builder.setStreamDuration(media.getDuration());
- }
- return builder.build();
- }
-}
diff --git a/core/src/play/java/de/danoeh/antennapod/core/cast/SwitchableMediaRouteActionProvider.java b/core/src/play/java/de/danoeh/antennapod/core/cast/SwitchableMediaRouteActionProvider.java
deleted file mode 100644
index 5a6a0aa2b..000000000
--- a/core/src/play/java/de/danoeh/antennapod/core/cast/SwitchableMediaRouteActionProvider.java
+++ /dev/null
@@ -1,106 +0,0 @@
-package de.danoeh.antennapod.core.cast;
-
-import android.app.Activity;
-import android.content.Context;
-import android.content.ContextWrapper;
-import androidx.fragment.app.FragmentActivity;
-import androidx.fragment.app.FragmentManager;
-import androidx.mediarouter.app.MediaRouteActionProvider;
-import androidx.mediarouter.app.MediaRouteChooserDialogFragment;
-import androidx.mediarouter.app.MediaRouteControllerDialogFragment;
-import androidx.mediarouter.media.MediaRouter;
-import android.util.Log;
-
-/**
- * <p>Action Provider that extends {@link MediaRouteActionProvider} and allows the client to
- * disable completely the button by calling {@link #setEnabled(boolean)}.</p>
- *
- * <p>It is disabled by default, so if a client wants to initially have it enabled it must call
- * <code>setEnabled(true)</code>.</p>
- */
-public class SwitchableMediaRouteActionProvider extends MediaRouteActionProvider {
- public static final String TAG = "SwitchblMediaRtActProv";
-
- private static final String CHOOSER_FRAGMENT_TAG =
- "android.support.v7.mediarouter:MediaRouteChooserDialogFragment";
- private static final String CONTROLLER_FRAGMENT_TAG =
- "android.support.v7.mediarouter:MediaRouteControllerDialogFragment";
- private boolean enabled;
-
- public SwitchableMediaRouteActionProvider(Context context) {
- super(context);
- enabled = false;
- }
-
- /**
- * <p>Sets whether the Media Router button should be allowed to become visible or not.</p>
- *
- * <p>It's invisible by default.</p>
- */
- public void setEnabled(boolean newVal) {
- enabled = newVal;
- refreshVisibility();
- }
-
- @Override
- public boolean isVisible() {
- return enabled && super.isVisible();
- }
-
- @Override
- public boolean onPerformDefaultAction() {
- if (!super.onPerformDefaultAction()) {
- // there is no button, but we should still show the dialog if it's the case.
- if (!isVisible()) {
- return false;
- }
- FragmentManager fm = getFragmentManager();
- if (fm == null) {
- return false;
- }
- MediaRouter.RouteInfo route = MediaRouter.getInstance(getContext()).getSelectedRoute();
- if (route.isDefault() || !route.matchesSelector(getRouteSelector())) {
- if (fm.findFragmentByTag(CHOOSER_FRAGMENT_TAG) != null) {
- Log.w(TAG, "showDialog(): Route chooser dialog already showing!");
- return false;
- }
- MediaRouteChooserDialogFragment f =
- getDialogFactory().onCreateChooserDialogFragment();
- f.setRouteSelector(getRouteSelector());
- f.show(fm, CHOOSER_FRAGMENT_TAG);
- } else {
- if (fm.findFragmentByTag(CONTROLLER_FRAGMENT_TAG) != null) {
- Log.w(TAG, "showDialog(): Route controller dialog already showing!");
- return false;
- }
- MediaRouteControllerDialogFragment f =
- getDialogFactory().onCreateControllerDialogFragment();
- f.show(fm, CONTROLLER_FRAGMENT_TAG);
- }
- return true;
-
- } else {
- return true;
- }
- }
-
- private FragmentManager getFragmentManager() {
- Activity activity = getActivity();
- if (activity instanceof FragmentActivity) {
- return ((FragmentActivity)activity).getSupportFragmentManager();
- }
- return null;
- }
-
- private Activity getActivity() {
- // Gross way of unwrapping the Activity so we can get the FragmentManager
- Context context = getContext();
- while (context instanceof ContextWrapper) {
- if (context instanceof Activity) {
- return (Activity)context;
- }
- context = ((ContextWrapper)context).getBaseContext();
- }
- return null;
- }
-}
diff --git a/core/src/play/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceFlavorHelper.java b/core/src/play/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceFlavorHelper.java
deleted file mode 100644
index 38e84017f..000000000
--- a/core/src/play/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceFlavorHelper.java
+++ /dev/null
@@ -1,314 +0,0 @@
-package de.danoeh.antennapod.core.service.playback;
-
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.net.NetworkInfo;
-import android.net.wifi.WifiManager;
-import android.os.Bundle;
-import androidx.annotation.NonNull;
-import androidx.annotation.StringRes;
-import android.support.v4.media.session.MediaSessionCompat;
-import android.support.v4.media.session.PlaybackStateCompat;
-import androidx.mediarouter.media.MediaRouter;
-import android.support.wearable.media.MediaControlConstants;
-import android.util.Log;
-import android.widget.Toast;
-
-import com.google.android.gms.cast.ApplicationMetadata;
-import com.google.android.libraries.cast.companionlibrary.cast.BaseCastManager;
-
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-
-import de.danoeh.antennapod.core.cast.CastConsumer;
-import de.danoeh.antennapod.core.cast.CastManager;
-import de.danoeh.antennapod.core.cast.DefaultCastConsumer;
-import de.danoeh.antennapod.core.event.MessageEvent;
-import de.danoeh.antennapod.model.playback.MediaType;
-import de.danoeh.antennapod.core.preferences.UserPreferences;
-import de.danoeh.antennapod.core.util.NetworkUtils;
-import org.greenrobot.eventbus.EventBus;
-
-/**
- * Class intended to work along PlaybackService and provide support for different flavors.
- */
-public class PlaybackServiceFlavorHelper {
- public static final String TAG = "PlaybackSrvFlavorHelper";
-
- /**
- * Time in seconds during which the CastManager will try to reconnect to the Cast Device after
- * the Wifi Connection is regained.
- */
- private static final int RECONNECTION_ATTEMPT_PERIOD_S = 15;
- /**
- * Stores the state of the cast playback just before it disconnects.
- */
- private volatile PlaybackServiceMediaPlayer.PSMPInfo infoBeforeCastDisconnection;
-
- private boolean wifiConnectivity = true;
- private BroadcastReceiver wifiBroadcastReceiver;
-
- private CastManager castManager;
- private MediaRouter mediaRouter;
- private PlaybackService.FlavorHelperCallback callback;
- private CastConsumer castConsumer;
-
- PlaybackServiceFlavorHelper(Context context, PlaybackService.FlavorHelperCallback callback) {
- this.callback = callback;
- if (!CastManager.isInitialized()) {
- return;
- }
- mediaRouter = MediaRouter.getInstance(context.getApplicationContext());
- setCastConsumer(context);
- }
-
- void initializeMediaPlayer(Context context) {
- if (!CastManager.isInitialized()) {
- callback.setMediaPlayer(new LocalPSMP(context, callback.getMediaPlayerCallback()));
- return;
- }
- castManager = CastManager.getInstance();
- castManager.addCastConsumer(castConsumer);
- boolean isCasting = castManager.isConnected();
- callback.setIsCasting(isCasting);
- if (isCasting) {
- if (UserPreferences.isCastEnabled()) {
- onCastAppConnected(context, false);
- } else {
- castManager.disconnect();
- }
- } else {
- callback.setMediaPlayer(new LocalPSMP(context, callback.getMediaPlayerCallback()));
- }
- }
-
- void removeCastConsumer() {
- if (!CastManager.isInitialized()) {
- return;
- }
- castManager.removeCastConsumer(castConsumer);
- }
-
- boolean castDisconnect(boolean castDisconnect) {
- if (!CastManager.isInitialized()) {
- return false;
- }
- if (castDisconnect) {
- castManager.disconnect();
- }
- return castDisconnect;
- }
-
- boolean onMediaPlayerInfo(Context context, int code, @StringRes int resourceId) {
- if (!CastManager.isInitialized()) {
- return false;
- }
- switch (code) {
- case RemotePSMP.CAST_ERROR:
- EventBus.getDefault().post(new MessageEvent(context.getString(resourceId)));
- return true;
- case RemotePSMP.CAST_ERROR_PRIORITY_HIGH:
- Toast.makeText(context, resourceId, Toast.LENGTH_SHORT).show();
- return true;
- default:
- return false;
- }
- }
-
- private void setCastConsumer(Context context) {
- castConsumer = new DefaultCastConsumer() {
- @Override
- public void onApplicationConnected(ApplicationMetadata appMetadata, String sessionId, boolean wasLaunched) {
- onCastAppConnected(context, wasLaunched);
- }
-
- @Override
- public void onDisconnectionReason(int reason) {
- Log.d(TAG, "onDisconnectionReason() with code " + reason);
- // This is our final chance to update the underlying stream position
- // In onDisconnected(), the underlying CastPlayback#mVideoCastConsumer
- // is disconnected and hence we update our local value of stream position
- // to the latest position.
- PlaybackServiceMediaPlayer mediaPlayer = callback.getMediaPlayer();
- if (mediaPlayer != null) {
- callback.saveCurrentPosition(true, null, PlaybackServiceMediaPlayer.INVALID_TIME);
- infoBeforeCastDisconnection = mediaPlayer.getPSMPInfo();
- if (reason != BaseCastManager.DISCONNECT_REASON_EXPLICIT &&
- infoBeforeCastDisconnection.playerStatus == PlayerStatus.PLAYING) {
- // If it's NOT based on user action, we shouldn't automatically resume local playback
- infoBeforeCastDisconnection.playerStatus = PlayerStatus.PAUSED;
- }
- }
- }
-
- @Override
- public void onDisconnected() {
- Log.d(TAG, "onDisconnected()");
- callback.setIsCasting(false);
- PlaybackServiceMediaPlayer.PSMPInfo info = infoBeforeCastDisconnection;
- infoBeforeCastDisconnection = null;
- PlaybackServiceMediaPlayer mediaPlayer = callback.getMediaPlayer();
- if (info == null && mediaPlayer != null) {
- info = mediaPlayer.getPSMPInfo();
- }
- if (info == null) {
- info = new PlaybackServiceMediaPlayer.PSMPInfo(PlayerStatus.INDETERMINATE,
- PlayerStatus.STOPPED, null);
- }
- switchMediaPlayer(new LocalPSMP(context, callback.getMediaPlayerCallback()),
- info, true);
- if (info.playable != null) {
- callback.sendNotificationBroadcast(PlaybackService.NOTIFICATION_TYPE_RELOAD,
- info.playable.getMediaType() == MediaType.AUDIO ?
- PlaybackService.EXTRA_CODE_AUDIO : PlaybackService.EXTRA_CODE_VIDEO);
- } else {
- Log.d(TAG, "Cast session disconnected, but no current media");
- callback.sendNotificationBroadcast(PlaybackService.NOTIFICATION_TYPE_PLAYBACK_END, 0);
- }
- // hardware volume buttons control the local device volume
- mediaRouter.setMediaSessionCompat(null);
- unregisterWifiBroadcastReceiver();
- callback.setupNotification(false, info);
- }
- };
- }
-
- private void onCastAppConnected(Context context, boolean wasLaunched) {
- Log.d(TAG, "A cast device application was " + (wasLaunched ? "launched" : "joined"));
- callback.setIsCasting(true);
- PlaybackServiceMediaPlayer.PSMPInfo info = null;
- PlaybackServiceMediaPlayer mediaPlayer = callback.getMediaPlayer();
- if (mediaPlayer != null) {
- info = mediaPlayer.getPSMPInfo();
- if (info.playerStatus == PlayerStatus.PLAYING) {
- // could be pause, but this way we make sure the new player will get the correct position,
- // since pause runs asynchronously and we could be directing the new player to play even before
- // the old player gives us back the position.
- callback.saveCurrentPosition(true, null, PlaybackServiceMediaPlayer.INVALID_TIME);
- }
- }
- if (info == null) {
- info = new PlaybackServiceMediaPlayer.PSMPInfo(PlayerStatus.INDETERMINATE, PlayerStatus.STOPPED, null);
- }
- callback.sendNotificationBroadcast(PlaybackService.NOTIFICATION_TYPE_RELOAD,
- PlaybackService.EXTRA_CODE_CAST);
- RemotePSMP remotePSMP = new RemotePSMP(context, callback.getMediaPlayerCallback());
- switchMediaPlayer(remotePSMP, info, wasLaunched);
- remotePSMP.init();
- // hardware volume buttons control the remote device volume
- mediaRouter.setMediaSessionCompat(callback.getMediaSession());
- registerWifiBroadcastReceiver();
- callback.setupNotification(true, info);
- }
-
- private void switchMediaPlayer(@NonNull PlaybackServiceMediaPlayer newPlayer,
- @NonNull PlaybackServiceMediaPlayer.PSMPInfo info,
- boolean wasLaunched) {
- PlaybackServiceMediaPlayer mediaPlayer = callback.getMediaPlayer();
- if (mediaPlayer != null) {
- try {
- mediaPlayer.stopPlayback(false).get(2, TimeUnit.SECONDS);
- } catch (InterruptedException | ExecutionException | TimeoutException e) {
- Log.e(TAG, "There was a problem stopping playback while switching media players", e);
- }
- mediaPlayer.shutdownQuietly();
- }
- mediaPlayer = newPlayer;
- callback.setMediaPlayer(mediaPlayer);
- Log.d(TAG, "switched to " + mediaPlayer.getClass().getSimpleName());
- if (!wasLaunched) {
- PlaybackServiceMediaPlayer.PSMPInfo candidate = mediaPlayer.getPSMPInfo();
- if (candidate.playable != null &&
- candidate.playerStatus.isAtLeast(PlayerStatus.PREPARING)) {
- // do not automatically send new media to cast device
- info.playable = null;
- }
- }
- if (info.playable != null) {
- mediaPlayer.playMediaObject(info.playable,
- !info.playable.localFileAvailable(),
- info.playerStatus == PlayerStatus.PLAYING,
- info.playerStatus.isAtLeast(PlayerStatus.PREPARING));
- }
- }
-
- void registerWifiBroadcastReceiver() {
- if (!CastManager.isInitialized()) {
- return;
- }
- if (wifiBroadcastReceiver != null) {
- return;
- }
- wifiBroadcastReceiver = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- if (intent.getAction().equals(WifiManager.NETWORK_STATE_CHANGED_ACTION)) {
- NetworkInfo info = intent.getParcelableExtra(WifiManager.EXTRA_NETWORK_INFO);
- boolean isConnected = info.isConnected();
- //apparently this method gets called twice when a change happens, but one run is enough.
- if (isConnected && !wifiConnectivity) {
- wifiConnectivity = true;
- castManager.startCastDiscovery();
- castManager.reconnectSessionIfPossible(RECONNECTION_ATTEMPT_PERIOD_S, NetworkUtils.getWifiSsid());
- } else {
- wifiConnectivity = isConnected;
- }
- }
- }
- };
- callback.registerReceiver(wifiBroadcastReceiver,
- new IntentFilter(WifiManager.NETWORK_STATE_CHANGED_ACTION));
- }
-
- void unregisterWifiBroadcastReceiver() {
- if (!CastManager.isInitialized()) {
- return;
- }
- if (wifiBroadcastReceiver != null) {
- callback.unregisterReceiver(wifiBroadcastReceiver);
- wifiBroadcastReceiver = null;
- }
- }
-
- boolean onSharedPreference(String key) {
- if (!CastManager.isInitialized()) {
- return false;
- }
- if (UserPreferences.PREF_CAST_ENABLED.equals(key)) {
- if (!UserPreferences.isCastEnabled()) {
- if (castManager.isConnecting() || castManager.isConnected()) {
- Log.d(TAG, "Disconnecting cast device due to a change in user preferences");
- castManager.disconnect();
- }
- }
- return true;
- }
- return false;
- }
-
- void sessionStateAddActionForWear(PlaybackStateCompat.Builder sessionState, String actionName, CharSequence name, int icon) {
- if (!CastManager.isInitialized()) {
- return;
- }
- PlaybackStateCompat.CustomAction.Builder actionBuilder =
- new PlaybackStateCompat.CustomAction.Builder(actionName, name, icon);
- Bundle actionExtras = new Bundle();
- actionExtras.putBoolean(MediaControlConstants.EXTRA_CUSTOM_ACTION_SHOW_ON_WEAR, true);
- actionBuilder.setExtras(actionExtras);
-
- sessionState.addCustomAction(actionBuilder.build());
- }
-
- void mediaSessionSetExtraForWear(MediaSessionCompat mediaSession) {
- if (!CastManager.isInitialized()) {
- return;
- }
- Bundle sessionExtras = new Bundle();
- sessionExtras.putBoolean(MediaControlConstants.EXTRA_RESERVE_SLOT_SKIP_TO_PREVIOUS, true);
- sessionExtras.putBoolean(MediaControlConstants.EXTRA_RESERVE_SLOT_SKIP_TO_NEXT, true);
- mediaSession.setExtras(sessionExtras);
- }
-}
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
new file mode 100644
index 000000000..2167d9f2c
--- /dev/null
+++ b/core/src/play/java/de/danoeh/antennapod/core/service/playback/WearMediaSession.java
@@ -0,0 +1,28 @@
+package de.danoeh.antennapod.core.service.playback;
+
+import android.os.Bundle;
+import android.support.v4.media.session.MediaSessionCompat;
+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 =
+ new PlaybackStateCompat.CustomAction.Builder(actionName, name, icon);
+ Bundle actionExtras = new Bundle();
+ actionExtras.putBoolean(MediaControlConstants.EXTRA_CUSTOM_ACTION_SHOW_ON_WEAR, true);
+ actionBuilder.setExtras(actionExtras);
+
+ sessionState.addCustomAction(actionBuilder.build());
+ }
+
+ static void mediaSessionSetExtraForWear(MediaSessionCompat mediaSession) {
+ Bundle sessionExtras = new Bundle();
+ sessionExtras.putBoolean(MediaControlConstants.EXTRA_RESERVE_SLOT_SKIP_TO_PREVIOUS, true);
+ sessionExtras.putBoolean(MediaControlConstants.EXTRA_RESERVE_SLOT_SKIP_TO_NEXT, true);
+ mediaSession.setExtras(sessionExtras);
+ }
+}
diff --git a/core/src/play/res/values/strings.xml b/core/src/play/res/values/strings.xml
deleted file mode 100644
index 7307849d2..000000000
--- a/core/src/play/res/values/strings.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<resources>
- <string name="pref_cast_message" translatable="false">@string/pref_cast_message_play_flavor</string>
-</resources>
diff --git a/core/src/test/java/de/danoeh/antennapod/core/feed/FeedFilterTest.java b/core/src/test/java/de/danoeh/antennapod/core/feed/FeedFilterTest.java
index 4ad578727..3840f6387 100644
--- a/core/src/test/java/de/danoeh/antennapod/core/feed/FeedFilterTest.java
+++ b/core/src/test/java/de/danoeh/antennapod/core/feed/FeedFilterTest.java
@@ -1,7 +1,10 @@
package de.danoeh.antennapod.core.feed;
+import de.danoeh.antennapod.core.util.Converter;
import de.danoeh.antennapod.model.feed.FeedFilter;
import de.danoeh.antennapod.model.feed.FeedItem;
+import de.danoeh.antennapod.model.feed.FeedMedia;
+
import org.junit.Test;
import static org.junit.Assert.assertEquals;
@@ -125,4 +128,32 @@ public class FeedFilterTest {
assertFalse(filter.shouldAutoDownload(doNotDownload2));
}
+ @Test
+ public void testMinimalDurationFilter() {
+ FeedItem download = new FeedItem();
+ download.setTitle("Hello friend!");
+ FeedMedia downloadMedia = FeedMediaMother.anyFeedMedia();
+ downloadMedia.setDuration(Converter.durationStringShortToMs("05:00", false));
+ download.setMedia(downloadMedia);
+ // because duration of the media in unknown
+ FeedItem download2 = new FeedItem();
+ download2.setTitle("Hello friend!");
+ FeedMedia unknownDurationMedia = FeedMediaMother.anyFeedMedia();
+ download2.setMedia(unknownDurationMedia);
+ // because it is not long enough
+ FeedItem doNotDownload = new FeedItem();
+ doNotDownload.setTitle("Hello friend!");
+ FeedMedia doNotDownloadMedia = FeedMediaMother.anyFeedMedia();
+ doNotDownloadMedia.setDuration(Converter.durationStringShortToMs("02:00", false));
+ doNotDownload.setMedia(doNotDownloadMedia);
+
+ int minimalDurationFilter = 3 * 60;
+ FeedFilter filter = new FeedFilter("", "", minimalDurationFilter);
+
+ assertTrue(filter.hasMinimalDurationFilter());
+ assertTrue(filter.shouldAutoDownload(download));
+ assertFalse(filter.shouldAutoDownload(doNotDownload));
+ assertTrue(filter.shouldAutoDownload(download2));
+ }
+
}
diff --git a/core/src/test/java/de/danoeh/antennapod/core/feed/FeedItemTest.java b/core/src/test/java/de/danoeh/antennapod/core/feed/FeedItemTest.java
index c4860d818..a08d0897d 100644
--- a/core/src/test/java/de/danoeh/antennapod/core/feed/FeedItemTest.java
+++ b/core/src/test/java/de/danoeh/antennapod/core/feed/FeedItemTest.java
@@ -1,6 +1,7 @@
package de.danoeh.antennapod.core.feed;
import de.danoeh.antennapod.model.feed.FeedItem;
+import de.danoeh.antennapod.model.feed.FeedMedia;
import org.junit.Before;
import org.junit.Test;
@@ -10,11 +11,13 @@ import java.util.Date;
import static de.danoeh.antennapod.core.feed.FeedItemMother.anyFeedItemWithImage;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
public class FeedItemTest {
private static final String TEXT_LONG = "Lorem ipsum dolor sit amet, consectetur adipiscing elit.";
private static final String TEXT_SHORT = "Lorem ipsum";
+ private static final long ONE_HOUR = 1000L * 3600L;
private FeedItem original;
private FeedItem changedFeedItem;
@@ -136,4 +139,36 @@ public class FeedItemTest {
item.setDescriptionIfLonger(contentEncoded);
assertEquals(TEXT_LONG, item.getDescription());
}
-} \ No newline at end of file
+
+ @Test
+ public void testAutoDownloadBackoff() {
+ FeedItem item = new FeedItem();
+ item.setMedia(new FeedMedia(item, "https://example.com/file.mp3", 0, "audio/mpeg"));
+
+ long now = ONE_HOUR; // In reality, this is System.currentTimeMillis()
+ assertTrue(item.isAutoDownloadable(now));
+ item.increaseFailedAutoDownloadAttempts(now);
+ assertFalse(item.isAutoDownloadable(now));
+
+ now += ONE_HOUR;
+ assertTrue(item.isAutoDownloadable(now));
+ item.increaseFailedAutoDownloadAttempts(now);
+ assertFalse(item.isAutoDownloadable(now));
+
+ now += ONE_HOUR;
+ assertFalse(item.isAutoDownloadable(now)); // Should backoff, so more than 1 hour needed
+
+ now += ONE_HOUR;
+ assertTrue(item.isAutoDownloadable(now)); // Now it's enough
+ item.increaseFailedAutoDownloadAttempts(now);
+ item.increaseFailedAutoDownloadAttempts(now);
+ item.increaseFailedAutoDownloadAttempts(now);
+
+ now += 1000L * ONE_HOUR;
+ assertFalse(item.isAutoDownloadable(now)); // Should have given up
+ item.increaseFailedAutoDownloadAttempts(now);
+
+ now += 1000L * ONE_HOUR;
+ assertFalse(item.isAutoDownloadable(now)); // Still given up
+ }
+}
diff --git a/core/src/test/java/de/danoeh/antennapod/core/service/playback/PlaybackVolumeUpdaterTest.java b/core/src/test/java/de/danoeh/antennapod/core/service/playback/PlaybackVolumeUpdaterTest.java
index 4890c471a..92c0e8e3d 100644
--- a/core/src/test/java/de/danoeh/antennapod/core/service/playback/PlaybackVolumeUpdaterTest.java
+++ b/core/src/test/java/de/danoeh/antennapod/core/service/playback/PlaybackVolumeUpdaterTest.java
@@ -6,6 +6,8 @@ import de.danoeh.antennapod.model.feed.FeedMedia;
import de.danoeh.antennapod.model.feed.FeedPreferences;
import de.danoeh.antennapod.model.feed.VolumeAdaptionSetting;
import de.danoeh.antennapod.model.playback.Playable;
+import de.danoeh.antennapod.playback.base.PlaybackServiceMediaPlayer;
+import de.danoeh.antennapod.playback.base.PlayerStatus;
import org.junit.Before;
import org.junit.Test;
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 f70ed6e29..5e73773db 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
@@ -188,14 +188,13 @@ public class DbWriterTest {
assertTrue(queue.size() != 0);
DBWriter.deleteFeedMediaOfItem(context, media.getId());
- Awaitility.await().until(() -> !dest.exists());
+ Awaitility.await().timeout(2, TimeUnit.SECONDS).until(() -> !dest.exists());
media = DBReader.getFeedMedia(media.getId());
assertNotNull(media);
assertFalse(dest.exists());
assertFalse(media.isDownloaded());
assertNull(media.getFile_url());
- queue = DBReader.getQueue();
- assertEquals(0, queue.size());
+ Awaitility.await().timeout(2, TimeUnit.SECONDS).until(() -> DBReader.getQueue().isEmpty());
}
@Test
diff --git a/core/src/test/java/de/danoeh/antennapod/core/sync/GuidValidatorTest.java b/core/src/test/java/de/danoeh/antennapod/core/sync/GuidValidatorTest.java
index 356a7f77e..552f7d70a 100644
--- a/core/src/test/java/de/danoeh/antennapod/core/sync/GuidValidatorTest.java
+++ b/core/src/test/java/de/danoeh/antennapod/core/sync/GuidValidatorTest.java
@@ -14,5 +14,6 @@ public class GuidValidatorTest extends TestCase {
assertFalse(GuidValidator.isValidGuid("\n"));
assertFalse(GuidValidator.isValidGuid(" \n"));
assertFalse(GuidValidator.isValidGuid(null));
+ assertFalse(GuidValidator.isValidGuid("null"));
}
} \ No newline at end of file
diff --git a/event/build.gradle b/event/build.gradle
new file mode 100644
index 000000000..c852c0351
--- /dev/null
+++ b/event/build.gradle
@@ -0,0 +1,8 @@
+apply plugin: "com.android.library"
+apply from: "../common.gradle"
+
+dependencies {
+ implementation project(':model')
+
+ annotationProcessor "androidx.annotation:annotation:$annotationVersion"
+}
diff --git a/event/src/main/AndroidManifest.xml b/event/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..7561cf555
--- /dev/null
+++ b/event/src/main/AndroidManifest.xml
@@ -0,0 +1 @@
+<manifest package="de.danoeh.antennapod.event" />
diff --git a/core/src/main/java/de/danoeh/antennapod/core/event/DiscoveryDefaultUpdateEvent.java b/event/src/main/java/de/danoeh/antennapod/event/DiscoveryDefaultUpdateEvent.java
index f7757935a..944c7759a 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/event/DiscoveryDefaultUpdateEvent.java
+++ b/event/src/main/java/de/danoeh/antennapod/event/DiscoveryDefaultUpdateEvent.java
@@ -1,4 +1,4 @@
-package de.danoeh.antennapod.core.event;
+package de.danoeh.antennapod.event;
public class DiscoveryDefaultUpdateEvent {
public DiscoveryDefaultUpdateEvent() {
diff --git a/core/src/main/java/de/danoeh/antennapod/core/event/FavoritesEvent.java b/event/src/main/java/de/danoeh/antennapod/event/FavoritesEvent.java
index cbfcc37e6..8b27f74ab 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/event/FavoritesEvent.java
+++ b/event/src/main/java/de/danoeh/antennapod/event/FavoritesEvent.java
@@ -1,9 +1,4 @@
-package de.danoeh.antennapod.core.event;
-
-import androidx.annotation.NonNull;
-
-import org.apache.commons.lang3.builder.ToStringBuilder;
-import org.apache.commons.lang3.builder.ToStringStyle;
+package de.danoeh.antennapod.event;
import de.danoeh.antennapod.model.feed.FeedItem;
@@ -28,14 +23,4 @@ public class FavoritesEvent {
public static FavoritesEvent removed(FeedItem item) {
return new FavoritesEvent(Action.REMOVED, item);
}
-
- @NonNull
- @Override
- public String toString() {
- return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE)
- .append("action", action)
- .append("item", item)
- .toString();
- }
-
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/event/FeedItemEvent.java b/event/src/main/java/de/danoeh/antennapod/event/FeedItemEvent.java
index 99cb01714..6c7adc2d7 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/event/FeedItemEvent.java
+++ b/event/src/main/java/de/danoeh/antennapod/event/FeedItemEvent.java
@@ -1,11 +1,8 @@
-package de.danoeh.antennapod.core.event;
+package de.danoeh.antennapod.event;
import androidx.annotation.NonNull;
-import org.apache.commons.lang3.builder.ToStringBuilder;
-import org.apache.commons.lang3.builder.ToStringStyle;
-
import java.util.Arrays;
import java.util.List;
@@ -41,14 +38,4 @@ public class FeedItemEvent {
public static FeedItemEvent updated(FeedItem... items) {
return updated(Arrays.asList(items));
}
-
- @NonNull
- @Override
- public String toString() {
- return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE)
- .append("action", action)
- .append("items", items)
- .toString();
- }
-
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/event/FeedListUpdateEvent.java b/event/src/main/java/de/danoeh/antennapod/event/FeedListUpdateEvent.java
index 4ed8e33ec..99a83abec 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/event/FeedListUpdateEvent.java
+++ b/event/src/main/java/de/danoeh/antennapod/event/FeedListUpdateEvent.java
@@ -1,4 +1,4 @@
-package de.danoeh.antennapod.core.event;
+package de.danoeh.antennapod.event;
import de.danoeh.antennapod.model.feed.Feed;
diff --git a/core/src/main/java/de/danoeh/antennapod/core/event/MessageEvent.java b/event/src/main/java/de/danoeh/antennapod/event/MessageEvent.java
index 9fb22b8ea..3f6b2db32 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/event/MessageEvent.java
+++ b/event/src/main/java/de/danoeh/antennapod/event/MessageEvent.java
@@ -1,4 +1,4 @@
-package de.danoeh.antennapod.core.event;
+package de.danoeh.antennapod.event;
import androidx.annotation.Nullable;
diff --git a/event/src/main/java/de/danoeh/antennapod/event/PlayerErrorEvent.java b/event/src/main/java/de/danoeh/antennapod/event/PlayerErrorEvent.java
new file mode 100644
index 000000000..662a16f81
--- /dev/null
+++ b/event/src/main/java/de/danoeh/antennapod/event/PlayerErrorEvent.java
@@ -0,0 +1,13 @@
+package de.danoeh.antennapod.event;
+
+public class PlayerErrorEvent {
+ private final String message;
+
+ public PlayerErrorEvent(String message) {
+ this.message = message;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/event/PlayerStatusEvent.java b/event/src/main/java/de/danoeh/antennapod/event/PlayerStatusEvent.java
index fe7f17968..4074bd98f 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/event/PlayerStatusEvent.java
+++ b/event/src/main/java/de/danoeh/antennapod/event/PlayerStatusEvent.java
@@ -1,4 +1,4 @@
-package de.danoeh.antennapod.core.event;
+package de.danoeh.antennapod.event;
public class PlayerStatusEvent {
public PlayerStatusEvent() {
diff --git a/core/src/main/java/de/danoeh/antennapod/core/event/QueueEvent.java b/event/src/main/java/de/danoeh/antennapod/event/QueueEvent.java
index c866939bd..578398865 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/event/QueueEvent.java
+++ b/event/src/main/java/de/danoeh/antennapod/event/QueueEvent.java
@@ -1,10 +1,7 @@
-package de.danoeh.antennapod.core.event;
+package de.danoeh.antennapod.event;
import androidx.annotation.Nullable;
-import org.apache.commons.lang3.builder.ToStringBuilder;
-import org.apache.commons.lang3.builder.ToStringStyle;
-
import java.util.List;
import de.danoeh.antennapod.model.feed.FeedItem;
@@ -58,14 +55,4 @@ public class QueueEvent {
public static QueueEvent moved(FeedItem item, int newPosition) {
return new QueueEvent(Action.MOVED, item, null, newPosition);
}
-
- @Override
- public String toString() {
- return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE)
- .append("action", action)
- .append("item", item)
- .append("items", items)
- .append("position", position)
- .toString();
- }
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/event/SyncServiceEvent.java b/event/src/main/java/de/danoeh/antennapod/event/SyncServiceEvent.java
index 7aa5f6bf1..2ebac8c0a 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/event/SyncServiceEvent.java
+++ b/event/src/main/java/de/danoeh/antennapod/event/SyncServiceEvent.java
@@ -1,4 +1,4 @@
-package de.danoeh.antennapod.core.event;
+package de.danoeh.antennapod.event;
public class SyncServiceEvent {
private final int messageResId;
diff --git a/core/src/main/java/de/danoeh/antennapod/core/event/UnreadItemsUpdateEvent.java b/event/src/main/java/de/danoeh/antennapod/event/UnreadItemsUpdateEvent.java
index c3efbfe8b..fb1bbc739 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/event/UnreadItemsUpdateEvent.java
+++ b/event/src/main/java/de/danoeh/antennapod/event/UnreadItemsUpdateEvent.java
@@ -1,4 +1,4 @@
-package de.danoeh.antennapod.core.event;
+package de.danoeh.antennapod.event;
public class UnreadItemsUpdateEvent {
public UnreadItemsUpdateEvent() {
diff --git a/event/src/main/java/de/danoeh/antennapod/event/playback/BufferUpdateEvent.java b/event/src/main/java/de/danoeh/antennapod/event/playback/BufferUpdateEvent.java
new file mode 100644
index 000000000..57d41ad13
--- /dev/null
+++ b/event/src/main/java/de/danoeh/antennapod/event/playback/BufferUpdateEvent.java
@@ -0,0 +1,35 @@
+package de.danoeh.antennapod.event.playback;
+
+public class BufferUpdateEvent {
+ private static final float PROGRESS_STARTED = -1;
+ private static final float PROGRESS_ENDED = -2;
+ final float progress;
+
+ private BufferUpdateEvent(float progress) {
+ this.progress = progress;
+ }
+
+ public static BufferUpdateEvent started() {
+ return new BufferUpdateEvent(PROGRESS_STARTED);
+ }
+
+ public static BufferUpdateEvent ended() {
+ return new BufferUpdateEvent(PROGRESS_ENDED);
+ }
+
+ public static BufferUpdateEvent progressUpdate(float progress) {
+ return new BufferUpdateEvent(progress);
+ }
+
+ public float getProgress() {
+ return progress;
+ }
+
+ public boolean hasStarted() {
+ return progress == PROGRESS_STARTED;
+ }
+
+ public boolean hasEnded() {
+ return progress == PROGRESS_ENDED;
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/event/PlaybackHistoryEvent.java b/event/src/main/java/de/danoeh/antennapod/event/playback/PlaybackHistoryEvent.java
index cd3f27bf5..b51377a3d 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/event/PlaybackHistoryEvent.java
+++ b/event/src/main/java/de/danoeh/antennapod/event/playback/PlaybackHistoryEvent.java
@@ -1,4 +1,4 @@
-package de.danoeh.antennapod.core.event;
+package de.danoeh.antennapod.event.playback;
public class PlaybackHistoryEvent {
diff --git a/core/src/main/java/de/danoeh/antennapod/core/event/PlaybackPositionEvent.java b/event/src/main/java/de/danoeh/antennapod/event/playback/PlaybackPositionEvent.java
index 3327d8a02..3746680eb 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/event/PlaybackPositionEvent.java
+++ b/event/src/main/java/de/danoeh/antennapod/event/playback/PlaybackPositionEvent.java
@@ -1,4 +1,4 @@
-package de.danoeh.antennapod.core.event;
+package de.danoeh.antennapod.event.playback;
public class PlaybackPositionEvent {
private final int position;
diff --git a/core/src/main/java/de/danoeh/antennapod/core/event/ServiceEvent.java b/event/src/main/java/de/danoeh/antennapod/event/playback/PlaybackServiceEvent.java
index 2230ee84f..8115730dd 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/event/ServiceEvent.java
+++ b/event/src/main/java/de/danoeh/antennapod/event/playback/PlaybackServiceEvent.java
@@ -1,6 +1,6 @@
-package de.danoeh.antennapod.core.event;
+package de.danoeh.antennapod.event.playback;
-public class ServiceEvent {
+public class PlaybackServiceEvent {
public enum Action {
SERVICE_STARTED,
SERVICE_SHUT_DOWN
@@ -8,7 +8,7 @@ public class ServiceEvent {
public final Action action;
- public ServiceEvent(Action action) {
+ public PlaybackServiceEvent(Action action) {
this.action = action;
}
}
diff --git a/event/src/main/java/de/danoeh/antennapod/event/playback/SleepTimerUpdatedEvent.java b/event/src/main/java/de/danoeh/antennapod/event/playback/SleepTimerUpdatedEvent.java
new file mode 100644
index 000000000..be61435a0
--- /dev/null
+++ b/event/src/main/java/de/danoeh/antennapod/event/playback/SleepTimerUpdatedEvent.java
@@ -0,0 +1,38 @@
+package de.danoeh.antennapod.event.playback;
+
+public class SleepTimerUpdatedEvent {
+ private static final long CANCELLED = Long.MAX_VALUE;
+ private final long timeLeft;
+
+ private SleepTimerUpdatedEvent(long timeLeft) {
+ this.timeLeft = timeLeft;
+ }
+
+ public static SleepTimerUpdatedEvent justEnabled(long timeLeft) {
+ return new SleepTimerUpdatedEvent(-timeLeft);
+ }
+
+ public static SleepTimerUpdatedEvent updated(long timeLeft) {
+ return new SleepTimerUpdatedEvent(Math.max(0, timeLeft));
+ }
+
+ public static SleepTimerUpdatedEvent cancelled() {
+ return new SleepTimerUpdatedEvent(CANCELLED);
+ }
+
+ public long getTimeLeft() {
+ return Math.abs(timeLeft);
+ }
+
+ public boolean isOver() {
+ return timeLeft == 0;
+ }
+
+ public boolean wasJustEnabled() {
+ return timeLeft < 0;
+ }
+
+ public boolean isCancelled() {
+ return timeLeft == CANCELLED;
+ }
+}
diff --git a/event/src/main/java/de/danoeh/antennapod/event/playback/SpeedChangedEvent.java b/event/src/main/java/de/danoeh/antennapod/event/playback/SpeedChangedEvent.java
new file mode 100644
index 000000000..243d20628
--- /dev/null
+++ b/event/src/main/java/de/danoeh/antennapod/event/playback/SpeedChangedEvent.java
@@ -0,0 +1,13 @@
+package de.danoeh.antennapod.event.playback;
+
+public class SpeedChangedEvent {
+ private final float newSpeed;
+
+ public SpeedChangedEvent(float newSpeed) {
+ this.newSpeed = newSpeed;
+ }
+
+ public float getNewSpeed() {
+ return newSpeed;
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/event/settings/SkipIntroEndingChangedEvent.java b/event/src/main/java/de/danoeh/antennapod/event/settings/SkipIntroEndingChangedEvent.java
index 583f7b13f..bb6010796 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/event/settings/SkipIntroEndingChangedEvent.java
+++ b/event/src/main/java/de/danoeh/antennapod/event/settings/SkipIntroEndingChangedEvent.java
@@ -1,4 +1,4 @@
-package de.danoeh.antennapod.core.event.settings;
+package de.danoeh.antennapod.event.settings;
public class SkipIntroEndingChangedEvent {
private final int skipIntro;
diff --git a/core/src/main/java/de/danoeh/antennapod/core/event/settings/SpeedPresetChangedEvent.java b/event/src/main/java/de/danoeh/antennapod/event/settings/SpeedPresetChangedEvent.java
index 0ac7e1316..6ca8a1290 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/event/settings/SpeedPresetChangedEvent.java
+++ b/event/src/main/java/de/danoeh/antennapod/event/settings/SpeedPresetChangedEvent.java
@@ -1,4 +1,4 @@
-package de.danoeh.antennapod.core.event.settings;
+package de.danoeh.antennapod.event.settings;
public class SpeedPresetChangedEvent {
private final float speed;
diff --git a/core/src/main/java/de/danoeh/antennapod/core/event/settings/VolumeAdaptionChangedEvent.java b/event/src/main/java/de/danoeh/antennapod/event/settings/VolumeAdaptionChangedEvent.java
index 3905ce68f..5a2c0b63c 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/event/settings/VolumeAdaptionChangedEvent.java
+++ b/event/src/main/java/de/danoeh/antennapod/event/settings/VolumeAdaptionChangedEvent.java
@@ -1,4 +1,4 @@
-package de.danoeh.antennapod.core.event.settings;
+package de.danoeh.antennapod.event.settings;
import de.danoeh.antennapod.model.feed.VolumeAdaptionSetting;
diff --git a/model/src/main/java/de/danoeh/antennapod/model/feed/FeedFilter.java b/model/src/main/java/de/danoeh/antennapod/model/feed/FeedFilter.java
index 31d263b24..3b35fe5bd 100644
--- a/model/src/main/java/de/danoeh/antennapod/model/feed/FeedFilter.java
+++ b/model/src/main/java/de/danoeh/antennapod/model/feed/FeedFilter.java
@@ -10,18 +10,24 @@ import java.util.regex.Pattern;
public class FeedFilter implements Serializable {
private final String includeFilter;
private final String excludeFilter;
+ private final int minimalDuration;
public FeedFilter() {
- this("", "");
+ this("", "", -1);
}
- public FeedFilter(String includeFilter, String excludeFilter) {
+ public FeedFilter(String includeFilter, String excludeFilter, int minimalDuration) {
// We're storing the strings and not the parsed terms because
// 1. It's easier to show the user exactly what they typed in this way
// (we don't have to recreate it)
// 2. We don't know if we'll actually be asked to parse anything anyways.
this.includeFilter = includeFilter;
this.excludeFilter = excludeFilter;
+ this.minimalDuration = minimalDuration;
+ }
+
+ public FeedFilter(String includeFilter, String excludeFilter) {
+ this(includeFilter, excludeFilter, -1);
}
/**
@@ -49,11 +55,20 @@ public class FeedFilter implements Serializable {
List<String> includeTerms = parseTerms(includeFilter);
List<String> excludeTerms = parseTerms(excludeFilter);
- if (includeTerms.size() == 0 && excludeTerms.size() == 0) {
+ if (includeTerms.size() == 0 && excludeTerms.size() == 0 && minimalDuration <= -1) {
// nothing has been specified, so include everything
return true;
}
+ // Check if the episode is long enough if minimal duration filter is on
+ if (hasMinimalDurationFilter() && item.getMedia() != null) {
+ int durationInMs = item.getMedia().getDuration();
+ // Minimal Duration is stored in seconds
+ if (durationInMs > 0 && durationInMs / 1000 < minimalDuration) {
+ return false;
+ }
+ }
+
// check using lowercase so the users don't have to worry about case.
String title = item.getTitle().toLowerCase(Locale.getDefault());
@@ -78,6 +93,12 @@ public class FeedFilter implements Serializable {
return true;
}
+ // if they only set minimal duration filter and arrived here, autodownload
+ // should happen
+ if (hasMinimalDurationFilter()) {
+ return true;
+ }
+
return false;
}
@@ -89,6 +110,10 @@ public class FeedFilter implements Serializable {
return excludeFilter;
}
+ public int getMinimalDurationFilter() {
+ return minimalDuration;
+ }
+
/**
* @return true if only include is set
*/
@@ -110,4 +135,8 @@ public class FeedFilter implements Serializable {
public boolean hasExcludeFilter() {
return excludeFilter.length() > 0;
}
+
+ public boolean hasMinimalDurationFilter() {
+ return minimalDuration > -1;
+ }
}
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 460f50f88..08f79252a 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
@@ -64,12 +64,6 @@ public class FeedItem extends FeedComponent implements Serializable {
private transient List<Chapter> chapters;
private String imageUrl;
- /*
- * 0: auto download disabled
- * 1: auto download enabled (default)
- * > 1: auto download enabled, (approx.) timestamp of the last failed attempt
- * where last digit denotes the number of failed attempts
- */
private long autoDownload = 1;
/**
@@ -361,15 +355,18 @@ public class FeedItem extends FeedComponent implements Serializable {
return hasChapters;
}
- public void setAutoDownload(boolean autoDownload) {
- this.autoDownload = autoDownload ? 1 : 0;
+ public void disableAutoDownload() {
+ this.autoDownload = 0;
}
- public boolean getAutoDownload() {
- return this.autoDownload > 0;
+ public long getAutoDownloadAttemptsAndTime() {
+ return autoDownload;
}
public int getFailedAutoDownloadAttempts() {
+ // 0: auto download disabled
+ // 1: auto download enabled (default)
+ // > 1: auto download enabled, timestamp of last failed attempt, last digit denotes number of failed attempts
if (autoDownload <= 1) {
return 0;
}
@@ -380,23 +377,33 @@ public class FeedItem extends FeedComponent implements Serializable {
return failedAttempts;
}
- public boolean isDownloaded() {
- return media != null && media.isDownloaded();
+ public void increaseFailedAutoDownloadAttempts(long now) {
+ if (autoDownload == 0) {
+ return; // Don't re-enable
+ }
+ int failedAttempts = getFailedAutoDownloadAttempts() + 1;
+ if (failedAttempts >= 5) {
+ disableAutoDownload(); // giving up
+ } else {
+ autoDownload = (now / 10) * 10 + failedAttempts;
+ }
}
- public boolean isAutoDownloadable() {
+ public boolean isAutoDownloadable(long now) {
if (media == null || media.isDownloaded() || autoDownload == 0) {
return false;
}
if (autoDownload == 1) {
- return true;
+ return true; // Never failed
}
int failedAttempts = getFailedAutoDownloadAttempts();
- double magicValue = 1.767; // 1.767^(10[=#maxNumAttempts]-1) = 168 hours / 7 days
- int millisecondsInHour = 3600000;
- long waitingTime = (long) (Math.pow(magicValue, failedAttempts - 1) * millisecondsInHour);
- long grace = TimeUnit.MINUTES.toMillis(5);
- return System.currentTimeMillis() > (autoDownload + waitingTime - grace);
+ long waitingTime = TimeUnit.HOURS.toMillis((long) Math.pow(2, failedAttempts - 1));
+ long lastAttempt = (autoDownload / 10) * 10;
+ return now >= (lastAttempt + waitingTime);
+ }
+
+ public boolean isDownloaded() {
+ return media != null && media.isDownloaded();
}
/**
diff --git a/net/sync/gpoddernet/build.gradle b/net/sync/gpoddernet/build.gradle
index eb5af1b60..13674b5c3 100644
--- a/net/sync/gpoddernet/build.gradle
+++ b/net/sync/gpoddernet/build.gradle
@@ -9,4 +9,7 @@ dependencies {
implementation "com.squareup.okhttp3:okhttp:$okhttpVersion"
implementation "org.apache.commons:commons-lang3:$commonslangVersion"
+ implementation "commons-io:commons-io:$commonsioVersion"
+ implementation "io.reactivex.rxjava2:rxandroid:$rxAndroidVersion"
+ implementation "io.reactivex.rxjava2:rxjava:$rxJavaVersion"
}
diff --git a/net/sync/gpoddernet/src/main/java/de/danoeh/antennapod/net/sync/HostnameParser.java b/net/sync/gpoddernet/src/main/java/de/danoeh/antennapod/net/sync/HostnameParser.java
new file mode 100644
index 000000000..ebb415248
--- /dev/null
+++ b/net/sync/gpoddernet/src/main/java/de/danoeh/antennapod/net/sync/HostnameParser.java
@@ -0,0 +1,41 @@
+package de.danoeh.antennapod.net.sync;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class HostnameParser {
+ public String scheme;
+ public int port;
+ public String host;
+
+ // split into schema, host and port - missing parts are null
+ private static final Pattern URLSPLIT_REGEX = Pattern.compile("(?:(https?)://)?([^:]+)(?::(\\d+))?");
+
+ public HostnameParser(String hosturl) {
+ Matcher m = URLSPLIT_REGEX.matcher(hosturl);
+ if (m.matches()) {
+ scheme = m.group(1);
+ host = m.group(2);
+ if (m.group(3) == null) {
+ port = -1;
+ } else {
+ port = Integer.parseInt(m.group(3)); // regex -> can only be digits
+ }
+ } else {
+ // URL does not match regex: use it anyway -> this will cause an exception on connect
+ scheme = "https";
+ host = hosturl;
+ port = 443;
+ }
+
+ if (scheme == null) { // assume https
+ scheme = "https";
+ }
+
+ if (scheme.equals("https") && port == -1) {
+ port = 443;
+ } else if (scheme.equals("http") && port == -1) {
+ port = 80;
+ }
+ }
+}
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 eb18da80b..21a362a40 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
@@ -1,25 +1,10 @@
package de.danoeh.antennapod.net.sync.gpoddernet;
import android.util.Log;
+
import androidx.annotation.NonNull;
-import de.danoeh.antennapod.net.sync.gpoddernet.model.GpodnetDevice;
-import de.danoeh.antennapod.net.sync.model.EpisodeAction;
-import de.danoeh.antennapod.net.sync.model.EpisodeActionChanges;
-import de.danoeh.antennapod.net.sync.gpoddernet.model.GpodnetEpisodeActionPostResponse;
-import de.danoeh.antennapod.net.sync.gpoddernet.model.GpodnetPodcast;
-import de.danoeh.antennapod.net.sync.model.ISyncService;
-import de.danoeh.antennapod.net.sync.model.SubscriptionChanges;
-import de.danoeh.antennapod.net.sync.gpoddernet.model.GpodnetTag;
-import de.danoeh.antennapod.net.sync.gpoddernet.model.GpodnetUploadChangesResponse;
-import de.danoeh.antennapod.net.sync.model.SyncServiceException;
-import de.danoeh.antennapod.net.sync.model.UploadChangesResponse;
-import okhttp3.Credentials;
-import okhttp3.MediaType;
-import okhttp3.OkHttpClient;
-import okhttp3.Request;
-import okhttp3.RequestBody;
-import okhttp3.Response;
-import okhttp3.ResponseBody;
+
+import de.danoeh.antennapod.net.sync.HostnameParser;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
@@ -35,12 +20,28 @@ import java.net.URL;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collections;
-import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
+import de.danoeh.antennapod.net.sync.gpoddernet.mapper.ResponseMapper;
+import de.danoeh.antennapod.net.sync.gpoddernet.model.GpodnetDevice;
+import de.danoeh.antennapod.net.sync.gpoddernet.model.GpodnetEpisodeActionPostResponse;
+import de.danoeh.antennapod.net.sync.gpoddernet.model.GpodnetPodcast;
+import de.danoeh.antennapod.net.sync.gpoddernet.model.GpodnetTag;
+import de.danoeh.antennapod.net.sync.gpoddernet.model.GpodnetUploadChangesResponse;
+import de.danoeh.antennapod.net.sync.model.EpisodeAction;
+import de.danoeh.antennapod.net.sync.model.EpisodeActionChanges;
+import de.danoeh.antennapod.net.sync.model.ISyncService;
+import de.danoeh.antennapod.net.sync.model.SubscriptionChanges;
+import de.danoeh.antennapod.net.sync.model.SyncServiceException;
+import de.danoeh.antennapod.net.sync.model.UploadChangesResponse;
+import okhttp3.Credentials;
+import okhttp3.MediaType;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+import okhttp3.ResponseBody;
/**
* Communicates with the gpodder.net service.
@@ -61,43 +62,16 @@ public class GpodnetService implements ISyncService {
private final OkHttpClient httpClient;
- // split into schema, host and port - missing parts are null
- private static final Pattern URLSPLIT_REGEX = Pattern.compile("(?:(https?)://)?([^:]+)(?::(\\d+))?");
-
public GpodnetService(OkHttpClient httpClient, String baseHosturl,
String deviceId, String username, String password) {
this.httpClient = httpClient;
this.deviceId = deviceId;
this.username = username;
this.password = password;
-
- Matcher m = URLSPLIT_REGEX.matcher(baseHosturl);
- if (m.matches()) {
- this.baseScheme = m.group(1);
- this.baseHost = m.group(2);
- if (m.group(3) == null) {
- this.basePort = -1;
- } else {
- this.basePort = Integer.parseInt(m.group(3)); // regex -> can only be digits
- }
- } else {
- // URL does not match regex: use it anyway -> this will cause an exception on connect
- this.baseScheme = "https";
- this.baseHost = baseHosturl;
- this.basePort = 443;
- }
-
- if (this.baseScheme == null) { // assume https
- this.baseScheme = "https";
- }
-
- if (this.baseScheme.equals("https") && this.basePort == -1) {
- this.basePort = 443;
- }
-
- if (this.baseScheme.equals("http") && this.basePort == -1) {
- this.basePort = 80;
- }
+ HostnameParser hostname = new HostnameParser(baseHosturl == null ? DEFAULT_BASE_HOST : baseHosturl);
+ this.baseHost = hostname.host;
+ this.basePort = hostname.port;
+ this.baseScheme = hostname.scheme;
}
private void requireLoggedIn() {
@@ -434,7 +408,7 @@ public class GpodnetService implements ISyncService {
String response = executeRequest(request);
JSONObject changes = new JSONObject(response);
- return readSubscriptionChangesFromJsonObject(changes);
+ return ResponseMapper.readSubscriptionChangesFromJsonObject(changes);
} catch (URISyntaxException e) {
e.printStackTrace();
throw new IllegalStateException(e);
@@ -515,7 +489,7 @@ public class GpodnetService implements ISyncService {
String response = executeRequest(request);
JSONObject json = new JSONObject(response);
- return readEpisodeActionsFromJsonObject(json);
+ return ResponseMapper.readEpisodeActionsFromJsonObject(json);
} catch (URISyntaxException e) {
e.printStackTrace();
throw new IllegalStateException(e);
@@ -526,7 +500,6 @@ public class GpodnetService implements ISyncService {
}
-
/**
* Logs in a specific user. This method must be called if any of the methods
* that require authentication is used.
@@ -615,7 +588,13 @@ public class GpodnetService implements ISyncService {
e.printStackTrace();
}
}
- throw new GpodnetServiceBadStatusCodeException("Bad response code: " + responseCode, responseCode);
+ if (responseCode >= 500) {
+ throw new GpodnetServiceBadStatusCodeException("Gpodder.net is currently unavailable (code "
+ + responseCode + ")", responseCode);
+ } else {
+ throw new GpodnetServiceBadStatusCodeException("Unable to connect to Gpodder.net (code "
+ + responseCode + ": " + response.message() + ")", responseCode);
+ }
}
}
}
@@ -689,48 +668,6 @@ public class GpodnetService implements ISyncService {
return new GpodnetDevice(id, caption, type, subscriptions);
}
- private SubscriptionChanges readSubscriptionChangesFromJsonObject(@NonNull JSONObject object)
- throws JSONException {
-
- List<String> added = new LinkedList<>();
- JSONArray jsonAdded = object.getJSONArray("add");
- for (int i = 0; i < jsonAdded.length(); i++) {
- String addedUrl = jsonAdded.getString(i);
- // gpodder escapes colons unnecessarily
- addedUrl = addedUrl.replace("%3A", ":");
- added.add(addedUrl);
- }
-
- List<String> removed = new LinkedList<>();
- JSONArray jsonRemoved = object.getJSONArray("remove");
- for (int i = 0; i < jsonRemoved.length(); i++) {
- String removedUrl = jsonRemoved.getString(i);
- // gpodder escapes colons unnecessarily
- removedUrl = removedUrl.replace("%3A", ":");
- removed.add(removedUrl);
- }
-
- long timestamp = object.getLong("timestamp");
- return new SubscriptionChanges(added, removed, timestamp);
- }
-
- private EpisodeActionChanges readEpisodeActionsFromJsonObject(@NonNull JSONObject object)
- throws JSONException {
-
- List<EpisodeAction> episodeActions = new ArrayList<>();
-
- long timestamp = object.getLong("timestamp");
- JSONArray jsonActions = object.getJSONArray("actions");
- for (int i = 0; i < jsonActions.length(); i++) {
- JSONObject jsonAction = jsonActions.getJSONObject(i);
- EpisodeAction episodeAction = EpisodeAction.readFromJsonObject(jsonAction);
- if (episodeAction != null) {
- episodeActions.add(episodeAction);
- }
- }
- return new EpisodeActionChanges(episodeActions, timestamp);
- }
-
@Override
public void logout() {
diff --git a/net/sync/gpoddernet/src/main/java/de/danoeh/antennapod/net/sync/gpoddernet/mapper/ResponseMapper.java b/net/sync/gpoddernet/src/main/java/de/danoeh/antennapod/net/sync/gpoddernet/mapper/ResponseMapper.java
new file mode 100644
index 000000000..c8e607d74
--- /dev/null
+++ b/net/sync/gpoddernet/src/main/java/de/danoeh/antennapod/net/sync/gpoddernet/mapper/ResponseMapper.java
@@ -0,0 +1,60 @@
+package de.danoeh.antennapod.net.sync.gpoddernet.mapper;
+
+import androidx.annotation.NonNull;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+
+import de.danoeh.antennapod.net.sync.model.EpisodeAction;
+import de.danoeh.antennapod.net.sync.model.EpisodeActionChanges;
+import de.danoeh.antennapod.net.sync.model.SubscriptionChanges;
+
+public class ResponseMapper {
+
+ public static SubscriptionChanges readSubscriptionChangesFromJsonObject(@NonNull JSONObject object)
+ throws JSONException {
+
+ List<String> added = new LinkedList<>();
+ JSONArray jsonAdded = object.getJSONArray("add");
+ for (int i = 0; i < jsonAdded.length(); i++) {
+ String addedUrl = jsonAdded.getString(i);
+ // gpodder escapes colons unnecessarily
+ addedUrl = addedUrl.replace("%3A", ":");
+ added.add(addedUrl);
+ }
+
+ List<String> removed = new LinkedList<>();
+ JSONArray jsonRemoved = object.getJSONArray("remove");
+ for (int i = 0; i < jsonRemoved.length(); i++) {
+ String removedUrl = jsonRemoved.getString(i);
+ // gpodder escapes colons unnecessarily
+ removedUrl = removedUrl.replace("%3A", ":");
+ removed.add(removedUrl);
+ }
+
+ long timestamp = object.getLong("timestamp");
+ return new SubscriptionChanges(added, removed, timestamp);
+ }
+
+ public static EpisodeActionChanges readEpisodeActionsFromJsonObject(@NonNull JSONObject object)
+ throws JSONException {
+
+ List<EpisodeAction> episodeActions = new ArrayList<>();
+
+ long timestamp = object.getLong("timestamp");
+ JSONArray jsonActions = object.getJSONArray("actions");
+ for (int i = 0; i < jsonActions.length(); i++) {
+ JSONObject jsonAction = jsonActions.getJSONObject(i);
+ EpisodeAction episodeAction = EpisodeAction.readFromJsonObject(jsonAction);
+ if (episodeAction != null) {
+ episodeActions.add(episodeAction);
+ }
+ }
+ return new EpisodeActionChanges(episodeActions, timestamp);
+ }
+}
diff --git a/net/sync/gpoddernet/src/main/java/de/danoeh/antennapod/net/sync/nextcloud/NextcloudLoginFlow.java b/net/sync/gpoddernet/src/main/java/de/danoeh/antennapod/net/sync/nextcloud/NextcloudLoginFlow.java
new file mode 100644
index 000000000..b66c44402
--- /dev/null
+++ b/net/sync/gpoddernet/src/main/java/de/danoeh/antennapod/net/sync/nextcloud/NextcloudLoginFlow.java
@@ -0,0 +1,107 @@
+package de.danoeh.antennapod.net.sync.nextcloud;
+
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import de.danoeh.antennapod.net.sync.HostnameParser;
+import io.reactivex.Observable;
+import io.reactivex.android.schedulers.AndroidSchedulers;
+import io.reactivex.disposables.Disposable;
+import io.reactivex.schedulers.Schedulers;
+import okhttp3.MediaType;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+import okhttp3.ResponseBody;
+import org.json.JSONException;
+import org.json.JSONObject;
+import android.util.Log;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URL;
+import java.util.concurrent.TimeUnit;
+
+public class NextcloudLoginFlow {
+ private static final String TAG = "NextcloudLoginFlow";
+
+ private final OkHttpClient httpClient;
+ private final HostnameParser hostname;
+ private final Context context;
+ private final AuthenticationCallback callback;
+ private String token;
+ private String endpoint;
+ private Disposable startDisposable;
+ private Disposable pollDisposable;
+
+ public NextcloudLoginFlow(OkHttpClient httpClient, String hostUrl, Context context,
+ AuthenticationCallback callback) {
+ this.httpClient = httpClient;
+ this.hostname = new HostnameParser(hostUrl);
+ this.context = context;
+ this.callback = callback;
+ }
+
+ public void start() {
+ startDisposable = Observable.fromCallable(() -> {
+ URL url = new URI(hostname.scheme, null, hostname.host, hostname.port,
+ "/index.php/login/v2", null, null).toURL();
+ JSONObject result = doRequest(url, "");
+ String loginUrl = result.getString("login");
+ this.token = result.getJSONObject("poll").getString("token");
+ this.endpoint = result.getJSONObject("poll").getString("endpoint");
+ return loginUrl;
+ })
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(
+ result -> {
+ Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(result));
+ context.startActivity(browserIntent);
+ poll();
+ }, error -> {
+ Log.e(TAG, Log.getStackTraceString(error));
+ callback.onNextcloudAuthError(error.getLocalizedMessage());
+ });
+ }
+
+ private void poll() {
+ pollDisposable = Observable.fromCallable(() -> doRequest(URI.create(endpoint).toURL(), "token=" + token))
+ .delay(1, TimeUnit.SECONDS)
+ .retry(60 * 10) // 10 minutes
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(result -> {
+ callback.onNextcloudAuthenticated(result.getString("server"),
+ result.getString("loginName"), result.getString("appPassword"));
+ }, Throwable::printStackTrace);
+ }
+
+ public void cancel() {
+ if (startDisposable != null) {
+ startDisposable.dispose();
+ }
+ if (pollDisposable != null) {
+ pollDisposable.dispose();
+ }
+ }
+
+ private JSONObject doRequest(URL url, String bodyContent) throws IOException, JSONException {
+ RequestBody requestBody = RequestBody.create(
+ MediaType.get("application/x-www-form-urlencoded"), bodyContent);
+ Request request = new Request.Builder().url(url).method("POST", requestBody).build();
+ Response response = httpClient.newCall(request).execute();
+ if (response.code() != 200) {
+ throw new IOException("Return code " + response.code());
+ }
+ ResponseBody body = response.body();
+ return new JSONObject(body.string());
+ }
+
+ public interface AuthenticationCallback {
+ void onNextcloudAuthenticated(String server, String username, String password);
+
+ void onNextcloudAuthError(String errorMessage);
+ }
+}
diff --git a/net/sync/gpoddernet/src/main/java/de/danoeh/antennapod/net/sync/nextcloud/NextcloudSyncService.java b/net/sync/gpoddernet/src/main/java/de/danoeh/antennapod/net/sync/nextcloud/NextcloudSyncService.java
new file mode 100644
index 000000000..647a9073c
--- /dev/null
+++ b/net/sync/gpoddernet/src/main/java/de/danoeh/antennapod/net/sync/nextcloud/NextcloudSyncService.java
@@ -0,0 +1,169 @@
+package de.danoeh.antennapod.net.sync.nextcloud;
+
+import de.danoeh.antennapod.net.sync.HostnameParser;
+import de.danoeh.antennapod.net.sync.gpoddernet.mapper.ResponseMapper;
+import de.danoeh.antennapod.net.sync.gpoddernet.model.GpodnetUploadChangesResponse;
+import de.danoeh.antennapod.net.sync.model.EpisodeAction;
+import de.danoeh.antennapod.net.sync.model.EpisodeActionChanges;
+import de.danoeh.antennapod.net.sync.model.ISyncService;
+import de.danoeh.antennapod.net.sync.model.SubscriptionChanges;
+import de.danoeh.antennapod.net.sync.model.SyncServiceException;
+import de.danoeh.antennapod.net.sync.model.UploadChangesResponse;
+import okhttp3.Credentials;
+import okhttp3.HttpUrl;
+import okhttp3.MediaType;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.util.HashMap;
+import java.util.List;
+
+public class NextcloudSyncService implements ISyncService {
+ private static final int UPLOAD_BULK_SIZE = 30;
+ private final OkHttpClient httpClient;
+ private final String baseScheme;
+ private final int basePort;
+ private final String baseHost;
+ private final String username;
+ private final String password;
+
+ public NextcloudSyncService(OkHttpClient httpClient, String baseHosturl,
+ String username, String password) {
+ this.httpClient = httpClient;
+ this.username = username;
+ this.password = password;
+ HostnameParser hostname = new HostnameParser(baseHosturl);
+ this.baseHost = hostname.host;
+ this.basePort = hostname.port;
+ this.baseScheme = hostname.scheme;
+ }
+
+ @Override
+ public void login() {
+ }
+
+ @Override
+ public SubscriptionChanges getSubscriptionChanges(long lastSync) throws SyncServiceException {
+ try {
+ HttpUrl.Builder url = makeUrl("/index.php/apps/gpoddersync/subscriptions");
+ url.addQueryParameter("since", "" + lastSync);
+ String responseString = performRequest(url, "GET", null);
+ JSONObject json = new JSONObject(responseString);
+ return ResponseMapper.readSubscriptionChangesFromJsonObject(json);
+ } catch (JSONException | MalformedURLException e) {
+ e.printStackTrace();
+ throw new SyncServiceException(e);
+ } catch (Exception e) {
+ e.printStackTrace();
+ throw new SyncServiceException(e);
+ }
+ }
+
+ @Override
+ public UploadChangesResponse uploadSubscriptionChanges(List<String> addedFeeds,
+ List<String> removedFeeds)
+ throws NextcloudSynchronizationServiceException {
+ try {
+ HttpUrl.Builder url = makeUrl("/index.php/apps/gpoddersync/subscription_change/create");
+ final JSONObject requestObject = new JSONObject();
+ requestObject.put("add", new JSONArray(addedFeeds));
+ requestObject.put("remove", new JSONArray(removedFeeds));
+ RequestBody requestBody = RequestBody.create(
+ MediaType.get("application/json"), requestObject.toString());
+ performRequest(url, "POST", requestBody);
+ } catch (Exception e) {
+ e.printStackTrace();
+ throw new NextcloudSynchronizationServiceException(e);
+ }
+
+ return new GpodnetUploadChangesResponse(System.currentTimeMillis() / 1000, new HashMap<>());
+ }
+
+ @Override
+ public EpisodeActionChanges getEpisodeActionChanges(long timestamp) throws SyncServiceException {
+ try {
+ HttpUrl.Builder uri = makeUrl("/index.php/apps/gpoddersync/episode_action");
+ uri.addQueryParameter("since", "" + timestamp);
+ String responseString = performRequest(uri, "GET", null);
+ JSONObject json = new JSONObject(responseString);
+ return ResponseMapper.readEpisodeActionsFromJsonObject(json);
+ } catch (JSONException | MalformedURLException e) {
+ e.printStackTrace();
+ throw new SyncServiceException(e);
+ } catch (Exception e) {
+ e.printStackTrace();
+ throw new SyncServiceException(e);
+ }
+ }
+
+ @Override
+ public UploadChangesResponse uploadEpisodeActions(List<EpisodeAction> queuedEpisodeActions)
+ throws NextcloudSynchronizationServiceException {
+ for (int i = 0; i < queuedEpisodeActions.size(); i += UPLOAD_BULK_SIZE) {
+ uploadEpisodeActionsPartial(queuedEpisodeActions,
+ i, Math.min(queuedEpisodeActions.size(), i + UPLOAD_BULK_SIZE));
+ }
+ return new NextcloudGpodderEpisodeActionPostResponse(System.currentTimeMillis() / 1000);
+ }
+
+ private void uploadEpisodeActionsPartial(List<EpisodeAction> queuedEpisodeActions, int from, int to)
+ throws NextcloudSynchronizationServiceException {
+ try {
+ final JSONArray list = new JSONArray();
+ for (int i = from; i < to; i++) {
+ EpisodeAction episodeAction = queuedEpisodeActions.get(i);
+ JSONObject obj = episodeAction.writeToJsonObject();
+ if (obj != null) {
+ list.put(obj);
+ }
+ }
+ HttpUrl.Builder url = makeUrl("/index.php/apps/gpoddersync/episode_action/create");
+ RequestBody requestBody = RequestBody.create(
+ MediaType.get("application/json"), list.toString());
+ performRequest(url, "POST", requestBody);
+ } catch (Exception e) {
+ e.printStackTrace();
+ throw new NextcloudSynchronizationServiceException(e);
+ }
+ }
+
+ private String performRequest(HttpUrl.Builder url, String method, RequestBody body) throws IOException {
+ Request request = new Request.Builder()
+ .url(url.build())
+ .header("Authorization", Credentials.basic(username, password))
+ .header("Accept", "application/json")
+ .method(method, body)
+ .build();
+ Response response = httpClient.newCall(request).execute();
+ if (response.code() != 200) {
+ throw new IOException("Response code: " + response.code());
+ }
+ return response.body().string();
+ }
+
+ private HttpUrl.Builder makeUrl(String path) {
+ return new HttpUrl.Builder()
+ .scheme(baseScheme)
+ .host(baseHost)
+ .port(basePort)
+ .addPathSegments(path);
+ }
+
+ @Override
+ public void logout() {
+ }
+
+ private static class NextcloudGpodderEpisodeActionPostResponse extends UploadChangesResponse {
+ public NextcloudGpodderEpisodeActionPostResponse(long epochSecond) {
+ super(epochSecond);
+ }
+ }
+}
+
diff --git a/net/sync/gpoddernet/src/main/java/de/danoeh/antennapod/net/sync/nextcloud/NextcloudSynchronizationServiceException.java b/net/sync/gpoddernet/src/main/java/de/danoeh/antennapod/net/sync/nextcloud/NextcloudSynchronizationServiceException.java
new file mode 100644
index 000000000..d907c229e
--- /dev/null
+++ b/net/sync/gpoddernet/src/main/java/de/danoeh/antennapod/net/sync/nextcloud/NextcloudSynchronizationServiceException.java
@@ -0,0 +1,9 @@
+package de.danoeh.antennapod.net.sync.nextcloud;
+
+import de.danoeh.antennapod.net.sync.model.SyncServiceException;
+
+public class NextcloudSynchronizationServiceException extends SyncServiceException {
+ public NextcloudSynchronizationServiceException(Throwable e) {
+ super(e);
+ }
+}
diff --git a/net/sync/model/build.gradle b/net/sync/model/build.gradle
index e47040892..72d962536 100644
--- a/net/sync/model/build.gradle
+++ b/net/sync/model/build.gradle
@@ -5,5 +5,4 @@ dependencies {
implementation project(':model')
annotationProcessor "androidx.annotation:annotation:$annotationVersion"
- implementation "androidx.appcompat:appcompat:$appcompatVersion"
}
diff --git a/net/sync/model/src/main/java/de/danoeh/antennapod/net/sync/model/EpisodeAction.java b/net/sync/model/src/main/java/de/danoeh/antennapod/net/sync/model/EpisodeAction.java
index da398d83e..42fbdb310 100644
--- a/net/sync/model/src/main/java/de/danoeh/antennapod/net/sync/model/EpisodeAction.java
+++ b/net/sync/model/src/main/java/de/danoeh/antennapod/net/sync/model/EpisodeAction.java
@@ -4,7 +4,6 @@ import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.NonNull;
-import androidx.core.util.ObjectsCompat;
import org.json.JSONException;
import org.json.JSONObject;
@@ -13,6 +12,7 @@ import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
+import java.util.Objects;
import java.util.TimeZone;
import de.danoeh.antennapod.model.feed.FeedItem;
@@ -159,10 +159,10 @@ public class EpisodeAction {
&& position == that.position
&& total == that.total
&& action != that.action
- && ObjectsCompat.equals(podcast, that.podcast)
- && ObjectsCompat.equals(episode, that.episode)
- && ObjectsCompat.equals(timestamp, that.timestamp)
- && ObjectsCompat.equals(guid, that.guid);
+ && Objects.equals(podcast, that.podcast)
+ && Objects.equals(episode, that.episode)
+ && Objects.equals(timestamp, that.timestamp)
+ && Objects.equals(guid, that.guid);
}
@Override
diff --git a/parser/feed/build.gradle b/parser/feed/build.gradle
index 4909d3fac..774e08a66 100644
--- a/parser/feed/build.gradle
+++ b/parser/feed/build.gradle
@@ -18,6 +18,6 @@ dependencies {
implementation "commons-io:commons-io:$commonsioVersion"
implementation "org.jsoup:jsoup:$jsoupVersion"
- testImplementation 'junit:junit:4.13'
- testImplementation 'org.robolectric:robolectric:4.5-alpha-1'
+ testImplementation "junit:junit:$junitVersion"
+ testImplementation "org.robolectric:robolectric:$robolectricVersion"
}
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 5f47f8377..63d8dd476 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
@@ -49,66 +49,33 @@ public class Itunes extends Namespace {
return;
}
- if (AUTHOR.equals(localName)) {
- parseAuthor(state);
- } else if (DURATION.equals(localName)) {
- parseDuration(state);
- } else if (SUBTITLE.equals(localName)) {
- parseSubtitle(state);
- } else if (SUMMARY.equals(localName)) {
- SyndElement secondElement = state.getSecondTag();
- parseSummary(state, secondElement.getName());
- }
- }
-
- private void parseAuthor(HandlerState state) {
- if (state.getFeed() != null) {
- String author = state.getContentBuf().toString();
- state.getFeed().setAuthor(HtmlCompat.fromHtml(author,
- HtmlCompat.FROM_HTML_MODE_LEGACY).toString());
- }
- }
-
- private void parseDuration(HandlerState state) {
- String durationStr = state.getContentBuf().toString();
- if (TextUtils.isEmpty(durationStr)) {
+ String content = state.getContentBuf().toString();
+ String contentFromHtml = HtmlCompat.fromHtml(content, HtmlCompat.FROM_HTML_MODE_COMPACT).toString();
+ if (TextUtils.isEmpty(content)) {
return;
}
- try {
- long durationMs = DurationParser.inMillis(durationStr);
- state.getTempObjects().put(DURATION, (int) durationMs);
- } catch (NumberFormatException e) {
- Log.e(NSTAG, String.format("Duration '%s' could not be parsed", durationStr));
- }
- }
-
- private void parseSubtitle(HandlerState state) {
- String subtitle = state.getContentBuf().toString();
- if (TextUtils.isEmpty(subtitle)) {
- return;
- }
- if (state.getCurrentItem() != null) {
- if (TextUtils.isEmpty(state.getCurrentItem().getDescription())) {
- state.getCurrentItem().setDescriptionIfLonger(subtitle);
+ if (AUTHOR.equals(localName) && state.getFeed() != null) {
+ state.getFeed().setAuthor(contentFromHtml);
+ } else if (DURATION.equals(localName)) {
+ try {
+ long durationMs = DurationParser.inMillis(content);
+ state.getTempObjects().put(DURATION, (int) durationMs);
+ } catch (NumberFormatException e) {
+ Log.e(NSTAG, String.format("Duration '%s' could not be parsed", content));
}
- } else {
- if (state.getFeed() != null && TextUtils.isEmpty(state.getFeed().getDescription())) {
- state.getFeed().setDescription(subtitle);
+ } else if (SUBTITLE.equals(localName)) {
+ if (state.getCurrentItem() != null && TextUtils.isEmpty(state.getCurrentItem().getDescription())) {
+ state.getCurrentItem().setDescriptionIfLonger(content);
+ } else if (state.getFeed() != null && TextUtils.isEmpty(state.getFeed().getDescription())) {
+ state.getFeed().setDescription(content);
+ }
+ } else if (SUMMARY.equals(localName)) {
+ if (state.getCurrentItem() != null) {
+ state.getCurrentItem().setDescriptionIfLonger(content);
+ } else if (Rss20.CHANNEL.equals(state.getSecondTag().getName()) && state.getFeed() != null) {
+ state.getFeed().setDescription(content);
}
- }
- }
-
- private void parseSummary(HandlerState state, String secondElementName) {
- String summary = state.getContentBuf().toString();
- if (TextUtils.isEmpty(summary)) {
- return;
- }
-
- if (state.getCurrentItem() != null) {
- state.getCurrentItem().setDescriptionIfLonger(summary);
- } else if (Rss20.CHANNEL.equals(secondElementName) && state.getFeed() != null) {
- state.getFeed().setDescription(summary);
}
}
}
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 a49cd16dd..a39e1b5b7 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
@@ -3,6 +3,7 @@ package de.danoeh.antennapod.parser.feed.namespace;
import android.text.TextUtils;
import android.util.Log;
+import androidx.core.text.HtmlCompat;
import de.danoeh.antennapod.parser.feed.HandlerState;
import de.danoeh.antennapod.parser.feed.element.SyndElement;
import de.danoeh.antennapod.parser.feed.util.DateUtils;
@@ -39,14 +40,12 @@ public class Rss20 extends Namespace {
private static final String ENC_TYPE = "type";
@Override
- public SyndElement handleElementStart(String localName, HandlerState state,
- Attributes attributes) {
- if (ITEM.equals(localName)) {
+ public SyndElement handleElementStart(String localName, HandlerState state, Attributes attributes) {
+ if (ITEM.equals(localName) && CHANNEL.equals(state.getTagstack().lastElement().getName())) {
state.setCurrentItem(new FeedItem());
state.getItems().add(state.getCurrentItem());
state.getCurrentItem().setFeed(state.getFeed());
-
- } else if (ENCLOSURE.equals(localName)) {
+ } else if (ENCLOSURE.equals(localName) && ITEM.equals(state.getTagstack().peek().getName())) {
String type = attributes.getValue(ENC_TYPE);
String url = attributes.getValue(ENC_URL);
@@ -72,7 +71,6 @@ public class Rss20 extends Namespace {
FeedMedia media = new FeedMedia(state.getCurrentItem(), url, size, type);
state.getCurrentItem().setMedia(media);
}
-
}
return new SyndElement(localName, this);
}
@@ -100,6 +98,7 @@ public class Rss20 extends Namespace {
} else if (state.getTagstack().size() >= 2 && state.getContentBuf() != null) {
String contentRaw = state.getContentBuf().toString();
String content = SyndStringUtils.trimAllWhitespace(contentRaw);
+ String contentFromHtml = HtmlCompat.fromHtml(content, HtmlCompat.FROM_HTML_MODE_COMPACT).toString();
SyndElement topElement = state.getTagstack().peek();
String top = topElement.getName();
SyndElement secondElement = state.getSecondTag();
@@ -116,9 +115,9 @@ public class Rss20 extends Namespace {
}
} else if (TITLE.equals(top)) {
if (ITEM.equals(second) && state.getCurrentItem() != null) {
- state.getCurrentItem().setTitle(content);
+ state.getCurrentItem().setTitle(contentFromHtml);
} else if (CHANNEL.equals(second) && state.getFeed() != null) {
- state.getFeed().setTitle(content);
+ state.getFeed().setTitle(contentFromHtml);
}
} else if (LINK.equals(top)) {
if (CHANNEL.equals(second) && state.getFeed() != null) {
@@ -135,9 +134,9 @@ public class Rss20 extends Namespace {
}
} else if (DESCR.equals(localName)) {
if (CHANNEL.equals(second) && state.getFeed() != null) {
- state.getFeed().setDescription(content);
+ state.getFeed().setDescription(contentFromHtml);
} else if (ITEM.equals(second) && state.getCurrentItem() != null) {
- state.getCurrentItem().setDescriptionIfLonger(content);
+ state.getCurrentItem().setDescriptionIfLonger(contentFromHtml);
}
} else if (LANGUAGE.equals(localName) && state.getFeed() != null) {
state.getFeed().setLanguage(content.toLowerCase(Locale.US));
diff --git a/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/util/TypeGetter.java b/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/util/TypeGetter.java
index 12834f94f..714dbb9ac 100644
--- a/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/util/TypeGetter.java
+++ b/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/util/TypeGetter.java
@@ -73,7 +73,12 @@ public class TypeGetter {
throw new UnsupportedFeedtypeException(Type.INVALID, tag);
}
} else {
- eventType = xpp.next();
+ try {
+ eventType = xpp.next();
+ } catch (RuntimeException e) {
+ // Apparently this happens on some devices...
+ throw new UnsupportedFeedtypeException("Unable to get type");
+ }
}
}
} catch (XmlPullParserException e) {
diff --git a/parser/feed/src/test/java/de/danoeh/antennapod/parser/feed/element/namespace/RssParserTest.java b/parser/feed/src/test/java/de/danoeh/antennapod/parser/feed/element/namespace/RssParserTest.java
index 8f8942d7b..88ac5c731 100644
--- a/parser/feed/src/test/java/de/danoeh/antennapod/parser/feed/element/namespace/RssParserTest.java
+++ b/parser/feed/src/test/java/de/danoeh/antennapod/parser/feed/element/namespace/RssParserTest.java
@@ -96,4 +96,12 @@ public class RssParserTest {
assertTrue(TextUtils.isEmpty(feed.getPaymentLinks().get(2).content));
assertEquals("https://example.com/funding3", feed.getPaymentLinks().get(2).url);
}
+
+ @Test
+ public void testUnsupportedElements() throws Exception {
+ File feedFile = FeedParserTestHelper.getFeedFile("feed-rss-testUnsupportedElements.xml");
+ Feed feed = FeedParserTestHelper.runFeedParser(feedFile);
+ assertEquals(1, feed.getItems().size());
+ assertEquals("item-0", feed.getItems().get(0).getTitle());
+ }
}
diff --git a/parser/feed/src/test/resources/feed-rss-testUnsupportedElements.xml b/parser/feed/src/test/resources/feed-rss-testUnsupportedElements.xml
new file mode 100644
index 000000000..f21ca7ebd
--- /dev/null
+++ b/parser/feed/src/test/resources/feed-rss-testUnsupportedElements.xml
@@ -0,0 +1,14 @@
+<?xml version='1.0' encoding='UTF-8' ?>
+<rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
+ <channel>
+ <title>title</title>
+ <item>
+ <title>item-0</title>
+ </item>
+ <unsupported-element>
+ <item>
+ <title>item-1</title>
+ </item>
+ </unsupported-element>
+ </channel>
+</rss>
diff --git a/parser/media/build.gradle b/parser/media/build.gradle
index c6ae6964a..106247d1d 100644
--- a/parser/media/build.gradle
+++ b/parser/media/build.gradle
@@ -8,5 +8,5 @@ dependencies {
implementation "commons-io:commons-io:$commonsioVersion"
- testImplementation 'junit:junit:4.13'
+ testImplementation "junit:junit:$junitVersion"
}
diff --git a/parser/media/src/main/java/de/danoeh/antennapod/parser/media/vorbis/OggInputStream.java b/parser/media/src/main/java/de/danoeh/antennapod/parser/media/vorbis/OggInputStream.java
deleted file mode 100644
index ed495bcf3..000000000
--- a/parser/media/src/main/java/de/danoeh/antennapod/parser/media/vorbis/OggInputStream.java
+++ /dev/null
@@ -1,81 +0,0 @@
-package de.danoeh.antennapod.parser.media.vorbis;
-
-import org.apache.commons.io.IOUtils;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.Arrays;
-
-class OggInputStream extends InputStream {
- private final InputStream input;
-
- /** True if OggInputStream is currently inside an Ogg page. */
- private boolean isInPage;
- private long bytesLeft;
-
- public OggInputStream(InputStream input) {
- super();
- isInPage = false;
- this.input = input;
- }
-
- @Override
- public int read() throws IOException {
- if (!isInPage) {
- readOggPage();
- }
-
- if (isInPage && bytesLeft > 0) {
- int result = input.read();
- bytesLeft -= 1;
- if (bytesLeft == 0) {
- isInPage = false;
- }
- return result;
- }
- return -1;
- }
-
- private void readOggPage() throws IOException {
- // find OggS
- int[] buffer = new int[4];
- int c;
- boolean isInOggS = false;
- while ((c = input.read()) != -1) {
- switch (c) {
- case 'O':
- isInOggS = true;
- buffer[0] = c;
- break;
- case 'g':
- if (buffer[1] != c) {
- buffer[1] = c;
- } else {
- buffer[2] = c;
- }
- break;
- case 'S':
- buffer[3] = c;
- break;
- default:
- if (isInOggS) {
- Arrays.fill(buffer, 0);
- isInOggS = false;
- }
- }
- if (buffer[0] == 'O' && buffer[1] == 'g' && buffer[2] == 'g'
- && buffer[3] == 'S') {
- break;
- }
- }
- // read segments
- IOUtils.skipFully(input, 22);
- bytesLeft = 0;
- int numSegments = input.read();
- for (int i = 0; i < numSegments; i++) {
- bytesLeft += input.read();
- }
- isInPage = true;
- }
-
-}
diff --git a/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 f833f683b..82455d180 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
@@ -66,11 +66,6 @@ public class VorbisCommentChapterReader extends VorbisCommentReader {
}
@Override
- public void onNoVorbisCommentFound() {
- System.out.println("No vorbis comment found");
- }
-
- @Override
public void onEndOfComment() {
System.out.println("End of comment");
for (Chapter c : chapters) {
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 319d3759c..b4f87bd70 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
@@ -35,43 +35,54 @@ public abstract class VorbisCommentReader {
*/
protected abstract void onContentVectorValue(String key, String value) throws VorbisCommentReaderException;
- protected abstract void onNoVorbisCommentFound();
-
protected abstract void onEndOfComment();
protected abstract void onError(VorbisCommentReaderException exception);
public void readInputStream(InputStream input) throws VorbisCommentReaderException {
try {
- // look for identification header
- if (findIdentificationHeader(input)) {
- onVorbisCommentFound();
- input = new OggInputStream(input);
- if (findCommentHeader(input)) {
- VorbisCommentHeader commentHeader = readCommentHeader(input);
- onVorbisCommentHeaderFound(commentHeader);
- for (int i = 0; i < commentHeader.getUserCommentLength(); i++) {
- readUserComment(input);
- }
- onEndOfComment();
- } else {
- onError(new VorbisCommentReaderException("No comment header found"));
- }
- } else {
- onNoVorbisCommentFound();
+ findIdentificationHeader(input);
+ onVorbisCommentFound();
+ findOggPage(input);
+ findCommentHeader(input);
+ VorbisCommentHeader commentHeader = readCommentHeader(input);
+ onVorbisCommentHeaderFound(commentHeader);
+ for (int i = 0; i < commentHeader.getUserCommentLength(); i++) {
+ readUserComment(input);
}
+ onEndOfComment();
} catch (IOException e) {
onError(new VorbisCommentReaderException(e));
}
}
+ private void findOggPage(InputStream input) throws IOException {
+ // find OggS
+ byte[] buffer = new byte[4];
+ final byte[] oggPageHeader = {'O', 'g', 'g', 'S'};
+ for (int bytesRead = 0; bytesRead < SECOND_PAGE_MAX_LENGTH; bytesRead++) {
+ buffer[bytesRead % buffer.length] = (byte) input.read();
+ if (bufferMatches(buffer, oggPageHeader, bytesRead)) {
+ break;
+ }
+ }
+ // read segments
+ IOUtils.skipFully(input, 22);
+ int numSegments = input.read();
+ IOUtils.skipFully(input, numSegments);
+ }
+
private void readUserComment(InputStream input) 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 key = readContentVectorKey(input, vectorLength).toLowerCase(Locale.US);
boolean readValue = onContentVectorKey(key);
if (readValue) {
- String value = readUtf8String(input, (int) (vectorLength - key.length() - 1));
+ String value = readUtf8String(input, vectorLength - key.length() - 1);
onContentVectorValue(key, value);
} else {
IOUtils.skipFully(input, vectorLength - key.length() - 1);
@@ -93,33 +104,32 @@ public abstract class VorbisCommentReader {
* identification header is found, it will be skipped completely and the
* method will return true, otherwise false.
*/
- private boolean findIdentificationHeader(InputStream input) throws IOException {
+ private void findIdentificationHeader(InputStream input) 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' };
for (int i = 6; i < buffer.length; i++) {
if (bufferMatches(buffer, oggIdentificationHeader, i)) {
IOUtils.skip(input, FIRST_OGG_PAGE_LENGTH - FIRST_OPUS_PAGE_LENGTH);
- return true;
} else if (bufferMatches(buffer, "OpusHead".getBytes(), i)) {
- return true;
+ return;
}
}
- return false;
+ throw new IOException("No identification header found");
}
- private boolean findCommentHeader(InputStream input) throws IOException {
+ private void findCommentHeader(InputStream input) 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++) {
buffer[bytesRead % buffer.length] = (byte) input.read();
if (bufferMatches(buffer, oggCommentHeader, bytesRead)) {
- return true;
+ return;
} else if (bufferMatches(buffer, "OpusTags".getBytes(), bytesRead)) {
- return true;
+ return;
}
}
- return false;
+ throw new IOException("No comment header found");
}
/**
diff --git a/playback/README.md b/playback/README.md
new file mode 100644
index 000000000..0709ac2c6
--- /dev/null
+++ b/playback/README.md
@@ -0,0 +1,3 @@
+# :playback
+
+This folder contains modules that deal with media playback.
diff --git a/playback/base/README.md b/playback/base/README.md
new file mode 100644
index 000000000..281a799f1
--- /dev/null
+++ b/playback/base/README.md
@@ -0,0 +1,3 @@
+# :playback:base
+
+This module provides the basic interfaces for a PlaybackServiceMediaPlayer.
diff --git a/playback/base/build.gradle b/playback/base/build.gradle
new file mode 100644
index 000000000..a1d344492
--- /dev/null
+++ b/playback/base/build.gradle
@@ -0,0 +1,10 @@
+apply plugin: "com.android.library"
+apply from: "../../common.gradle"
+
+dependencies {
+ implementation project(':model')
+
+ annotationProcessor "androidx.annotation:annotation:$annotationVersion"
+
+ testImplementation "junit:junit:$junitVersion"
+}
diff --git a/playback/base/src/main/AndroidManifest.xml b/playback/base/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..c6a44a212
--- /dev/null
+++ b/playback/base/src/main/AndroidManifest.xml
@@ -0,0 +1 @@
+<manifest package="de.danoeh.antennapod.playback.base" />
diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceMediaPlayer.java b/playback/base/src/main/java/de/danoeh/antennapod/playback/base/PlaybackServiceMediaPlayer.java
index e093383b9..d03695896 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceMediaPlayer.java
+++ b/playback/base/src/main/java/de/danoeh/antennapod/playback/base/PlaybackServiceMediaPlayer.java
@@ -1,10 +1,9 @@
-package de.danoeh.antennapod.core.service.playback;
+package de.danoeh.antennapod.playback.base;
import android.content.Context;
import android.media.AudioManager;
import android.net.wifi.WifiManager;
import androidx.annotation.NonNull;
-import androidx.annotation.StringRes;
import android.util.Log;
import android.util.Pair;
import android.view.SurfaceHolder;
@@ -12,6 +11,7 @@ import android.view.SurfaceHolder;
import java.util.List;
import java.util.concurrent.Future;
+import androidx.annotation.Nullable;
import de.danoeh.antennapod.model.playback.MediaType;
import de.danoeh.antennapod.model.playback.Playable;
@@ -31,20 +31,20 @@ public abstract class PlaybackServiceMediaPlayer {
/**
* Return value of some PSMP methods if the method call failed.
*/
- static final int INVALID_TIME = -1;
+ public static final int INVALID_TIME = -1;
private volatile PlayerStatus oldPlayerStatus;
- volatile PlayerStatus playerStatus;
+ protected volatile PlayerStatus playerStatus;
/**
* A wifi-lock that is acquired if the media file is being streamed.
*/
private WifiManager.WifiLock wifiLock;
- final PSMPCallback callback;
- final Context context;
+ protected final PSMPCallback callback;
+ protected final Context context;
- PlaybackServiceMediaPlayer(@NonNull Context context,
+ protected PlaybackServiceMediaPlayer(@NonNull Context context,
@NonNull PSMPCallback callback){
this.context = context;
this.callback = callback;
@@ -281,7 +281,9 @@ public abstract class PlaybackServiceMediaPlayer {
*/
protected abstract boolean shouldLockWifi();
- final synchronized void acquireWifiLockIfNecessary() {
+ public abstract boolean isCasting();
+
+ protected final synchronized void acquireWifiLockIfNecessary() {
if (shouldLockWifi()) {
if (wifiLock == null) {
wifiLock = ((WifiManager) context.getApplicationContext().getSystemService(Context.WIFI_SERVICE))
@@ -292,7 +294,7 @@ public abstract class PlaybackServiceMediaPlayer {
}
}
- final synchronized void releaseWifiLockIfNecessary() {
+ protected final synchronized void releaseWifiLockIfNecessary() {
if (wifiLock != null && wifiLock.isHeld()) {
wifiLock.release();
}
@@ -313,7 +315,8 @@ public abstract class PlaybackServiceMediaPlayer {
* @param position The position to be set to the current Playable object in case playback started or paused.
* Will be ignored if given the value of {@link #INVALID_TIME}.
*/
- final synchronized void setPlayerStatus(@NonNull PlayerStatus newStatus, Playable newMedia, int position) {
+ protected final synchronized void setPlayerStatus(@NonNull PlayerStatus newStatus,
+ Playable newMedia, int position) {
Log.d(TAG, this.getClass().getSimpleName() + ": Setting player status to " + newStatus);
this.oldPlayerStatus = playerStatus;
@@ -339,7 +342,7 @@ public abstract class PlaybackServiceMediaPlayer {
/**
* @see #setPlayerStatus(PlayerStatus, Playable, int)
*/
- final void setPlayerStatus(@NonNull PlayerStatus newStatus, Playable newMedia) {
+ protected final void setPlayerStatus(@NonNull PlayerStatus newStatus, Playable newMedia) {
setPlayerStatus(newStatus, newMedia, INVALID_TIME);
}
@@ -348,16 +351,8 @@ public abstract class PlaybackServiceMediaPlayer {
void shouldStop();
- void playbackSpeedChanged(float s);
-
- void onBufferingUpdate(int percent);
-
void onMediaChanged(boolean reloadUI);
- boolean onMediaPlayerInfo(int code, @StringRes int resourceId);
-
- boolean onMediaPlayerError(Object inObj, int what, int extra);
-
void onPostPlayback(@NonNull Playable media, boolean ended, boolean skipped, boolean playingNext);
void onPlaybackStart(@NonNull Playable playable, int position);
@@ -366,7 +361,12 @@ public abstract class PlaybackServiceMediaPlayer {
Playable getNextInQueue(Playable currentMedia);
+ @Nullable
+ Playable findMedia(@NonNull String url);
+
void onPlaybackEnded(MediaType mediaType, boolean stopPlaying);
+
+ void ensureMediaInfoLoaded(@NonNull Playable media);
}
/**
@@ -377,7 +377,7 @@ public abstract class PlaybackServiceMediaPlayer {
public PlayerStatus playerStatus;
public Playable playable;
- PSMPInfo(PlayerStatus oldPlayerStatus, PlayerStatus playerStatus, Playable playable) {
+ public PSMPInfo(PlayerStatus oldPlayerStatus, PlayerStatus playerStatus, Playable playable) {
this.oldPlayerStatus = oldPlayerStatus;
this.playerStatus = playerStatus;
this.playable = playable;
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
new file mode 100644
index 000000000..d995ae21f
--- /dev/null
+++ b/playback/base/src/main/java/de/danoeh/antennapod/playback/base/PlayerStatus.java
@@ -0,0 +1,33 @@
+package de.danoeh.antennapod.playback.base;
+
+public enum PlayerStatus {
+ INDETERMINATE(0), // player is currently changing its state, listeners should wait until the state is left
+ ERROR(-1),
+ PREPARING(19),
+ PAUSED(30),
+ PLAYING(40),
+ STOPPED(5),
+ PREPARED(20),
+ SEEKING(29),
+ INITIALIZING(9), // playback service is loading the Playable's metadata
+ INITIALIZED(10); // playback service was started, data source of media player was set
+
+ private final int statusValue;
+ private static final PlayerStatus[] fromOrdinalLookup;
+
+ static {
+ fromOrdinalLookup = PlayerStatus.values();
+ }
+
+ PlayerStatus(int val) {
+ 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/core/src/main/java/de/danoeh/antennapod/core/util/RewindAfterPauseUtils.java b/playback/base/src/main/java/de/danoeh/antennapod/playback/base/RewindAfterPauseUtils.java
index 813c6d0f7..7d694f38b 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/util/RewindAfterPauseUtils.java
+++ b/playback/base/src/main/java/de/danoeh/antennapod/playback/base/RewindAfterPauseUtils.java
@@ -1,4 +1,4 @@
-package de.danoeh.antennapod.core.util;
+package de.danoeh.antennapod.playback.base;
import java.util.concurrent.TimeUnit;
diff --git a/core/src/test/java/de/danoeh/antennapod/core/util/RewindAfterPauseUtilTest.java b/playback/base/src/test/java/de/danoeh/antennapod/playback/base/RewindAfterPauseUtilTest.java
index dc64f6ae0..b122971b2 100644
--- a/core/src/test/java/de/danoeh/antennapod/core/util/RewindAfterPauseUtilTest.java
+++ b/playback/base/src/test/java/de/danoeh/antennapod/playback/base/RewindAfterPauseUtilTest.java
@@ -1,4 +1,4 @@
-package de.danoeh.antennapod.core.util;
+package de.danoeh.antennapod.playback.base;
import org.junit.Test;
diff --git a/playback/cast/README.md b/playback/cast/README.md
new file mode 100644
index 000000000..29eb8eacd
--- /dev/null
+++ b/playback/cast/README.md
@@ -0,0 +1,3 @@
+# :playback:cast
+
+This module provides Chromecast support for the Google Play version of the app.
diff --git a/playback/cast/build.gradle b/playback/cast/build.gradle
new file mode 100644
index 000000000..c51354838
--- /dev/null
+++ b/playback/cast/build.gradle
@@ -0,0 +1,17 @@
+apply plugin: "com.android.library"
+apply from: "../../common.gradle"
+apply from: "../../playFlavor.gradle"
+
+dependencies {
+ implementation project(':event')
+ implementation project(':model')
+ implementation project(':playback:base')
+
+ annotationProcessor "androidx.annotation:annotation:$annotationVersion"
+ implementation "androidx.appcompat:appcompat:$appcompatVersion"
+ implementation "org.greenrobot:eventbus:$eventbusVersion"
+ annotationProcessor "org.greenrobot:eventbus-annotation-processor:$eventbusVersion"
+
+ playApi 'androidx.mediarouter:mediarouter:1.2.5'
+ playApi 'com.google.android.gms:play-services-cast-framework:20.0.0'
+}
diff --git a/app/src/free/java/de/danoeh/antennapod/activity/CastEnabledActivity.java b/playback/cast/src/free/java/de/danoeh/antennapod/playback/cast/CastEnabledActivity.java
index 98d506f65..36524b236 100644
--- a/app/src/free/java/de/danoeh/antennapod/activity/CastEnabledActivity.java
+++ b/playback/cast/src/free/java/de/danoeh/antennapod/playback/cast/CastEnabledActivity.java
@@ -1,4 +1,4 @@
-package de.danoeh.antennapod.activity;
+package de.danoeh.antennapod.playback.cast;
import androidx.appcompat.app.AppCompatActivity;
diff --git a/playback/cast/src/free/java/de/danoeh/antennapod/playback/cast/CastPsmp.java b/playback/cast/src/free/java/de/danoeh/antennapod/playback/cast/CastPsmp.java
new file mode 100644
index 000000000..7f5e0f2ab
--- /dev/null
+++ b/playback/cast/src/free/java/de/danoeh/antennapod/playback/cast/CastPsmp.java
@@ -0,0 +1,17 @@
+package de.danoeh.antennapod.playback.cast;
+
+import android.content.Context;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import de.danoeh.antennapod.playback.base.PlaybackServiceMediaPlayer;
+
+/**
+ * Stub implementation of CastPsmp for Free build flavour
+ */
+public class CastPsmp {
+ @Nullable
+ public static PlaybackServiceMediaPlayer getInstanceIfConnected(@NonNull Context context,
+ @NonNull PlaybackServiceMediaPlayer.PSMPCallback callback) {
+ return null;
+ }
+}
diff --git a/playback/cast/src/free/java/de/danoeh/antennapod/playback/cast/CastStateListener.java b/playback/cast/src/free/java/de/danoeh/antennapod/playback/cast/CastStateListener.java
new file mode 100644
index 000000000..60cc7dd2c
--- /dev/null
+++ b/playback/cast/src/free/java/de/danoeh/antennapod/playback/cast/CastStateListener.java
@@ -0,0 +1,15 @@
+package de.danoeh.antennapod.playback.cast;
+
+import android.content.Context;
+
+public class CastStateListener {
+
+ public CastStateListener(Context context) {
+ }
+
+ public void destroy() {
+ }
+
+ public void onSessionStartedOrEnded() {
+ }
+}
diff --git a/playback/cast/src/main/AndroidManifest.xml b/playback/cast/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..58c2b9396
--- /dev/null
+++ b/playback/cast/src/main/AndroidManifest.xml
@@ -0,0 +1 @@
+<manifest package="de.danoeh.antennapod.playback.cast" />
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
new file mode 100644
index 000000000..2cebde6a3
--- /dev/null
+++ b/playback/cast/src/play/java/de/danoeh/antennapod/playback/cast/CastEnabledActivity.java
@@ -0,0 +1,35 @@
+package de.danoeh.antennapod.playback.cast;
+
+import android.os.Bundle;
+import android.view.Menu;
+import androidx.appcompat.app.AppCompatActivity;
+import com.google.android.gms.cast.framework.CastButtonFactory;
+import com.google.android.gms.cast.framework.CastContext;
+import com.google.android.gms.common.ConnectionResult;
+import com.google.android.gms.common.GoogleApiAvailability;
+
+/**
+ * Activity that allows for showing the MediaRouter button whenever there's a cast device in the
+ * network.
+ */
+public abstract class CastEnabledActivity extends AppCompatActivity {
+ private static final String TAG = "CastEnabledActivity";
+ private boolean canCast = false;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ canCast = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(this) == ConnectionResult.SUCCESS;
+ if (canCast) {
+ CastContext.getSharedInstance(this);
+ }
+ }
+
+ public void requestCastButton(Menu menu) {
+ if (!canCast) {
+ return;
+ }
+ getMenuInflater().inflate(R.menu.cast_button, menu);
+ CastButtonFactory.setUpMediaRouteButton(getApplicationContext(), menu, R.id.media_route_menu_item);
+ }
+}
diff --git a/playback/cast/src/play/java/de/danoeh/antennapod/playback/cast/CastOptionsProvider.java b/playback/cast/src/play/java/de/danoeh/antennapod/playback/cast/CastOptionsProvider.java
new file mode 100644
index 000000000..37885bdd0
--- /dev/null
+++ b/playback/cast/src/play/java/de/danoeh/antennapod/playback/cast/CastOptionsProvider.java
@@ -0,0 +1,26 @@
+package de.danoeh.antennapod.playback.cast;
+
+import android.content.Context;
+import androidx.annotation.NonNull;
+import com.google.android.gms.cast.CastMediaControlIntent;
+import com.google.android.gms.cast.framework.CastOptions;
+import com.google.android.gms.cast.framework.OptionsProvider;
+import com.google.android.gms.cast.framework.SessionProvider;
+
+import java.util.List;
+
+@SuppressWarnings("unused")
+public class CastOptionsProvider implements OptionsProvider {
+ @Override
+ @NonNull
+ public CastOptions getCastOptions(@NonNull Context context) {
+ return new CastOptions.Builder()
+ .setReceiverApplicationId(CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID)
+ .build();
+ }
+
+ @Override
+ public List<SessionProvider> getAdditionalSessionProviders(@NonNull Context context) {
+ return null;
+ }
+} \ No newline at end of file
diff --git a/core/src/play/java/de/danoeh/antennapod/core/service/playback/RemotePSMP.java b/playback/cast/src/play/java/de/danoeh/antennapod/playback/cast/CastPsmp.java
index 38b469e8e..8e74154e8 100644
--- a/core/src/play/java/de/danoeh/antennapod/core/service/playback/RemotePSMP.java
+++ b/playback/cast/src/play/java/de/danoeh/antennapod/playback/cast/CastPsmp.java
@@ -1,66 +1,77 @@
-package de.danoeh.antennapod.core.service.playback;
+package de.danoeh.antennapod.playback.cast;
import android.content.Context;
-import android.media.MediaPlayer;
import androidx.annotation.NonNull;
import android.util.Log;
import android.util.Pair;
import android.view.SurfaceHolder;
-import com.google.android.gms.cast.Cast;
-import com.google.android.gms.cast.CastStatusCodes;
-import com.google.android.gms.cast.MediaInfo;
-import com.google.android.gms.cast.MediaStatus;
-import com.google.android.libraries.cast.companionlibrary.cast.exceptions.CastException;
-import com.google.android.libraries.cast.companionlibrary.cast.exceptions.NoConnectionException;
-import com.google.android.libraries.cast.companionlibrary.cast.exceptions.TransientNetworkDisconnectionException;
-
-import de.danoeh.antennapod.core.cast.MediaInfoCreator;
-
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
import java.util.concurrent.atomic.AtomicBoolean;
-import de.danoeh.antennapod.core.R;
-import de.danoeh.antennapod.core.cast.CastConsumer;
-import de.danoeh.antennapod.core.cast.CastManager;
-import de.danoeh.antennapod.core.cast.CastUtils;
-import de.danoeh.antennapod.core.cast.DefaultCastConsumer;
-import de.danoeh.antennapod.core.storage.DBReader;
-import de.danoeh.antennapod.model.playback.RemoteMedia;
+import androidx.annotation.Nullable;
+import com.google.android.gms.cast.MediaError;
+import com.google.android.gms.cast.MediaInfo;
+import com.google.android.gms.cast.MediaLoadOptions;
+import com.google.android.gms.cast.MediaLoadRequestData;
+import com.google.android.gms.cast.MediaSeekOptions;
+import com.google.android.gms.cast.MediaStatus;
+import com.google.android.gms.cast.framework.CastContext;
+import com.google.android.gms.cast.framework.CastState;
+import com.google.android.gms.cast.framework.media.RemoteMediaClient;
+import com.google.android.gms.common.ConnectionResult;
+import com.google.android.gms.common.GoogleApiAvailability;
+import de.danoeh.antennapod.event.PlayerErrorEvent;
+import de.danoeh.antennapod.event.playback.BufferUpdateEvent;
import de.danoeh.antennapod.model.feed.FeedMedia;
import de.danoeh.antennapod.model.playback.MediaType;
-import de.danoeh.antennapod.core.util.RewindAfterPauseUtils;
import de.danoeh.antennapod.model.playback.Playable;
+import de.danoeh.antennapod.model.playback.RemoteMedia;
+import de.danoeh.antennapod.playback.base.PlaybackServiceMediaPlayer;
+import de.danoeh.antennapod.playback.base.PlayerStatus;
+import de.danoeh.antennapod.playback.base.RewindAfterPauseUtils;
+import org.greenrobot.eventbus.EventBus;
/**
* Implementation of PlaybackServiceMediaPlayer suitable for remote playback on Cast Devices.
*/
-public class RemotePSMP extends PlaybackServiceMediaPlayer {
-
- public static final String TAG = "RemotePSMP";
+public class CastPsmp extends PlaybackServiceMediaPlayer {
- public static final int CAST_ERROR = 3001;
-
- public static final int CAST_ERROR_PRIORITY_HIGH = 3005;
-
- private final CastManager castMgr;
+ public static final String TAG = "CastPSMP";
private volatile Playable media;
private volatile MediaType mediaType;
private volatile MediaInfo remoteMedia;
private volatile int remoteState;
+ private final CastContext castContext;
+ private final RemoteMediaClient remoteMediaClient;
private final AtomicBoolean isBuffering;
private final AtomicBoolean startWhenPrepared;
- public RemotePSMP(@NonNull Context context, @NonNull PSMPCallback callback) {
+ @Nullable
+ public static PlaybackServiceMediaPlayer getInstanceIfConnected(@NonNull Context context,
+ @NonNull PSMPCallback callback) {
+ if (GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context) != ConnectionResult.SUCCESS) {
+ return null;
+ }
+ if (CastContext.getSharedInstance(context).getCastState() == CastState.CONNECTED) {
+ return new CastPsmp(context, callback);
+ } else {
+ return null;
+ }
+ }
+
+ public CastPsmp(@NonNull Context context, @NonNull PSMPCallback callback) {
super(context, callback);
- castMgr = CastManager.getInstance();
+ castContext = CastContext.getSharedInstance(context);
+ remoteMediaClient = castContext.getSessionManager().getCurrentCastSession().getRemoteMediaClient();
+ remoteMediaClient.registerCallback(remoteMediaClientCallback);
media = null;
mediaType = null;
startWhenPrepared = new AtomicBoolean(false);
@@ -68,94 +79,48 @@ public class RemotePSMP extends PlaybackServiceMediaPlayer {
remoteState = MediaStatus.PLAYER_STATE_UNKNOWN;
}
- public void init() {
- try {
- if (castMgr.isConnected() && castMgr.isRemoteMediaLoaded()) {
- onRemoteMediaPlayerStatusUpdated();
- }
- } catch (TransientNetworkDisconnectionException | NoConnectionException e) {
- Log.e(TAG, "Unable to do initial check for loaded media", e);
- }
-
- castMgr.addCastConsumer(castConsumer);
- }
-
- private CastConsumer castConsumer = new DefaultCastConsumer() {
- @Override
- public void onRemoteMediaPlayerMetadataUpdated() {
- RemotePSMP.this.onRemoteMediaPlayerStatusUpdated();
- }
-
+ private final RemoteMediaClient.Callback remoteMediaClientCallback = new RemoteMediaClient.Callback() {
@Override
- public void onRemoteMediaPlayerStatusUpdated() {
- RemotePSMP.this.onRemoteMediaPlayerStatusUpdated();
+ public void onMetadataUpdated() {
+ super.onMetadataUpdated();
+ onRemoteMediaPlayerStatusUpdated();
}
@Override
- public void onMediaLoadResult(int statusCode) {
- if (playerStatus == PlayerStatus.PREPARING) {
- if (statusCode == CastStatusCodes.SUCCESS) {
- setPlayerStatus(PlayerStatus.PREPARED, media);
- if (media.getDuration() == 0) {
- Log.d(TAG, "Setting duration of media");
- try {
- media.setDuration((int) castMgr.getMediaDuration());
- } catch (TransientNetworkDisconnectionException | NoConnectionException e) {
- Log.e(TAG, "Unable to get remote media's duration");
- }
- }
- } else if (statusCode != CastStatusCodes.REPLACED){
- Log.d(TAG, "Remote media failed to load");
- setPlayerStatus(PlayerStatus.INITIALIZED, media);
- }
- } else {
- Log.d(TAG, "onMediaLoadResult called, but Player Status wasn't in preparing state, so we ignore the result");
- }
+ public void onPreloadStatusUpdated() {
+ super.onPreloadStatusUpdated();
+ onRemoteMediaPlayerStatusUpdated();
}
@Override
- public void onApplicationStatusChanged(String appStatus) {
- if (playerStatus != PlayerStatus.PLAYING) {
- Log.d(TAG, "onApplicationStatusChanged, but no media was playing");
- return;
- }
- boolean playbackEnded = false;
- try {
- int standbyState = castMgr.getApplicationStandbyState();
- Log.d(TAG, "standbyState: " + standbyState);
- playbackEnded = standbyState == Cast.STANDBY_STATE_YES;
- } catch (IllegalStateException e) {
- Log.d(TAG, "unable to get standbyState on onApplicationStatusChanged()");
- }
- if (playbackEnded) {
- // This is an unconventional thing to occur...
- Log.w(TAG, "Somehow, Chromecast went from playing directly to standby mode");
- endPlayback(false, false, true, true);
- }
+ public void onStatusUpdated() {
+ super.onStatusUpdated();
+ onRemoteMediaPlayerStatusUpdated();
}
@Override
- public void onFailed(int resourceId, int statusCode) {
- callback.onMediaPlayerInfo(CAST_ERROR, resourceId);
+ public void onMediaError(@NonNull MediaError mediaError) {
+ EventBus.getDefault().post(new PlayerErrorEvent(mediaError.getReason()));
}
};
private void setBuffering(boolean buffering) {
if (buffering && isBuffering.compareAndSet(false, true)) {
- callback.onMediaPlayerInfo(MediaPlayer.MEDIA_INFO_BUFFERING_START, 0);
+ EventBus.getDefault().post(BufferUpdateEvent.started());
} else if (!buffering && isBuffering.compareAndSet(true, false)) {
- callback.onMediaPlayerInfo(MediaPlayer.MEDIA_INFO_BUFFERING_END, 0);
+ EventBus.getDefault().post(BufferUpdateEvent.ended());
}
}
- private Playable localVersion(MediaInfo info){
- if (info == null) {
+ private Playable localVersion(MediaInfo info) {
+ if (info == null || info.getMetadata() == null) {
return null;
}
if (CastUtils.matches(info, media)) {
return media;
}
- return CastUtils.getPlayable(info, true);
+ String streamUrl = info.getMetadata().getString(CastUtils.KEY_STREAM_URL);
+ return streamUrl == null ? CastUtils.makeRemoteMedia(info) : callback.findMedia(streamUrl);
}
private MediaInfo remoteVersion(Playable playable) {
@@ -166,7 +131,7 @@ public class RemotePSMP extends PlaybackServiceMediaPlayer {
return remoteMedia;
}
if (playable instanceof FeedMedia) {
- return CastUtils.convertFromFeedMedia((FeedMedia) playable);
+ return MediaInfoCreator.from((FeedMedia) playable);
}
if (playable instanceof RemoteMedia) {
return MediaInfoCreator.from((RemoteMedia) playable);
@@ -175,7 +140,7 @@ public class RemotePSMP extends PlaybackServiceMediaPlayer {
}
private void onRemoteMediaPlayerStatusUpdated() {
- MediaStatus status = castMgr.getMediaStatus();
+ MediaStatus status = remoteMediaClient.getMediaStatus();
if (status == null) {
Log.d(TAG, "Received null MediaStatus");
return;
@@ -206,8 +171,8 @@ public class RemotePSMP extends PlaybackServiceMediaPlayer {
remoteState = state;
}
- if (mediaChanged && stateChanged && oldState == MediaStatus.PLAYER_STATE_PLAYING &&
- state != MediaStatus.PLAYER_STATE_IDLE) {
+ if (mediaChanged && stateChanged && oldState == MediaStatus.PLAYER_STATE_PLAYING
+ && state != MediaStatus.PLAYER_STATE_IDLE) {
callback.onPlaybackPause(null, INVALID_TIME);
// We don't want setPlayerStatus to handle the onPlaybackPause callback
setPlayerStatus(PlayerStatus.INDETERMINATE, currentMedia);
@@ -230,9 +195,8 @@ public class RemotePSMP extends PlaybackServiceMediaPlayer {
setPlayerStatus(PlayerStatus.PAUSED, currentMedia, position);
break;
case MediaStatus.PLAYER_STATE_BUFFERING:
- setPlayerStatus((mediaChanged || playerStatus == PlayerStatus.PREPARING) ?
- PlayerStatus.PREPARING : PlayerStatus.SEEKING,
- currentMedia,
+ setPlayerStatus((mediaChanged || playerStatus == PlayerStatus.PREPARING)
+ ? PlayerStatus.PREPARING : PlayerStatus.SEEKING, currentMedia,
currentMedia != null ? currentMedia.getPosition() : INVALID_TIME);
break;
case MediaStatus.PLAYER_STATE_IDLE:
@@ -271,11 +235,13 @@ public class RemotePSMP extends PlaybackServiceMediaPlayer {
endPlayback(true, false, true, true);
return;
case MediaStatus.IDLE_REASON_ERROR:
- Log.w(TAG, "Got an error status from the Chromecast. Skipping, if possible, to the next episode...");
- callback.onMediaPlayerInfo(CAST_ERROR_PRIORITY_HIGH,
- R.string.cast_failed_media_error_skipping);
+ Log.w(TAG, "Got an error status from the Chromecast. "
+ + "Skipping, if possible, to the next episode...");
+ EventBus.getDefault().post(new PlayerErrorEvent("Chromecast error code 1"));
endPlayback(false, false, true, true);
return;
+ default:
+ return;
}
break;
case MediaStatus.PLAYER_STATE_UNKNOWN:
@@ -284,7 +250,7 @@ public class RemotePSMP extends PlaybackServiceMediaPlayer {
}
break;
default:
- Log.wtf(TAG, "Remote media state undetermined!");
+ Log.w(TAG, "Remote media state undetermined!");
}
if (mediaChanged) {
callback.onMediaChanged(true);
@@ -295,25 +261,29 @@ public class RemotePSMP extends PlaybackServiceMediaPlayer {
}
@Override
- public void playMediaObject(@NonNull final Playable playable, final boolean stream, final boolean startWhenPrepared, final boolean prepareImmediately) {
+ public void playMediaObject(@NonNull final Playable playable, final boolean stream,
+ final boolean startWhenPrepared, final boolean prepareImmediately) {
Log.d(TAG, "playMediaObject() called");
playMediaObject(playable, false, stream, startWhenPrepared, prepareImmediately);
}
/**
- * Internal implementation of playMediaObject. This method has an additional parameter that allows the caller to force a media player reset even if
+ * Internal implementation of playMediaObject. This method has an additional parameter that
+ * allows the caller to force a media player reset even if
* the given playable parameter is the same object as the currently playing media.
*
* @see #playMediaObject(Playable, boolean, boolean, boolean)
*/
- private void playMediaObject(@NonNull final Playable playable, final boolean forceReset, final boolean stream, final boolean startWhenPrepared, final boolean prepareImmediately) {
- if (!CastUtils.isCastable(playable)) {
+ private void playMediaObject(@NonNull final Playable playable, final boolean forceReset,
+ final boolean stream, final boolean startWhenPrepared, final boolean prepareImmediately) {
+ if (!CastUtils.isCastable(playable, castContext.getSessionManager().getCurrentCastSession())) {
Log.d(TAG, "media provided is not compatible with cast device");
- callback.onMediaPlayerInfo(CAST_ERROR_PRIORITY_HIGH, R.string.cast_not_castable);
+ EventBus.getDefault().post(new PlayerErrorEvent("Media not compatible with cast device"));
Playable nextPlayable = playable;
do {
nextPlayable = callback.getNextInQueue(nextPlayable);
- } while (nextPlayable != null && !CastUtils.isCastable(nextPlayable));
+ } while (nextPlayable != null && !CastUtils.isCastable(nextPlayable,
+ castContext.getSessionManager().getCurrentCastSession()));
if (nextPlayable != null) {
playMediaObject(nextPlayable, forceReset, stream, startWhenPrepared, prepareImmediately);
}
@@ -328,14 +298,8 @@ public class RemotePSMP extends PlaybackServiceMediaPlayer {
return;
} else {
// set temporarily to pause in order to update list with current position
- boolean isPlaying = playerStatus == PlayerStatus.PLAYING;
- int position = media.getPosition();
- try {
- isPlaying = castMgr.isRemoteMediaPlaying();
- position = (int) castMgr.getCurrentMediaPosition();
- } catch (TransientNetworkDisconnectionException | NoConnectionException e) {
- Log.e(TAG, "Unable to determine whether media was playing, falling back to stored player status", e);
- }
+ boolean isPlaying = remoteMediaClient.isPlaying();
+ int position = (int) remoteMediaClient.getApproximateStreamPosition();
if (isPlaying) {
callback.onPlaybackPause(media, position);
}
@@ -343,7 +307,6 @@ public class RemotePSMP extends PlaybackServiceMediaPlayer {
final Playable oldMedia = media;
callback.onPostPlayback(oldMedia, false, false, true);
}
-
setPlayerStatus(PlayerStatus.INDETERMINATE, null);
}
}
@@ -353,9 +316,7 @@ public class RemotePSMP extends PlaybackServiceMediaPlayer {
this.mediaType = media.getMediaType();
this.startWhenPrepared.set(startWhenPrepared);
setPlayerStatus(PlayerStatus.INITIALIZING, media);
- if (media instanceof FeedMedia && ((FeedMedia) media).getItem() == null) {
- ((FeedMedia) media).setItem(DBReader.getFeedItem(((FeedMedia) media).getItemId()));
- }
+ callback.ensureMediaInfoLoaded(media);
callback.onMediaChanged(true);
setPlayerStatus(PlayerStatus.INITIALIZED, media);
if (prepareImmediately) {
@@ -365,29 +326,16 @@ public class RemotePSMP extends PlaybackServiceMediaPlayer {
@Override
public void resume() {
- try {
- if (playerStatus == PlayerStatus.PREPARED && media.getPosition() > 0) {
- int newPosition = RewindAfterPauseUtils.calculatePositionWithRewind(
+ int newPosition = RewindAfterPauseUtils.calculatePositionWithRewind(
media.getPosition(),
media.getLastPlayedTime());
- castMgr.play(newPosition);
- } else {
- castMgr.play();
- }
- } catch (CastException | TransientNetworkDisconnectionException | NoConnectionException e) {
- Log.e(TAG, "Unable to resume remote playback", e);
- }
+ seekTo(newPosition);
+ remoteMediaClient.play();
}
@Override
public void pause(boolean abandonFocus, boolean reinit) {
- try {
- if (castMgr.isRemoteMediaPlaying()) {
- castMgr.pause();
- }
- } catch (CastException | TransientNetworkDisconnectionException | NoConnectionException e) {
- Log.e(TAG, "Unable to pause", e);
- }
+ remoteMediaClient.pause();
}
@Override
@@ -395,18 +343,16 @@ public class RemotePSMP extends PlaybackServiceMediaPlayer {
if (playerStatus == PlayerStatus.INITIALIZED) {
Log.d(TAG, "Preparing media player");
setPlayerStatus(PlayerStatus.PREPARING, media);
- try {
- int position = media.getPosition();
- if (position > 0) {
- position = RewindAfterPauseUtils.calculatePositionWithRewind(
- position,
- media.getLastPlayedTime());
- }
- castMgr.loadMedia(remoteMedia, startWhenPrepared.get(), position);
- } catch (TransientNetworkDisconnectionException | NoConnectionException e) {
- Log.e(TAG, "Error loading media", e);
- setPlayerStatus(PlayerStatus.INITIALIZED, media);
+ int position = media.getPosition();
+ if (position > 0) {
+ position = RewindAfterPauseUtils.calculatePositionWithRewind(
+ position,
+ media.getLastPlayedTime());
}
+ remoteMediaClient.load(new MediaLoadRequestData.Builder()
+ .setMediaInfo(remoteMedia)
+ .setAutoplay(startWhenPrepared.get())
+ .setCurrentTime(position).build());
}
}
@@ -422,19 +368,9 @@ public class RemotePSMP extends PlaybackServiceMediaPlayer {
@Override
public void seekTo(int t) {
- //TODO check other seek implementations and see if there's no issue with sending too many seek commands to the remote media player
- try {
- if (castMgr.isRemoteMediaLoaded()) {
- setPlayerStatus(PlayerStatus.SEEKING, media);
- castMgr.seek(t);
- } else if (media != null && playerStatus == PlayerStatus.INITIALIZED){
- media.setPosition(t);
- startWhenPrepared.set(false);
- prepare();
- }
- } catch (TransientNetworkDisconnectionException | NoConnectionException e) {
- Log.e(TAG, "Unable to seek", e);
- }
+ new Exception("Seeking to " + t).printStackTrace();
+ remoteMediaClient.seek(new MediaSeekOptions.Builder()
+ .setPosition(t).build());
}
@Override
@@ -449,49 +385,19 @@ public class RemotePSMP extends PlaybackServiceMediaPlayer {
@Override
public int getDuration() {
- int retVal = INVALID_TIME;
- boolean prepared;
- try {
- prepared = castMgr.isRemoteMediaLoaded();
- } catch (TransientNetworkDisconnectionException | NoConnectionException e) {
- Log.e(TAG, "Unable to check if remote media is loaded", e);
- prepared = playerStatus.isAtLeast(PlayerStatus.PREPARED);
- }
- if (prepared) {
- try {
- retVal = (int) castMgr.getMediaDuration();
- } catch (TransientNetworkDisconnectionException | NoConnectionException e) {
- Log.e(TAG, "Unable to determine remote media's duration", e);
- }
- }
- if(retVal == INVALID_TIME && media != null && media.getDuration() > 0) {
+ int retVal = (int) remoteMediaClient.getStreamDuration();
+ if (retVal == INVALID_TIME && media != null && media.getDuration() > 0) {
retVal = media.getDuration();
}
- Log.d(TAG, "getDuration() -> " + retVal);
return retVal;
}
@Override
public int getPosition() {
- int retVal = INVALID_TIME;
- boolean prepared;
- try {
- prepared = castMgr.isRemoteMediaLoaded();
- } catch (TransientNetworkDisconnectionException | NoConnectionException e) {
- Log.e(TAG, "Unable to check if remote media is loaded", e);
- prepared = playerStatus.isAtLeast(PlayerStatus.PREPARED);
- }
- if (prepared) {
- try {
- retVal = (int) castMgr.getCurrentMediaPosition();
- } catch (TransientNetworkDisconnectionException | NoConnectionException e) {
- Log.e(TAG, "Unable to determine remote media's position", e);
- }
- }
- if(retVal <= 0 && media != null && media.getPosition() >= 0) {
+ int retVal = (int) remoteMediaClient.getApproximateStreamPosition();
+ if (retVal <= 0 && media != null && media.getPosition() >= 0) {
retVal = media.getPosition();
}
- Log.d(TAG, "getPosition() -> " + retVal);
return retVal;
}
@@ -507,29 +413,21 @@ public class RemotePSMP extends PlaybackServiceMediaPlayer {
@Override
public void setPlaybackParams(float speed, boolean skipSilence) {
- //Can be safely ignored as neither set speed not skipSilence is supported
+ double playbackRate = (float) Math.max(MediaLoadOptions.PLAYBACK_RATE_MIN,
+ Math.min(MediaLoadOptions.PLAYBACK_RATE_MAX, speed));
+ remoteMediaClient.setPlaybackRate(playbackRate);
}
@Override
public float getPlaybackSpeed() {
- return 1;
+ MediaStatus status = remoteMediaClient.getMediaStatus();
+ return status != null ? (float) status.getPlaybackRate() : 1.0f;
}
@Override
public void setVolume(float volumeLeft, float volumeRight) {
Log.d(TAG, "Setting the Stream volume on Remote Media Player");
- double volume = (volumeLeft+volumeRight)/2;
- if (volume > 1.0) {
- volume = 1.0;
- }
- if (volume < 0.0) {
- volume = 0.0;
- }
- try {
- castMgr.setStreamVolume(volume);
- } catch (TransientNetworkDisconnectionException | NoConnectionException | CastException e) {
- Log.e(TAG, "Unable to set the volume", e);
- }
+ remoteMediaClient.setStreamVolume(volumeLeft);
}
@Override
@@ -554,7 +452,7 @@ public class RemotePSMP extends PlaybackServiceMediaPlayer {
@Override
public void shutdown() {
- castMgr.removeCastConsumer(castConsumer);
+ remoteMediaClient.unregisterCallback(remoteMediaClientCallback);
}
@Override
@@ -626,7 +524,7 @@ public class RemotePSMP extends PlaybackServiceMediaPlayer {
boolean playNextEpisode = isPlaying && nextMedia != null;
if (playNextEpisode) {
Log.d(TAG, "Playback of next episode will start immediately.");
- } else if (nextMedia == null){
+ } else if (nextMedia == null) {
Log.d(TAG, "No more episodes available to play");
} else {
Log.d(TAG, "Loading next episode, but not playing automatically.");
@@ -636,45 +534,34 @@ public class RemotePSMP extends PlaybackServiceMediaPlayer {
callback.onPlaybackEnded(nextMedia.getMediaType(), !playNextEpisode);
// setting media to null signals to playMediaObject() that we're taking care of post-playback processing
media = null;
- playMediaObject(nextMedia, false, true /*TODO for now we always stream*/, playNextEpisode, playNextEpisode);
+ playMediaObject(nextMedia, false, true, playNextEpisode, playNextEpisode);
}
}
if (shouldContinue || toStoppedState) {
- boolean shouldPostProcess = true;
if (nextMedia == null) {
- try {
- castMgr.stop();
- shouldPostProcess = false;
- } catch (CastException | TransientNetworkDisconnectionException | NoConnectionException e) {
- Log.e(TAG, "Unable to stop playback", e);
- callback.onPlaybackEnded(null, true);
- stop();
- }
- }
- if (shouldPostProcess) {
+ remoteMediaClient.stop();
// Otherwise we rely on the chromecast callback to tell us the playback has stopped.
- callback.onPostPlayback(currentMedia, hasEnded, wasSkipped, nextMedia != null);
+ callback.onPostPlayback(currentMedia, hasEnded, wasSkipped, false);
+ } else {
+ callback.onPostPlayback(currentMedia, hasEnded, wasSkipped, true);
}
} else if (isPlaying) {
callback.onPlaybackPause(currentMedia,
currentMedia != null ? currentMedia.getPosition() : INVALID_TIME);
}
- FutureTask<?> future = new FutureTask<>(() -> {}, null);
+ FutureTask<?> future = new FutureTask<>(() -> { }, null);
future.run();
return future;
}
- private void stop() {
- if (playerStatus == PlayerStatus.INDETERMINATE) {
- setPlayerStatus(PlayerStatus.STOPPED, null);
- } else {
- Log.d(TAG, "Ignored call to stop: Current player state is: " + playerStatus);
- }
- }
-
@Override
protected boolean shouldLockWifi() {
return false;
}
+
+ @Override
+ public boolean isCasting() {
+ return true;
+ }
}
diff --git a/playback/cast/src/play/java/de/danoeh/antennapod/playback/cast/CastStateListener.java b/playback/cast/src/play/java/de/danoeh/antennapod/playback/cast/CastStateListener.java
new file mode 100644
index 000000000..39f54b11c
--- /dev/null
+++ b/playback/cast/src/play/java/de/danoeh/antennapod/playback/cast/CastStateListener.java
@@ -0,0 +1,69 @@
+package de.danoeh.antennapod.playback.cast;
+
+import android.content.Context;
+import androidx.annotation.NonNull;
+import com.google.android.gms.cast.framework.CastContext;
+import com.google.android.gms.cast.framework.CastSession;
+import com.google.android.gms.cast.framework.SessionManagerListener;
+import com.google.android.gms.common.ConnectionResult;
+import com.google.android.gms.common.GoogleApiAvailability;
+
+public class CastStateListener implements SessionManagerListener<CastSession> {
+ private final CastContext castContext;
+
+ public CastStateListener(Context context) {
+ if (GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context) != ConnectionResult.SUCCESS) {
+ castContext = null;
+ return;
+ }
+ castContext = CastContext.getSharedInstance(context);
+ castContext.getSessionManager().addSessionManagerListener(this, CastSession.class);
+ }
+
+ public void destroy() {
+ if (castContext != null) {
+ castContext.getSessionManager().removeSessionManagerListener(this, CastSession.class);
+ }
+ }
+
+ @Override
+ public void onSessionStarting(@NonNull CastSession castSession) {
+ }
+
+ @Override
+ public void onSessionStarted(@NonNull CastSession session, @NonNull String sessionId) {
+ onSessionStartedOrEnded();
+ }
+
+ @Override
+ public void onSessionStartFailed(@NonNull CastSession castSession, int i) {
+ }
+
+ @Override
+ public void onSessionEnding(@NonNull CastSession castSession) {
+ }
+
+ @Override
+ public void onSessionResumed(@NonNull CastSession session, boolean wasSuspended) {
+ }
+
+ @Override
+ public void onSessionResumeFailed(@NonNull CastSession castSession, int i) {
+ }
+
+ @Override
+ public void onSessionSuspended(@NonNull CastSession castSession, int i) {
+ }
+
+ @Override
+ public void onSessionEnded(@NonNull CastSession session, int error) {
+ onSessionStartedOrEnded();
+ }
+
+ @Override
+ public void onSessionResuming(@NonNull CastSession castSession, @NonNull String s) {
+ }
+
+ public void onSessionStartedOrEnded() {
+ }
+}
diff --git a/playback/cast/src/play/java/de/danoeh/antennapod/playback/cast/CastUtils.java b/playback/cast/src/play/java/de/danoeh/antennapod/playback/cast/CastUtils.java
new file mode 100644
index 000000000..312b6b2f9
--- /dev/null
+++ b/playback/cast/src/play/java/de/danoeh/antennapod/playback/cast/CastUtils.java
@@ -0,0 +1,181 @@
+package de.danoeh.antennapod.playback.cast;
+
+import android.content.ContentResolver;
+import android.util.Log;
+import android.text.TextUtils;
+import com.google.android.gms.cast.CastDevice;
+import com.google.android.gms.cast.MediaInfo;
+import com.google.android.gms.cast.MediaMetadata;
+import com.google.android.gms.cast.framework.CastSession;
+import com.google.android.gms.common.images.WebImage;
+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.playback.Playable;
+import de.danoeh.antennapod.model.playback.RemoteMedia;
+
+import java.util.List;
+
+/**
+ * Helper functions for Cast support.
+ */
+public class CastUtils {
+ private CastUtils() {
+ }
+
+ private static final String TAG = "CastUtils";
+
+ public static final String KEY_MEDIA_ID = "de.danoeh.antennapod.core.cast.MediaId";
+
+ public static final String KEY_EPISODE_IDENTIFIER = "de.danoeh.antennapod.core.cast.EpisodeId";
+ public static final String KEY_EPISODE_LINK = "de.danoeh.antennapod.core.cast.EpisodeLink";
+ public static final String KEY_STREAM_URL = "de.danoeh.antennapod.core.cast.StreamUrl";
+ public static final String KEY_FEED_URL = "de.danoeh.antennapod.core.cast.FeedUrl";
+ public static final String KEY_FEED_WEBSITE = "de.danoeh.antennapod.core.cast.FeedWebsite";
+ public static final String KEY_EPISODE_NOTES = "de.danoeh.antennapod.core.cast.EpisodeNotes";
+
+ /**
+ * The field <code>AntennaPod.FormatVersion</code> specifies which version of MediaMetaData
+ * fields we're using. Future implementations should try to be backwards compatible with earlier
+ * versions, and earlier versions should be forward compatible until the version indicated by
+ * <code>MAX_VERSION_FORWARD_COMPATIBILITY</code>. If an update makes the format unreadable for
+ * an earlier version, then its version number should be greater than the
+ * <code>MAX_VERSION_FORWARD_COMPATIBILITY</code> value set on the earlier one, so that it
+ * doesn't try to parse the object.
+ */
+ public static final String KEY_FORMAT_VERSION = "de.danoeh.antennapod.core.cast.FormatVersion";
+ public static final int FORMAT_VERSION_VALUE = 1;
+ public static final int MAX_VERSION_FORWARD_COMPATIBILITY = 9999;
+
+ public static boolean isCastable(Playable media, CastSession castSession) {
+ if (media == null || castSession == null || castSession.getCastDevice() == null) {
+ return false;
+ }
+ if (media instanceof FeedMedia || media instanceof RemoteMedia) {
+ String url = media.getStreamUrl();
+ if (url == null || url.isEmpty()) {
+ return false;
+ }
+ if (url.startsWith(ContentResolver.SCHEME_CONTENT)) {
+ return false; // Local feed
+ }
+ switch (media.getMediaType()) {
+ case AUDIO:
+ return castSession.getCastDevice().hasCapability(CastDevice.CAPABILITY_AUDIO_OUT);
+ case VIDEO:
+ return castSession.getCastDevice().hasCapability(CastDevice.CAPABILITY_VIDEO_OUT);
+ default:
+ return false;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Converts {@link MediaInfo} objects into the appropriate implementation of {@link Playable}.
+ * @return {@link Playable} object in a format proper for casting.
+ */
+ public static Playable makeRemoteMedia(MediaInfo media) {
+ MediaMetadata metadata = media.getMetadata();
+ int version = metadata.getInt(KEY_FORMAT_VERSION);
+ if (version <= 0 || version > MAX_VERSION_FORWARD_COMPATIBILITY) {
+ Log.w(TAG, "MediaInfo object obtained from the cast device is not compatible with this"
+ + "version of AntennaPod CastUtils, curVer=" + FORMAT_VERSION_VALUE
+ + ", object version=" + version);
+ return null;
+ }
+ List<WebImage> imageList = metadata.getImages();
+ String imageUrl = null;
+ if (!imageList.isEmpty()) {
+ imageUrl = imageList.get(0).getUrl().toString();
+ }
+ String notes = metadata.getString(KEY_EPISODE_NOTES);
+ RemoteMedia result = new RemoteMedia(media.getContentId(),
+ metadata.getString(KEY_EPISODE_IDENTIFIER),
+ metadata.getString(KEY_FEED_URL),
+ metadata.getString(MediaMetadata.KEY_SUBTITLE),
+ metadata.getString(MediaMetadata.KEY_TITLE),
+ metadata.getString(KEY_EPISODE_LINK),
+ metadata.getString(MediaMetadata.KEY_ARTIST),
+ imageUrl,
+ metadata.getString(KEY_FEED_WEBSITE),
+ media.getContentType(),
+ metadata.getDate(MediaMetadata.KEY_RELEASE_DATE).getTime(),
+ notes);
+ if (result.getDuration() == 0 && media.getStreamDuration() > 0) {
+ result.setDuration((int) media.getStreamDuration());
+ }
+ return result;
+ }
+
+ /**
+ * Compares a {@link MediaInfo} instance with a {@link FeedMedia} one and evaluates whether they
+ * represent the same podcast episode.
+ *
+ * @param info the {@link MediaInfo} object to be compared.
+ * @param media the {@link FeedMedia} object to be compared.
+ * @return <true>true</true> if there's a match, <code>false</code> otherwise.
+ *
+ * @see RemoteMedia#equals(Object)
+ */
+ public static boolean matches(MediaInfo info, FeedMedia media) {
+ if (info == null || media == null) {
+ return false;
+ }
+ if (!TextUtils.equals(info.getContentId(), media.getStreamUrl())) {
+ return false;
+ }
+ MediaMetadata metadata = info.getMetadata();
+ FeedItem fi = media.getItem();
+ if (fi == null || metadata == null
+ || !TextUtils.equals(metadata.getString(KEY_EPISODE_IDENTIFIER), fi.getItemIdentifier())) {
+ return false;
+ }
+ Feed feed = fi.getFeed();
+ return feed != null && TextUtils.equals(metadata.getString(KEY_FEED_URL), feed.getDownload_url());
+ }
+
+ /**
+ * Compares a {@link MediaInfo} instance with a {@link RemoteMedia} one and evaluates whether they
+ * represent the same podcast episode.
+ *
+ * @param info the {@link MediaInfo} object to be compared.
+ * @param media the {@link RemoteMedia} object to be compared.
+ * @return <true>true</true> if there's a match, <code>false</code> otherwise.
+ *
+ * @see RemoteMedia#equals(Object)
+ */
+ public static boolean matches(MediaInfo info, RemoteMedia media) {
+ if (info == null || media == null) {
+ return false;
+ }
+ if (!TextUtils.equals(info.getContentId(), media.getStreamUrl())) {
+ return false;
+ }
+ MediaMetadata metadata = info.getMetadata();
+ return metadata != null
+ && TextUtils.equals(metadata.getString(KEY_EPISODE_IDENTIFIER), media.getEpisodeIdentifier())
+ && TextUtils.equals(metadata.getString(KEY_FEED_URL), media.getFeedUrl());
+ }
+
+ /**
+ * Compares a {@link MediaInfo} instance with a {@link Playable} and evaluates whether they
+ * represent the same podcast episode. Useful every time we get a MediaInfo from the Cast Device
+ * and want to avoid unnecessary conversions.
+ *
+ * @param info the {@link MediaInfo} object to be compared.
+ * @param media the {@link Playable} object to be compared.
+ * @return <true>true</true> if there's a match, <code>false</code> otherwise.
+ *
+ * @see RemoteMedia#equals(Object)
+ */
+ public static boolean matches(MediaInfo info, Playable media) {
+ if (info == null || media == null) {
+ return false;
+ }
+ if (media instanceof RemoteMedia) {
+ return matches(info, (RemoteMedia) media);
+ }
+ return media instanceof FeedMedia && matches(info, (FeedMedia) media);
+ }
+}
diff --git a/playback/cast/src/play/java/de/danoeh/antennapod/playback/cast/MediaInfoCreator.java b/playback/cast/src/play/java/de/danoeh/antennapod/playback/cast/MediaInfoCreator.java
new file mode 100644
index 000000000..dd408d4a7
--- /dev/null
+++ b/playback/cast/src/play/java/de/danoeh/antennapod/playback/cast/MediaInfoCreator.java
@@ -0,0 +1,135 @@
+package de.danoeh.antennapod.playback.cast;
+
+import android.net.Uri;
+import android.text.TextUtils;
+import com.google.android.gms.cast.MediaInfo;
+import com.google.android.gms.cast.MediaMetadata;
+import com.google.android.gms.common.images.WebImage;
+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.playback.RemoteMedia;
+import java.util.Calendar;
+
+public class MediaInfoCreator {
+ public static MediaInfo from(RemoteMedia media) {
+ MediaMetadata metadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_GENERIC);
+
+ metadata.putString(MediaMetadata.KEY_TITLE, media.getEpisodeTitle());
+ metadata.putString(MediaMetadata.KEY_SUBTITLE, media.getFeedTitle());
+ if (!TextUtils.isEmpty(media.getImageLocation())) {
+ metadata.addImage(new WebImage(Uri.parse(media.getImageLocation())));
+ }
+ Calendar calendar = Calendar.getInstance();
+ calendar.setTime(media.getPubDate());
+ metadata.putDate(MediaMetadata.KEY_RELEASE_DATE, calendar);
+ if (!TextUtils.isEmpty(media.getFeedAuthor())) {
+ metadata.putString(MediaMetadata.KEY_ARTIST, media.getFeedAuthor());
+ }
+ if (!TextUtils.isEmpty(media.getFeedUrl())) {
+ metadata.putString(CastUtils.KEY_FEED_URL, media.getFeedUrl());
+ }
+ if (!TextUtils.isEmpty(media.getFeedLink())) {
+ metadata.putString(CastUtils.KEY_FEED_WEBSITE, media.getFeedLink());
+ }
+ if (!TextUtils.isEmpty(media.getEpisodeIdentifier())) {
+ metadata.putString(CastUtils.KEY_EPISODE_IDENTIFIER, media.getEpisodeIdentifier());
+ } else {
+ metadata.putString(CastUtils.KEY_EPISODE_IDENTIFIER, media.getDownloadUrl());
+ }
+ if (!TextUtils.isEmpty(media.getEpisodeLink())) {
+ metadata.putString(CastUtils.KEY_EPISODE_LINK, media.getEpisodeLink());
+ }
+ String notes = media.getNotes();
+ if (notes != null) {
+ metadata.putString(CastUtils.KEY_EPISODE_NOTES, notes);
+ }
+ // Default id value
+ metadata.putInt(CastUtils.KEY_MEDIA_ID, 0);
+ metadata.putInt(CastUtils.KEY_FORMAT_VERSION, CastUtils.FORMAT_VERSION_VALUE);
+ metadata.putString(CastUtils.KEY_STREAM_URL, media.getStreamUrl());
+
+ MediaInfo.Builder builder = new MediaInfo.Builder(media.getDownloadUrl())
+ .setContentType(media.getMimeType())
+ .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
+ .setMetadata(metadata);
+ if (media.getDuration() > 0) {
+ builder.setStreamDuration(media.getDuration());
+ }
+ return builder.build();
+ }
+
+ /**
+ * Converts {@link FeedMedia} objects into a format suitable for sending to a Cast Device.
+ * Before using this method, one should make sure isCastable(Playable) returns
+ * {@code true}. This method should not run on the main thread.
+ *
+ * @param media The {@link FeedMedia} object to be converted.
+ * @return {@link MediaInfo} object in a format proper for casting.
+ */
+ public static MediaInfo from(FeedMedia media) {
+ if (media == null) {
+ return null;
+ }
+ MediaMetadata metadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_GENERIC);
+ if (media.getItem() == null) {
+ throw new IllegalStateException("item is null");
+ //media.setItem(DBReader.getFeedItem(media.getItemId()));
+ }
+ FeedItem feedItem = media.getItem();
+ if (feedItem != null) {
+ metadata.putString(MediaMetadata.KEY_TITLE, media.getEpisodeTitle());
+ String subtitle = media.getFeedTitle();
+ if (subtitle != null) {
+ metadata.putString(MediaMetadata.KEY_SUBTITLE, subtitle);
+ }
+
+ // Manual because cast does not support embedded images
+ String url = feedItem.getImageUrl() == null ? feedItem.getFeed().getImageUrl() : feedItem.getImageUrl();
+ if (!TextUtils.isEmpty(url)) {
+ metadata.addImage(new WebImage(Uri.parse(url)));
+ }
+ Calendar calendar = Calendar.getInstance();
+ calendar.setTime(media.getItem().getPubDate());
+ metadata.putDate(MediaMetadata.KEY_RELEASE_DATE, calendar);
+ Feed feed = feedItem.getFeed();
+ if (feed != null) {
+ if (!TextUtils.isEmpty(feed.getAuthor())) {
+ metadata.putString(MediaMetadata.KEY_ARTIST, feed.getAuthor());
+ }
+ if (!TextUtils.isEmpty(feed.getDownload_url())) {
+ metadata.putString(CastUtils.KEY_FEED_URL, feed.getDownload_url());
+ }
+ if (!TextUtils.isEmpty(feed.getLink())) {
+ metadata.putString(CastUtils.KEY_FEED_WEBSITE, feed.getLink());
+ }
+ }
+ if (!TextUtils.isEmpty(feedItem.getItemIdentifier())) {
+ metadata.putString(CastUtils.KEY_EPISODE_IDENTIFIER, feedItem.getItemIdentifier());
+ } else {
+ metadata.putString(CastUtils.KEY_EPISODE_IDENTIFIER, media.getStreamUrl());
+ }
+ if (!TextUtils.isEmpty(feedItem.getLink())) {
+ metadata.putString(CastUtils.KEY_EPISODE_LINK, feedItem.getLink());
+ }
+ }
+ // This field only identifies the id on the device that has the original version.
+ // Idea is to perhaps, on a first approach, check if the version on the local DB with the
+ // same id matches the remote object, and if not then search for episode and feed identifiers.
+ // This at least should make media recognition for a single device much quicker.
+ metadata.putInt(CastUtils.KEY_MEDIA_ID, ((Long) media.getIdentifier()).intValue());
+ // A way to identify different casting media formats in case we change it in the future and
+ // senders with different versions share a casting device.
+ metadata.putInt(CastUtils.KEY_FORMAT_VERSION, CastUtils.FORMAT_VERSION_VALUE);
+ metadata.putString(CastUtils.KEY_STREAM_URL, media.getStreamUrl());
+
+ MediaInfo.Builder builder = new MediaInfo.Builder(media.getStreamUrl())
+ .setContentType(media.getMime_type())
+ .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
+ .setMetadata(metadata);
+ if (media.getDuration() > 0) {
+ builder.setStreamDuration(media.getDuration());
+ }
+ return builder.build();
+ }
+}
diff --git a/playback/cast/src/play/res/menu/cast_button.xml b/playback/cast/src/play/res/menu/cast_button.xml
new file mode 100644
index 000000000..6e65bce18
--- /dev/null
+++ b/playback/cast/src/play/res/menu/cast_button.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+
+ <item
+ android:id="@+id/media_route_menu_item"
+ android:title=""
+ app:actionProviderClass="androidx.mediarouter.app.MediaRouteActionProvider"
+ app:showAsAction="always" />
+
+</menu>
diff --git a/settings.gradle b/settings.gradle
index f73020141..c7f5e6449 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1,5 +1,6 @@
include ':app'
include ':core'
+include ':event'
include ':model'
include ':net:ssl'
@@ -9,6 +10,9 @@ include ':net:sync:model'
include ':parser:feed'
include ':parser:media'
+include ':playback:base'
+include ':playback:cast'
+
include ':ui:app-start-intent'
include ':ui:common'
include ':ui:png-icons'
diff --git a/ui/app-start-intent/src/main/java/de/danoeh/antennapod/ui/appstartintent/MainActivityStarter.java b/ui/app-start-intent/src/main/java/de/danoeh/antennapod/ui/appstartintent/MainActivityStarter.java
index 33f96f141..88c0378c1 100644
--- a/ui/app-start-intent/src/main/java/de/danoeh/antennapod/ui/appstartintent/MainActivityStarter.java
+++ b/ui/app-start-intent/src/main/java/de/danoeh/antennapod/ui/appstartintent/MainActivityStarter.java
@@ -3,6 +3,7 @@ package de.danoeh.antennapod.ui.appstartintent;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
+import android.os.Build;
/**
* Launches the main activity of the app with specific arguments.
@@ -26,8 +27,8 @@ public class MainActivityStarter {
}
public PendingIntent getPendingIntent() {
- return PendingIntent.getActivity(context, R.id.pending_intent_player_activity,
- getIntent(), PendingIntent.FLAG_UPDATE_CURRENT);
+ return PendingIntent.getActivity(context, R.id.pending_intent_player_activity, getIntent(),
+ PendingIntent.FLAG_UPDATE_CURRENT | (Build.VERSION.SDK_INT >= 23 ? PendingIntent.FLAG_IMMUTABLE : 0));
}
public void start() {
diff --git a/ui/app-start-intent/src/main/java/de/danoeh/antennapod/ui/appstartintent/VideoPlayerActivityStarter.java b/ui/app-start-intent/src/main/java/de/danoeh/antennapod/ui/appstartintent/VideoPlayerActivityStarter.java
index 7536d34b6..53f8719de 100644
--- a/ui/app-start-intent/src/main/java/de/danoeh/antennapod/ui/appstartintent/VideoPlayerActivityStarter.java
+++ b/ui/app-start-intent/src/main/java/de/danoeh/antennapod/ui/appstartintent/VideoPlayerActivityStarter.java
@@ -28,8 +28,8 @@ public class VideoPlayerActivityStarter {
}
public PendingIntent getPendingIntent() {
- return PendingIntent.getActivity(context, R.id.pending_intent_video_player,
- getIntent(), PendingIntent.FLAG_UPDATE_CURRENT);
+ return PendingIntent.getActivity(context, R.id.pending_intent_video_player, getIntent(),
+ PendingIntent.FLAG_UPDATE_CURRENT | (Build.VERSION.SDK_INT >= 23 ? PendingIntent.FLAG_IMMUTABLE : 0));
}
public void start() {
diff --git a/ui/common/src/main/java/de/danoeh/antennapod/ui/common/ThemeUtils.java b/ui/common/src/main/java/de/danoeh/antennapod/ui/common/ThemeUtils.java
index 392d09e07..12a2f6323 100644
--- a/ui/common/src/main/java/de/danoeh/antennapod/ui/common/ThemeUtils.java
+++ b/ui/common/src/main/java/de/danoeh/antennapod/ui/common/ThemeUtils.java
@@ -5,6 +5,7 @@ import androidx.annotation.AttrRes;
import androidx.annotation.ColorInt;
import android.util.TypedValue;
import androidx.annotation.DrawableRes;
+import androidx.core.content.ContextCompat;
public class ThemeUtils {
private ThemeUtils() {
@@ -14,6 +15,9 @@ public class ThemeUtils {
public static @ColorInt int getColorFromAttr(Context context, @AttrRes int attr) {
TypedValue typedValue = new TypedValue();
context.getTheme().resolveAttribute(attr, typedValue, true);
+ if (typedValue.resourceId != 0) {
+ return ContextCompat.getColor(context, typedValue.resourceId);
+ }
return typedValue.data;
}
diff --git a/ui/png-icons/src/main/res/drawable/ic_notification_cast_off.xml b/ui/png-icons/src/main/res/drawable/ic_notification_cast_off.xml
deleted file mode 100644
index 3e3accd0b..000000000
--- a/ui/png-icons/src/main/res/drawable/ic_notification_cast_off.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:height="30dp" android:viewportHeight="24.0"
- android:viewportWidth="24.0" android:width="30dp">
- <path android:fillColor="#FFFFFFFF" android:pathData="M1.6,1.27L0.25,2.75L1.41,3.8C1.16,4.13 1,4.55 1,5V8H3V5.23L18.2,19H14V21H20.41L22.31,22.72L23.65,21.24M6.5,3L8.7,5H21V16.14L23,17.95V5C23,3.89 22.1,3 21,3M1,10V12A9,9 0 0,1 10,21H12C12,14.92 7.08,10 1,10M1,14V16A5,5 0 0,1 6,21H8A7,7 0 0,0 1,14M1,18V21H4A3,3 0 0,0 1,18Z" />
-</vector> \ No newline at end of file