Skip to content

Commit

Permalink
Custom Playback Effects: Hide segmented control for local files (#3097)
Browse files Browse the repository at this point in the history
Co-authored-by: Michał Sikora <[email protected]>
  • Loading branch information
ashiagr and MiSikora authored Oct 28, 2024
1 parent c8f1bd8 commit 7d8bc72
Show file tree
Hide file tree
Showing 3 changed files with 250 additions and 27 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,7 @@ import au.com.shiftyjelly.pocketcasts.views.fragments.BaseDialogFragment
import com.google.android.material.button.MaterialButtonToggleGroup
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.distinctUntilChanged
import au.com.shiftyjelly.pocketcasts.localization.R as LR

@AndroidEntryPoint
Expand Down Expand Up @@ -108,9 +107,9 @@ class EffectsFragment : BaseDialogFragment(), CompoundButton.OnCheckedChangeList
binding = null
}

private fun update(podcastEffectsPair: PlayerViewModel.PodcastEffectsPair) {
val podcast = podcastEffectsPair.podcast
val effects = podcastEffectsPair.effects
private fun update(podcastEffectsData: PlayerViewModel.PodcastEffectsData) {
val podcast = podcastEffectsData.podcast
val effects = podcastEffectsData.effects

val binding = binding ?: return

Expand Down Expand Up @@ -272,26 +271,27 @@ class EffectsFragment : BaseDialogFragment(), CompoundButton.OnCheckedChangeList
playbackManager.trackPlaybackEffectsEvent(event, props, SourceView.PLAYER_PLAYBACK_EFFECTS)
}

@OptIn(ExperimentalCoroutinesApi::class)
private fun FragmentEffectsBinding.setupEffectsSettingsSegmentedTabBar() {
effectsSettingsSegmentedTabBar.setContent {
val podcast by viewModel.effectsLive.asFlow()
.mapLatest { it.podcast }
val podcastEffectsData by viewModel.effectsLive.asFlow()
.distinctUntilChanged { t1, t2 -> t1.podcast.uuid == t2.podcast.uuid && t1.podcast.playbackEffects.toData() == t2.podcast.playbackEffects.toData() }
.collectAsStateWithLifecycle(null)
if (podcast == null) return@setContent

EffectsSettingsSegmentedTabBar(
selectedItem = if (podcast?.overrideGlobalEffects == true) {
PlaybackEffectsSettingsTab.ThisPodcast
} else {
PlaybackEffectsSettingsTab.AllPodcasts
},
onItemSelected = {
viewModel.updatedOverrideGlobalEffects(requireNotNull(podcast), PlaybackEffectsSettingsTab.entries[it])
},
modifier = Modifier
.padding(top = 24.dp),
)
val podcast = podcastEffectsData?.podcast ?: return@setContent

if (podcastEffectsData?.showCustomEffectsSettings == true) {
EffectsSettingsSegmentedTabBar(
selectedItem = if (podcastEffectsData?.podcast?.overrideGlobalEffects == true) {
PlaybackEffectsSettingsTab.ThisPodcast
} else {
PlaybackEffectsSettingsTab.AllPodcasts
},
onItemSelected = {
viewModel.onEffectsSettingsSegmentedTabSelected(podcast, PlaybackEffectsSettingsTab.entries[it])
},
modifier = Modifier
.padding(top = 24.dp),
)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import au.com.shiftyjelly.pocketcasts.preferences.model.ArtworkConfiguration
import au.com.shiftyjelly.pocketcasts.preferences.model.ShelfItem
import au.com.shiftyjelly.pocketcasts.repositories.bookmark.BookmarkManager
import au.com.shiftyjelly.pocketcasts.repositories.di.ApplicationScope
import au.com.shiftyjelly.pocketcasts.repositories.di.IoDispatcher
import au.com.shiftyjelly.pocketcasts.repositories.download.DownloadHelper
import au.com.shiftyjelly.pocketcasts.repositories.download.DownloadManager
import au.com.shiftyjelly.pocketcasts.repositories.playback.PlaybackManager
Expand Down Expand Up @@ -64,6 +65,7 @@ import kotlin.coroutines.CoroutineContext
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.DurationUnit
import kotlin.time.toDuration
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow
Expand Down Expand Up @@ -91,12 +93,13 @@ class PlayerViewModel @Inject constructor(
private val episodeAnalytics: EpisodeAnalytics,
@ApplicationContext private val context: Context,
@ApplicationScope private val applicationScope: CoroutineScope,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) : ViewModel(), CoroutineScope {

override val coroutineContext: CoroutineContext
get() = Dispatchers.Default

data class PodcastEffectsPair(val podcast: Podcast, val effects: PlaybackEffects)
data class PodcastEffectsData(val podcast: Podcast, val effects: PlaybackEffects, val showCustomEffectsSettings: Boolean = true)
data class PlayerHeader(
val positionMs: Int = 0,
val durationMs: Int = -1,
Expand Down Expand Up @@ -263,7 +266,7 @@ class PlayerViewModel @Inject constructor(

val upNextLive: LiveData<List<Any>> = upNextPlusData.toFlowable(BackpressureStrategy.LATEST).toLiveData()

val effectsObservable: Flowable<PodcastEffectsPair> = playbackStateObservable
val effectsObservable: Flowable<PodcastEffectsData> = playbackStateObservable
.toFlowable(BackpressureStrategy.LATEST)
.map { it.episodeUuid }
.switchMap { episodeManager.observeEpisodeByUuidRx(it) }
Expand All @@ -274,7 +277,14 @@ class PlayerViewModel @Inject constructor(
Flowable.just(Podcast.userPodcast.copy(overrideGlobalEffects = false))
}
}
.map { PodcastEffectsPair(it, if (it.overrideGlobalEffects) it.playbackEffects else settings.globalPlaybackEffects.value) }
.map { podcast ->
val isUserPodcast = podcast.uuid == Podcast.userPodcast.uuid
PodcastEffectsData(
podcast = podcast,
effects = if (podcast.overrideGlobalEffects) podcast.playbackEffects else settings.globalPlaybackEffects.value,
showCustomEffectsSettings = !isUserPodcast,
)
}
.doOnNext { Timber.i("Effects: Podcast: ${it.podcast.overrideGlobalEffects} ${it.effects}") }
.observeOn(AndroidSchedulers.mainThread())
val effectsLive = effectsObservable.toLiveData()
Expand Down Expand Up @@ -691,11 +701,11 @@ class PlayerViewModel @Inject constructor(
}
}

fun updatedOverrideGlobalEffects(podcast: Podcast, selectedTab: PlaybackEffectsSettingsTab) {
fun onEffectsSettingsSegmentedTabSelected(podcast: Podcast, selectedTab: PlaybackEffectsSettingsTab) {
val currentEpisode = playbackManager.getCurrentEpisode()
val isCurrentPodcast = currentEpisode?.podcastOrSubstituteUuid == podcast.uuid
if (!isCurrentPodcast) return
launch {
viewModelScope.launch(ioDispatcher) {
val override = selectedTab == PlaybackEffectsSettingsTab.ThisPodcast
podcastManager.updateOverrideGlobalEffects(podcast, override)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
package au.com.shiftyjelly.pocketcasts.player.viewmodel

import android.content.Context
import android.content.res.Resources
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.lifecycle.asFlow
import app.cash.turbine.test
import au.com.shiftyjelly.pocketcasts.analytics.AnalyticsTracker
import au.com.shiftyjelly.pocketcasts.analytics.EpisodeAnalytics
import au.com.shiftyjelly.pocketcasts.models.entity.BaseEpisode
import au.com.shiftyjelly.pocketcasts.models.entity.Podcast
import au.com.shiftyjelly.pocketcasts.models.entity.PodcastEpisode
import au.com.shiftyjelly.pocketcasts.models.entity.UserEpisode
import au.com.shiftyjelly.pocketcasts.models.to.PlaybackEffects
import au.com.shiftyjelly.pocketcasts.player.viewmodel.PlayerViewModel.PlaybackEffectsSettingsTab
import au.com.shiftyjelly.pocketcasts.preferences.Settings
import au.com.shiftyjelly.pocketcasts.preferences.UserSetting
import au.com.shiftyjelly.pocketcasts.preferences.model.ArtworkConfiguration
import au.com.shiftyjelly.pocketcasts.preferences.model.ShelfItem
import au.com.shiftyjelly.pocketcasts.repositories.bookmark.BookmarkManager
import au.com.shiftyjelly.pocketcasts.repositories.download.DownloadManager
import au.com.shiftyjelly.pocketcasts.repositories.playback.PlaybackManager
import au.com.shiftyjelly.pocketcasts.repositories.playback.PlaybackState
import au.com.shiftyjelly.pocketcasts.repositories.playback.SleepTimer
import au.com.shiftyjelly.pocketcasts.repositories.playback.UpNextQueue
import au.com.shiftyjelly.pocketcasts.repositories.playback.UpNextQueue.State
import au.com.shiftyjelly.pocketcasts.repositories.podcast.EpisodeManager
import au.com.shiftyjelly.pocketcasts.repositories.podcast.PodcastManager
import au.com.shiftyjelly.pocketcasts.repositories.podcast.UserEpisodeManager
import au.com.shiftyjelly.pocketcasts.sharedtest.MainCoroutineRule
import au.com.shiftyjelly.pocketcasts.ui.theme.Theme
import com.jakewharton.rxrelay2.BehaviorRelay
import io.reactivex.Flowable
import io.reactivex.Observable
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.Mockito.mock
import org.mockito.junit.MockitoJUnitRunner
import org.mockito.kotlin.anyOrNull
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever

@ExperimentalCoroutinesApi
@RunWith(MockitoJUnitRunner::class)
class PlayerViewModelTest {

@get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()

@get:Rule
val coroutineRule = MainCoroutineRule()

@Mock
private lateinit var episodeManager: EpisodeManager

@Mock
private lateinit var settings: Settings

@Mock
private lateinit var playbackManager: PlaybackManager

@Mock
private lateinit var bookmarkManager: BookmarkManager

@Mock
private lateinit var downloadManager: DownloadManager

@Mock
private lateinit var podcastManager: PodcastManager

@Mock
private lateinit var userEpisodeManager: UserEpisodeManager

@Mock
private lateinit var theme: Theme

@Mock
private lateinit var sleepTimer: SleepTimer

@Mock
private lateinit var analyticsTracker: AnalyticsTracker

@Mock
private lateinit var episodeAnalytics: EpisodeAnalytics

@Mock
private lateinit var context: Context

@Mock
private lateinit var applicationScope: CoroutineScope

@Mock
private lateinit var upNextQueue: UpNextQueue

@Mock
private lateinit var globalEffects: PlaybackEffects

@Mock
private lateinit var podcastPlaybackEffects: PlaybackEffects

@Mock
private lateinit var podcastEpisode: PodcastEpisode

@Mock
private lateinit var userEpisode: UserEpisode

@Mock
private lateinit var podcast: Podcast

@Mock
private lateinit var userSettingsGlobalEffects: UserSetting<PlaybackEffects>

private val podcastUuid = "podcastUuid"
private lateinit var viewModel: PlayerViewModel

@Test
fun `does not override global effects when all podcasts effects settings segmented tab selected`() {
initViewModel()

viewModel.onEffectsSettingsSegmentedTabSelected(podcast, PlaybackEffectsSettingsTab.AllPodcasts)

verify(podcastManager).updateOverrideGlobalEffects(podcast, false)
}

@Test
fun `override global effects when this podcasts effects settings segmented tab selected`() {
initViewModel()

viewModel.onEffectsSettingsSegmentedTabSelected(podcast, PlaybackEffectsSettingsTab.ThisPodcast)

verify(podcastManager).updateOverrideGlobalEffects(podcast, true)
}

@Test
fun `effects settings segmented tab bar shown for podcast episode`() = runTest {
initViewModel()

playbackManager.playbackStateRelay.accept(PlaybackState(podcast = podcast))

viewModel.effectsLive.asFlow().test {
assertTrue(awaitItem().showCustomEffectsSettings)
}
}

@Test
fun `effects settings segmented tab bar hidden for user episode`() = runTest {
initViewModel(currentEpisode = userEpisode)

playbackManager.playbackStateRelay.accept(PlaybackState(podcast = Podcast.userPodcast))

viewModel.effectsLive.asFlow().test {
assertFalse(awaitItem().showCustomEffectsSettings)
}
}

private fun initViewModel(
currentEpisode: BaseEpisode = podcastEpisode,
) {
whenever(playbackManager.playbackStateRelay).thenReturn(BehaviorRelay.create<PlaybackState>().toSerialized())
whenever(playbackManager.upNextQueue).thenReturn(upNextQueue)
whenever(upNextQueue.getChangesObservableWithLiveCurrentEpisode(episodeManager, podcastManager)).thenReturn(Observable.just(State.Empty))
val userSettingsIntMock = mock<UserSetting<Int>>()
whenever(userSettingsIntMock.flow).thenReturn(MutableStateFlow(0))
val userSettingsArtworkConfigurationMock = mock<UserSetting<ArtworkConfiguration>>()
whenever(userSettingsArtworkConfigurationMock.flow).thenReturn(MutableStateFlow(ArtworkConfiguration(false, emptySet())))
whenever(settings.skipBackInSecs).thenReturn(userSettingsIntMock)
whenever(settings.skipForwardInSecs).thenReturn(userSettingsIntMock)
whenever(settings.artworkConfiguration).thenReturn(userSettingsArtworkConfigurationMock)
val userSettingsShelfItemsMock = mock<UserSetting<List<ShelfItem>>>()
whenever(userSettingsShelfItemsMock.flow).thenReturn(MutableStateFlow(emptyList()))
whenever(settings.shelfItems).thenReturn(userSettingsShelfItemsMock)
val resourcesMock = mock<Resources>()
whenever(resourcesMock.getString(anyOrNull(), anyOrNull())).thenReturn("")
whenever(context.resources).thenReturn(resourcesMock)
whenever(podcast.uuid).thenReturn(podcastUuid)
whenever(userSettingsGlobalEffects.value).thenReturn(globalEffects)
whenever(settings.globalPlaybackEffects).thenReturn(userSettingsGlobalEffects)
whenever(podcastEpisode.podcastOrSubstituteUuid).thenReturn(podcastUuid)
whenever(playbackManager.getCurrentEpisode()).thenReturn(currentEpisode)
whenever(episodeManager.observeEpisodeByUuidRx(anyOrNull())).thenReturn(Flowable.just(currentEpisode))
whenever(podcast.playbackEffects).thenReturn(podcastPlaybackEffects)
whenever(podcastManager.observePodcastByUuid(anyOrNull())).thenReturn(Flowable.just(podcast))
val useRealTimeForPlaybackRemainingTimeMock = mock<UserSetting<Boolean>>()
whenever(useRealTimeForPlaybackRemainingTimeMock.flow).thenReturn(MutableStateFlow(false))
whenever(settings.useRealTimeForPlaybackRemaingTime).thenReturn(useRealTimeForPlaybackRemainingTimeMock)

viewModel = PlayerViewModel(
playbackManager = playbackManager,
episodeManager = episodeManager,
userEpisodeManager = userEpisodeManager,
podcastManager = podcastManager,
bookmarkManager = bookmarkManager,
downloadManager = downloadManager,
sleepTimer = sleepTimer,
settings = settings,
theme = theme,
analyticsTracker = analyticsTracker,
episodeAnalytics = episodeAnalytics,
context = context,
applicationScope = applicationScope,
ioDispatcher = UnconfinedTestDispatcher(),
)
}
}

0 comments on commit 7d8bc72

Please sign in to comment.