diff --git a/modules/features/player/src/main/java/au/com/shiftyjelly/pocketcasts/player/view/UpNextAdapter.kt b/modules/features/player/src/main/java/au/com/shiftyjelly/pocketcasts/player/view/UpNextAdapter.kt index 1895c98e53..738c6a2a39 100644 --- a/modules/features/player/src/main/java/au/com/shiftyjelly/pocketcasts/player/view/UpNextAdapter.kt +++ b/modules/features/player/src/main/java/au/com/shiftyjelly/pocketcasts/player/view/UpNextAdapter.kt @@ -81,6 +81,8 @@ class UpNextAdapter( notifyDataSetChanged() } + private var isSignedInAsPaidUser: Boolean = false + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { val inflater = LayoutInflater.from(parent.context) return when (viewType) { @@ -100,8 +102,7 @@ class UpNextAdapter( } override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - val item = getItem(position) - when (item) { + when (val item = getItem(position)) { is BaseEpisode -> bindEpisodeRow(holder as UpNextEpisodeViewHolder, item) is PlayerViewModel.UpNextSummary -> (holder as HeaderViewHolder).bind(item) is UpNextPlaying -> (holder as PlayingViewHolder).bind(item) @@ -158,6 +159,10 @@ class UpNextAdapter( (holder as? UpNextEpisodeViewHolder)?.clearDisposable() } + fun updateUserSignInState(isSignedInAsPaidUser: Boolean) { + this.isSignedInAsPaidUser = isSignedInAsPaidUser + } + inner class HeaderViewHolder(val binding: AdapterUpNextFooterBinding) : RecyclerView.ViewHolder(binding.root) { fun bind(header: PlayerViewModel.UpNextSummary) { @@ -190,19 +195,29 @@ class UpNextAdapter( private fun ImageButton.updateShuffleButton() { if (!FeatureFlag.isEnabled(Feature.UP_NEXT_SHUFFLE)) return - val isEnabled = settings.upNextShuffle.value - - this.setImageResource(if (isEnabled) IR.drawable.shuffle_enabled else IR.drawable.shuffle) + this.setImageResource( + when { + !isSignedInAsPaidUser -> IR.drawable.shuffle_plus_feature_icon + settings.upNextShuffle.value -> IR.drawable.shuffle_enabled + else -> IR.drawable.shuffle + }, + ) this.contentDescription = context.getString( - if (isEnabled) LR.string.up_next_shuffle_disable_button_content_description else LR.string.up_next_shuffle_button_content_description, + when { + isSignedInAsPaidUser -> LR.string.up_next_shuffle_button_content_description + settings.upNextShuffle.value -> LR.string.up_next_shuffle_disable_button_content_description + else -> LR.string.up_next_shuffle_button_content_description + }, ) - this.setImageTintList( - ColorStateList.valueOf( - if (isEnabled) ThemeColor.primaryIcon01(theme) else ThemeColor.primaryIcon02(theme), - ), - ) + if (isSignedInAsPaidUser) { + this.setImageTintList( + ColorStateList.valueOf( + if (settings.upNextShuffle.value) ThemeColor.primaryIcon01(theme) else ThemeColor.primaryIcon02(theme), + ), + ) + } TooltipCompat.setTooltipText( this, diff --git a/modules/features/player/src/main/java/au/com/shiftyjelly/pocketcasts/player/view/UpNextFragment.kt b/modules/features/player/src/main/java/au/com/shiftyjelly/pocketcasts/player/view/UpNextFragment.kt index 6340983fdc..a0525f4401 100644 --- a/modules/features/player/src/main/java/au/com/shiftyjelly/pocketcasts/player/view/UpNextFragment.kt +++ b/modules/features/player/src/main/java/au/com/shiftyjelly/pocketcasts/player/view/UpNextFragment.kt @@ -12,6 +12,7 @@ import androidx.core.os.bundleOf import androidx.core.view.isVisible import androidx.core.view.updatePadding import androidx.fragment.app.activityViewModels +import androidx.fragment.app.viewModels import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle @@ -27,6 +28,7 @@ import au.com.shiftyjelly.pocketcasts.models.type.EpisodeViewSource import au.com.shiftyjelly.pocketcasts.player.R import au.com.shiftyjelly.pocketcasts.player.databinding.FragmentUpnextBinding import au.com.shiftyjelly.pocketcasts.player.viewmodel.PlayerViewModel +import au.com.shiftyjelly.pocketcasts.player.viewmodel.UpNextViewModel import au.com.shiftyjelly.pocketcasts.preferences.Settings import au.com.shiftyjelly.pocketcasts.repositories.playback.PlaybackManager import au.com.shiftyjelly.pocketcasts.repositories.playback.UpNextSource @@ -93,6 +95,7 @@ class UpNextFragment : BaseFragment(), UpNextListener, UpNextTouchCallback.ItemT lateinit var adapter: UpNextAdapter private val sourceView = SourceView.UP_NEXT private val playerViewModel: PlayerViewModel by activityViewModels() + private val upNextViewModel: UpNextViewModel by viewModels() private val swipeButtonLayoutViewModel: SwipeButtonLayoutViewModel by activityViewModels() private var userRearrangingFrom: Int? = null private var userDraggingStart: Int? = null @@ -309,6 +312,13 @@ class UpNextFragment : BaseFragment(), UpNextListener, UpNextTouchCallback.ItemT toolbar.menu.findItem(R.id.clear_up_next)?.isVisible = it.upNextEpisodes.isNotEmpty() } + viewLifecycleOwner.lifecycleScope.launch { + upNextViewModel.isSignedInAsPaidUser.collect { isSignedInAsPaidUser -> + adapter.updateUserSignInState(isSignedInAsPaidUser) + adapter.notifyDataSetChanged() + } + } + view.isClickable = true val callback = UpNextTouchCallback(adapter = this) diff --git a/modules/features/player/src/main/java/au/com/shiftyjelly/pocketcasts/player/viewmodel/UpNextViewModel.kt b/modules/features/player/src/main/java/au/com/shiftyjelly/pocketcasts/player/viewmodel/UpNextViewModel.kt new file mode 100644 index 0000000000..6f86d9dd23 --- /dev/null +++ b/modules/features/player/src/main/java/au/com/shiftyjelly/pocketcasts/player/viewmodel/UpNextViewModel.kt @@ -0,0 +1,31 @@ +package au.com.shiftyjelly.pocketcasts.player.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import au.com.shiftyjelly.pocketcasts.repositories.di.IoDispatcher +import au.com.shiftyjelly.pocketcasts.repositories.user.UserManager +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.reactive.asFlow + +@HiltViewModel +class UpNextViewModel @Inject constructor( + val userManager: UserManager, + @IoDispatcher private val ioDispatcher: CoroutineDispatcher, +) : ViewModel() { + + private val _isSignedInAsPaidUser = MutableStateFlow(false) + val isSignedInAsPaidUser: StateFlow get() = _isSignedInAsPaidUser + + init { + viewModelScope.launch(ioDispatcher) { + userManager.getSignInState().asFlow().collect { signInState -> + _isSignedInAsPaidUser.value = signInState.isSignedInAsPlusOrPatron + } + } + } +} diff --git a/modules/features/player/src/test/java/au/com/shiftyjelly/pocketcasts/player/viewmodel/UpNextViewModelTest.kt b/modules/features/player/src/test/java/au/com/shiftyjelly/pocketcasts/player/viewmodel/UpNextViewModelTest.kt new file mode 100644 index 0000000000..867506bb70 --- /dev/null +++ b/modules/features/player/src/test/java/au/com/shiftyjelly/pocketcasts/player/viewmodel/UpNextViewModelTest.kt @@ -0,0 +1,67 @@ +package au.com.shiftyjelly.pocketcasts.player.viewmodel + +import au.com.shiftyjelly.pocketcasts.models.to.SignInState +import au.com.shiftyjelly.pocketcasts.models.to.SubscriptionStatus +import au.com.shiftyjelly.pocketcasts.models.type.SubscriptionFrequency +import au.com.shiftyjelly.pocketcasts.models.type.SubscriptionPlatform +import au.com.shiftyjelly.pocketcasts.models.type.SubscriptionTier +import au.com.shiftyjelly.pocketcasts.models.type.SubscriptionType +import au.com.shiftyjelly.pocketcasts.repositories.user.UserManager +import io.reactivex.Flowable +import java.util.Date +import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + +class UpNextViewModelTest { + + @Test + fun `initial state isSignedInAsPaidUser should be true for paid user`() = runTest { + val viewModel = initViewModel(isPaidUser = true) + + assertEquals(true, viewModel.isSignedInAsPaidUser.value) + } + + @Test + fun `initial state isSignedInAsPaidUser should be false for free user`() = runTest { + val viewModel = initViewModel(isPaidUser = false) + + assertEquals(false, viewModel.isSignedInAsPaidUser.value) + } + + @OptIn(ExperimentalCoroutinesApi::class) + private fun initViewModel( + isPaidUser: Boolean = false, + ): UpNextViewModel { + val userManager = mock() + + whenever(userManager.getSignInState()) + .thenReturn( + Flowable.just( + SignInState.SignedIn( + email = "", + subscriptionStatus = if (isPaidUser) { + SubscriptionStatus.Paid( + expiry = Date(), + autoRenew = true, + giftDays = 0, + frequency = SubscriptionFrequency.MONTHLY, + platform = SubscriptionPlatform.ANDROID, + subscriptionList = emptyList(), + type = SubscriptionType.PLUS, + tier = SubscriptionTier.PLUS, + index = 0, + ) + } else { + SubscriptionStatus.Free() + }, + ), + ), + ) + return UpNextViewModel(userManager, UnconfinedTestDispatcher()) + } +} diff --git a/modules/services/images/src/main/res/drawable/shuffle_plus_feature_icon.xml b/modules/services/images/src/main/res/drawable/shuffle_plus_feature_icon.xml new file mode 100644 index 0000000000..d075a4ab91 --- /dev/null +++ b/modules/services/images/src/main/res/drawable/shuffle_plus_feature_icon.xml @@ -0,0 +1,21 @@ + + + + + + + + + +