Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Project] Up Next Shuffle - Update Shuffle Button for Free users #3129

Merged
merged 2 commits into from
Oct 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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)
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
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
Expand All @@ -27,6 +28,7 @@
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
Expand Down Expand Up @@ -93,6 +95,7 @@
lateinit var adapter: UpNextAdapter
private val sourceView = SourceView.UP_NEXT
private val playerViewModel: PlayerViewModel by activityViewModels()
private val upNextViewModel: UpNextViewModel by viewModels<UpNextViewModel>()
private val swipeButtonLayoutViewModel: SwipeButtonLayoutViewModel by activityViewModels()
private var userRearrangingFrom: Int? = null
private var userDraggingStart: Int? = null
Expand Down Expand Up @@ -309,6 +312,13 @@
toolbar.menu.findItem(R.id.clear_up_next)?.isVisible = it.upNextEpisodes.isNotEmpty()
}

viewLifecycleOwner.lifecycleScope.launch {
upNextViewModel.isSignedInAsPaidUser.collect { isSignedInAsPaidUser ->
adapter.updateUserSignInState(isSignedInAsPaidUser)
adapter.notifyDataSetChanged()
Dismissed Show dismissed Hide dismissed
}
}

view.isClickable = true

val callback = UpNextTouchCallback(adapter = this)
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Boolean> get() = _isSignedInAsPaidUser

init {
viewModelScope.launch(ioDispatcher) {
userManager.getSignInState().asFlow().collect { signInState ->
_isSignedInAsPaidUser.value = signInState.isSignedInAsPlusOrPatron
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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<UserManager>()

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())
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M2.961,16.523C2.961,16.07 3.313,15.742 3.797,15.742H5.578C6.445,15.742 7.008,15.438 7.68,14.641L9.578,12.383L7.68,10.125C7.008,9.328 6.398,9.023 5.523,9.023H3.797C3.313,9.023 2.961,8.695 2.961,8.242C2.961,7.789 3.313,7.469 3.797,7.469H5.586C6.922,7.469 7.766,7.859 8.688,8.945L10.578,11.195L12.398,9.031C13.391,7.844 14.242,7.461 15.633,7.461H17.188V5.617C17.188,5.234 17.414,5.023 17.789,5.023C17.961,5.023 18.117,5.078 18.25,5.188L21.313,7.742C21.609,8 21.609,8.375 21.313,8.617L18.25,11.172C18.117,11.273 17.961,11.336 17.789,11.336C17.414,11.336 17.188,11.117 17.188,10.742V9.023H15.625C14.719,9.023 14.172,9.313 13.492,10.117L11.578,12.383L13.484,14.641C14.148,15.438 14.75,15.742 15.672,15.742H17.188V14.055C17.188,13.672 17.414,13.461 17.789,13.461C17.961,13.461 18.117,13.516 18.25,13.625L21.313,16.18C21.609,16.438 21.609,16.813 21.313,17.055L18.25,19.609C18.117,19.711 17.961,19.773 17.789,19.773C17.414,19.773 17.188,19.555 17.188,19.18V17.297H15.625C14.281,17.297 13.391,16.906 12.445,15.781L10.578,13.57L8.688,15.82C7.766,16.906 6.977,17.297 5.633,17.297H3.797C3.313,17.297 2.961,16.977 2.961,16.523Z">
Dismissed Show dismissed Hide dismissed
<aapt:attr name="android:fillColor">
<gradient
android:startX="4.62"
android:startY="5.023"
android:endX="21.347"
android:endY="6.846"
android:type="linear">
<item android:offset="0" android:color="#FFFED745"/>
<item android:offset="1" android:color="#FFFEB525"/>
</gradient>
</aapt:attr>
</path>
</vector>
Loading