From 759ce1b3574587b7045368c1ae476032b8b79942 Mon Sep 17 00:00:00 2001 From: Max Bechtold Date: Sat, 17 Aug 2019 15:46:08 +0200 Subject: [PATCH] #3248 Adapt volume for playing media if its volume reduction setting is changed --- .../fragment/FeedSettingsFragment.java | 13 +- .../playback/PlaybackVolumeAdaptor.java | 49 ++++ .../service/playback/PlaybackService.java | 28 ++- .../playback/PlaybackVolumeAdaptorTest.java | 230 ++++++++++++++++++ 4 files changed, 317 insertions(+), 3 deletions(-) create mode 100644 core/src/free/java/de/danoeh/antennapod/core/service/playback/PlaybackVolumeAdaptor.java create mode 100644 core/src/test/java/de/danoeh/antennapod/core/service/playback/PlaybackVolumeAdaptorTest.java 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 a71be76d7..f651a8930 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/FeedSettingsFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/FeedSettingsFragment.java @@ -3,6 +3,7 @@ package de.danoeh.antennapod.fragment; import android.arch.lifecycle.ViewModelProviders; import android.content.Context; import android.content.DialogInterface; +import android.content.Intent; import android.os.Bundle; import android.support.v14.preference.SwitchPreference; import android.support.v7.preference.ListPreference; @@ -13,6 +14,7 @@ import de.danoeh.antennapod.core.feed.Feed; import de.danoeh.antennapod.core.feed.FeedFilter; import de.danoeh.antennapod.core.feed.FeedPreferences; 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.dialog.AuthenticationDialog; import de.danoeh.antennapod.dialog.EpisodeFilterDialog; @@ -132,11 +134,20 @@ public class FeedSettingsFragment extends PreferenceFragmentCompat { } feed.savePreferences(); updateVolumeReductionValue(); - // TODO maxbechtold Check if we can call setVolume for the PlaybackService, if running. Else, show toast? + sendVolumeReductionChangedIntent(); + return false; }); } + private void sendVolumeReductionChangedIntent() { + Context context = getContext(); + Intent intent = new Intent(PlaybackService.ACTION_VOLUME_REDUCTION_CHANGED).setPackage(context.getPackageName()); + intent.putExtra(PlaybackService.EXTRA_VOLUME_REDUCTION_AFFECTED_FEED, feed.getIdentifyingValue()); + intent.putExtra(PlaybackService.EXTRA_VOLUME_REDUCTION_SETTING, feedPreferences.getVolumeReductionSetting()); + context.sendBroadcast(intent); + } + private void updateVolumeReductionValue() { ListPreference volumeReductionPreference = (ListPreference) findPreference("volumeReduction"); diff --git a/core/src/free/java/de/danoeh/antennapod/core/service/playback/PlaybackVolumeAdaptor.java b/core/src/free/java/de/danoeh/antennapod/core/service/playback/PlaybackVolumeAdaptor.java new file mode 100644 index 000000000..949f8c8cc --- /dev/null +++ b/core/src/free/java/de/danoeh/antennapod/core/service/playback/PlaybackVolumeAdaptor.java @@ -0,0 +1,49 @@ +package de.danoeh.antennapod.core.service.playback; + +import de.danoeh.antennapod.core.feed.FeedMedia; +import de.danoeh.antennapod.core.feed.FeedPreferences; +import de.danoeh.antennapod.core.util.playback.Playable; + +class PlaybackVolumeAdaptor { + + public void adaptVolumeIfNecessary(PlaybackServiceMediaPlayer mediaPlayer, String affectedFeedIdentifier, FeedPreferences.VolumeReductionSetting volumeReductionSetting) { + Playable playable = mediaPlayer.getPlayable(); + boolean isFeedMedia = playable instanceof FeedMedia; + boolean isPlayableLoaded = isPlayableLoaded(mediaPlayer.getPlayerStatus()); + + if (isPlayableLoaded && isFeedMedia) { + adaptFeedMediaVolumeIfNecessary(mediaPlayer, affectedFeedIdentifier, volumeReductionSetting, (FeedMedia) playable); + } + } + + private void adaptFeedMediaVolumeIfNecessary(PlaybackServiceMediaPlayer mediaPlayer, String affectedFeedIdentifier, FeedPreferences.VolumeReductionSetting volumeReductionSetting, FeedMedia feedMedia) { + if (mediaBelongsToAffectedFeed(feedMedia, affectedFeedIdentifier)) { + FeedPreferences preferences = feedMedia.getItem().getFeed().getPreferences(); + preferences.setVolumeReductionSetting(volumeReductionSetting); + + if (mediaPlayer.getPlayerStatus() == PlayerStatus.PLAYING) { + forceAdaptVolume(mediaPlayer); + } + } + } + + private static boolean isPlayableLoaded(PlayerStatus playerStatus) { + return playerStatus == PlayerStatus.PLAYING + || playerStatus == PlayerStatus.PAUSED + || playerStatus == PlayerStatus.SEEKING + || playerStatus == PlayerStatus.PREPARING + || playerStatus == PlayerStatus.PREPARED + || playerStatus == PlayerStatus.INITIALIZING; + } + + private static boolean mediaBelongsToAffectedFeed(FeedMedia feedMedia, String affectedFeedIdentifier) { + return affectedFeedIdentifier != null + && affectedFeedIdentifier.equals(feedMedia.getItem().getFeed().getIdentifyingValue()); + } + + private static void forceAdaptVolume(PlaybackServiceMediaPlayer mediaPlayer) { + mediaPlayer.pause(false, false); + mediaPlayer.resume(); + } + +} 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 ab1edc8e9..632dfd50e 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 @@ -12,7 +12,6 @@ import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; import android.graphics.Bitmap; -import android.graphics.BitmapFactory; import android.media.AudioManager; import android.media.MediaPlayer; import android.net.Uri; @@ -43,6 +42,7 @@ import com.bumptech.glide.Glide; import com.bumptech.glide.request.RequestOptions; import com.bumptech.glide.request.target.Target; +import java.io.Serializable; import java.util.ArrayList; import java.util.List; @@ -54,6 +54,7 @@ import de.danoeh.antennapod.core.feed.Chapter; import de.danoeh.antennapod.core.feed.Feed; import de.danoeh.antennapod.core.feed.FeedItem; import de.danoeh.antennapod.core.feed.FeedMedia; +import de.danoeh.antennapod.core.feed.FeedPreferences; import de.danoeh.antennapod.core.feed.MediaType; import de.danoeh.antennapod.core.feed.SearchResult; import de.danoeh.antennapod.core.glide.ApGlideSettings; @@ -66,7 +67,6 @@ 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.util.IntList; import de.danoeh.antennapod.core.util.IntentUtils; import de.danoeh.antennapod.core.util.NetworkUtils; import de.danoeh.antennapod.core.util.QueueAccess; @@ -80,6 +80,7 @@ import org.greenrobot.eventbus.EventBus; * Controls the MediaPlayer that plays a FeedMedia-file */ public class PlaybackService extends MediaBrowserServiceCompat { + /** * Logging tag */ @@ -131,6 +132,12 @@ public class PlaybackService extends MediaBrowserServiceCompat { */ public static final String ACTION_PAUSE_PLAY_CURRENT_EPISODE = "action.de.danoeh.antennapod.core.service.pausePlayCurrentEpisode"; + /** + * If the PlaybackService receives this action, it will try to apply the supplied volume reduction setting. + */ + public static final String ACTION_VOLUME_REDUCTION_CHANGED = "action.de.danoeh.antennapod.core.service.volumedReductionChanged"; + public static final String EXTRA_VOLUME_REDUCTION_SETTING = "PlaybackService.VolumeReductionSettingExtra"; + public static final String EXTRA_VOLUME_REDUCTION_AFFECTED_FEED = "PlaybackService.VolumeReductionSettingAffectedFeed"; /** * If the PlaybackService receives this action, it will resume playback. @@ -279,6 +286,7 @@ public class PlaybackService extends MediaBrowserServiceCompat { registerReceiver(skipCurrentEpisodeReceiver, new IntentFilter(ACTION_SKIP_CURRENT_EPISODE)); registerReceiver(pausePlayCurrentEpisodeReceiver, new IntentFilter(ACTION_PAUSE_PLAY_CURRENT_EPISODE)); registerReceiver(pauseResumeCurrentEpisodeReceiver, new IntentFilter(ACTION_RESUME_PLAY_CURRENT_EPISODE)); + registerReceiver(volumeReductionChangedReceiver, new IntentFilter(ACTION_VOLUME_REDUCTION_CHANGED)); taskManager = new PlaybackServiceTaskManager(this, taskManagerCallback); flavorHelper = new PlaybackServiceFlavorHelper(PlaybackService.this, flavorHelperCallback); @@ -1517,6 +1525,22 @@ public class PlaybackService extends MediaBrowserServiceCompat { } }; + private final BroadcastReceiver volumeReductionChangedReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (TextUtils.equals(intent.getAction(), ACTION_VOLUME_REDUCTION_CHANGED)) { + Log.d(TAG, "Received ACTION_VOLUME_REDUCTION_CHANGED intent"); + + String affectedFeed = intent.getStringExtra(EXTRA_VOLUME_REDUCTION_AFFECTED_FEED); + Serializable volumeReductionExtra = intent.getSerializableExtra(EXTRA_VOLUME_REDUCTION_SETTING); + FeedPreferences.VolumeReductionSetting volumeReductionSetting = (FeedPreferences.VolumeReductionSetting) volumeReductionExtra; + + PlaybackVolumeAdaptor playbackVolumeAdaptor = new PlaybackVolumeAdaptor(); + playbackVolumeAdaptor.adaptVolumeIfNecessary(mediaPlayer, affectedFeed, volumeReductionSetting); + } + } + }; + public static MediaType getCurrentMediaType() { return currentMediaType; } diff --git a/core/src/test/java/de/danoeh/antennapod/core/service/playback/PlaybackVolumeAdaptorTest.java b/core/src/test/java/de/danoeh/antennapod/core/service/playback/PlaybackVolumeAdaptorTest.java new file mode 100644 index 000000000..9036624d8 --- /dev/null +++ b/core/src/test/java/de/danoeh/antennapod/core/service/playback/PlaybackVolumeAdaptorTest.java @@ -0,0 +1,230 @@ +package de.danoeh.antennapod.core.service.playback; + +import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.feed.FeedMedia; +import de.danoeh.antennapod.core.feed.FeedPreferences; +import de.danoeh.antennapod.core.feed.FeedPreferences.VolumeReductionSetting; +import de.danoeh.antennapod.core.util.playback.Playable; +import org.junit.Before; +import org.junit.Test; + +import static org.mockito.Mockito.anyBoolean; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class PlaybackVolumeAdaptorTest { + + private static final String FEED_ID = "feedId"; + + private PlaybackServiceMediaPlayer mediaPlayer; + + @Before + public void setUp() throws Exception { + mediaPlayer = mock(PlaybackServiceMediaPlayer.class); + } + + @Test + public void noChangeIfNoFeedMediaPlaying() { + PlaybackVolumeAdaptor playbackVolumeAdaptor = new PlaybackVolumeAdaptor(); + + when(mediaPlayer.getPlayerStatus()).thenReturn(PlayerStatus.PAUSED); + + Playable noFeedMedia = mock(Playable.class); + when(mediaPlayer.getPlayable()).thenReturn(noFeedMedia); + + playbackVolumeAdaptor.adaptVolumeIfNecessary(mediaPlayer, FEED_ID, VolumeReductionSetting.OFF); + + verify(mediaPlayer, never()).pause(anyBoolean(), anyBoolean()); + verify(mediaPlayer, never()).resume(); + } + + @Test + public void noChangeIfPlayerStatusIsError() { + PlaybackVolumeAdaptor playbackVolumeAdaptor = new PlaybackVolumeAdaptor(); + + when(mediaPlayer.getPlayerStatus()).thenReturn(PlayerStatus.ERROR); + + FeedMedia feedMedia = mock(FeedMedia.class); + when(mediaPlayer.getPlayable()).thenReturn(feedMedia); + + playbackVolumeAdaptor.adaptVolumeIfNecessary(mediaPlayer, FEED_ID, VolumeReductionSetting.OFF); + + verify(mediaPlayer, never()).pause(anyBoolean(), anyBoolean()); + verify(mediaPlayer, never()).resume(); + } + + @Test + public void noChangeIfPlayerStatusIsIndeterminate() { + PlaybackVolumeAdaptor playbackVolumeAdaptor = new PlaybackVolumeAdaptor(); + + when(mediaPlayer.getPlayerStatus()).thenReturn(PlayerStatus.INDETERMINATE); + + FeedMedia feedMedia = mock(FeedMedia.class); + when(mediaPlayer.getPlayable()).thenReturn(feedMedia); + + playbackVolumeAdaptor.adaptVolumeIfNecessary(mediaPlayer, FEED_ID, VolumeReductionSetting.OFF); + + verify(mediaPlayer, never()).pause(anyBoolean(), anyBoolean()); + verify(mediaPlayer, never()).resume(); + } + + @Test + public void noChangeIfPlayerStatusIsStopped() { + PlaybackVolumeAdaptor playbackVolumeAdaptor = new PlaybackVolumeAdaptor(); + + when(mediaPlayer.getPlayerStatus()).thenReturn(PlayerStatus.STOPPED); + + FeedMedia feedMedia = mock(FeedMedia.class); + when(mediaPlayer.getPlayable()).thenReturn(feedMedia); + + playbackVolumeAdaptor.adaptVolumeIfNecessary(mediaPlayer, FEED_ID, VolumeReductionSetting.OFF); + + verify(mediaPlayer, never()).pause(anyBoolean(), anyBoolean()); + verify(mediaPlayer, never()).resume(); + } + + @Test + public void noChangeIfPlayableIsNoItemOfAffectedFeed() { + PlaybackVolumeAdaptor playbackVolumeAdaptor = new PlaybackVolumeAdaptor(); + + when(mediaPlayer.getPlayerStatus()).thenReturn(PlayerStatus.PLAYING); + + FeedMedia feedMedia = mock(FeedMedia.class); + when(mediaPlayer.getPlayable()).thenReturn(feedMedia); + Feed feed = mockFeed(feedMedia, FEED_ID); + when(feed.getIdentifyingValue()).thenReturn("wrongFeedId"); + + playbackVolumeAdaptor.adaptVolumeIfNecessary(mediaPlayer, FEED_ID, VolumeReductionSetting.OFF); + + verify(mediaPlayer, never()).pause(anyBoolean(), anyBoolean()); + verify(mediaPlayer, never()).resume(); + } + + @Test + public void adaptsPreferencesForLoadedFeedMediaIfPlayerStatusIsPaused() { + PlaybackVolumeAdaptor playbackVolumeAdaptor = new PlaybackVolumeAdaptor(); + + when(mediaPlayer.getPlayerStatus()).thenReturn(PlayerStatus.PAUSED); + + FeedMedia feedMedia = mock(FeedMedia.class); + when(mediaPlayer.getPlayable()).thenReturn(feedMedia); + FeedPreferences feedPreferences = mockFeedPreferences(feedMedia, FEED_ID); + + playbackVolumeAdaptor.adaptVolumeIfNecessary(mediaPlayer, FEED_ID, VolumeReductionSetting.LIGHT); + + verify(feedPreferences, times(1)).setVolumeReductionSetting(VolumeReductionSetting.LIGHT); + + verify(mediaPlayer, never()).pause(anyBoolean(), anyBoolean()); + verify(mediaPlayer, never()).resume(); + } + + @Test + public void adaptsPreferencesForLoadedFeedMediaIfPlayerStatusIsPrepared() { + PlaybackVolumeAdaptor playbackVolumeAdaptor = new PlaybackVolumeAdaptor(); + + when(mediaPlayer.getPlayerStatus()).thenReturn(PlayerStatus.PREPARED); + + FeedMedia feedMedia = mock(FeedMedia.class); + when(mediaPlayer.getPlayable()).thenReturn(feedMedia); + FeedPreferences feedPreferences = mockFeedPreferences(feedMedia, FEED_ID); + + playbackVolumeAdaptor.adaptVolumeIfNecessary(mediaPlayer, FEED_ID, VolumeReductionSetting.LIGHT); + + verify(feedPreferences, times(1)).setVolumeReductionSetting(VolumeReductionSetting.LIGHT); + + verify(mediaPlayer, never()).pause(anyBoolean(), anyBoolean()); + verify(mediaPlayer, never()).resume(); + } + + @Test + public void adaptsPreferencesForLoadedFeedMediaIfPlayerStatusIsInitializing() { + PlaybackVolumeAdaptor playbackVolumeAdaptor = new PlaybackVolumeAdaptor(); + + when(mediaPlayer.getPlayerStatus()).thenReturn(PlayerStatus.INITIALIZING); + + FeedMedia feedMedia = mock(FeedMedia.class); + when(mediaPlayer.getPlayable()).thenReturn(feedMedia); + FeedPreferences feedPreferences = mockFeedPreferences(feedMedia, FEED_ID); + + playbackVolumeAdaptor.adaptVolumeIfNecessary(mediaPlayer, FEED_ID, VolumeReductionSetting.LIGHT); + + verify(feedPreferences, times(1)).setVolumeReductionSetting(VolumeReductionSetting.LIGHT); + + verify(mediaPlayer, never()).pause(anyBoolean(), anyBoolean()); + verify(mediaPlayer, never()).resume(); + } + + @Test + public void adaptsPreferencesForLoadedFeedMediaIfPlayerStatusIsPreparing() { + PlaybackVolumeAdaptor playbackVolumeAdaptor = new PlaybackVolumeAdaptor(); + + when(mediaPlayer.getPlayerStatus()).thenReturn(PlayerStatus.PREPARING); + + FeedMedia feedMedia = mock(FeedMedia.class); + when(mediaPlayer.getPlayable()).thenReturn(feedMedia); + FeedPreferences feedPreferences = mockFeedPreferences(feedMedia, FEED_ID); + + playbackVolumeAdaptor.adaptVolumeIfNecessary(mediaPlayer, FEED_ID, VolumeReductionSetting.LIGHT); + + verify(feedPreferences, times(1)).setVolumeReductionSetting(VolumeReductionSetting.LIGHT); + + verify(mediaPlayer, never()).pause(anyBoolean(), anyBoolean()); + verify(mediaPlayer, never()).resume(); + } + + @Test + public void adaptsPreferencesForLoadedFeedMediaIfPlayerStatusIsSeeking() { + PlaybackVolumeAdaptor playbackVolumeAdaptor = new PlaybackVolumeAdaptor(); + + when(mediaPlayer.getPlayerStatus()).thenReturn(PlayerStatus.SEEKING); + + FeedMedia feedMedia = mock(FeedMedia.class); + when(mediaPlayer.getPlayable()).thenReturn(feedMedia); + FeedPreferences feedPreferences = mockFeedPreferences(feedMedia, FEED_ID); + + playbackVolumeAdaptor.adaptVolumeIfNecessary(mediaPlayer, FEED_ID, VolumeReductionSetting.LIGHT); + + verify(feedPreferences, times(1)).setVolumeReductionSetting(VolumeReductionSetting.LIGHT); + + verify(mediaPlayer, never()).pause(anyBoolean(), anyBoolean()); + verify(mediaPlayer, never()).resume(); + } + + @Test + public void adaptsPreferencesAndForcesVolumeChangeForLoadedFeedMediaIfPlayerStatusIsPlaying() { + PlaybackVolumeAdaptor playbackVolumeAdaptor = new PlaybackVolumeAdaptor(); + + when(mediaPlayer.getPlayerStatus()).thenReturn(PlayerStatus.PLAYING); + + FeedMedia feedMedia = mock(FeedMedia.class); + when(mediaPlayer.getPlayable()).thenReturn(feedMedia); + FeedPreferences feedPreferences = mockFeedPreferences(feedMedia, FEED_ID); + + playbackVolumeAdaptor.adaptVolumeIfNecessary(mediaPlayer, FEED_ID, VolumeReductionSetting.HEAVY); + + verify(feedPreferences, times(1)).setVolumeReductionSetting(VolumeReductionSetting.HEAVY); + + verify(mediaPlayer, times(1)).pause(false, false); + verify(mediaPlayer, times(1)).resume(); + } + + private FeedPreferences mockFeedPreferences(FeedMedia feedMedia, String feedId) { + Feed feed = mockFeed(feedMedia, feedId); + FeedPreferences feedPreferences = mock(FeedPreferences.class); + when(feed.getPreferences()).thenReturn(feedPreferences); + return feedPreferences; + } + + private Feed mockFeed(FeedMedia feedMedia, String feedId) { + FeedItem feedItem = mock(FeedItem.class); + when(feedMedia.getItem()).thenReturn(feedItem); + Feed feed = mock(Feed.class); + when(feed.getIdentifyingValue()).thenReturn(feedId); + when(feedItem.getFeed()).thenReturn(feed); + return feed; + } +}