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

Custom Playback Settings - Analytics #3136

Merged
merged 4 commits into from
Nov 1, 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 @@ -23,7 +23,6 @@ import androidx.fragment.app.activityViewModels
import androidx.lifecycle.asFlow
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import au.com.shiftyjelly.pocketcasts.analytics.AnalyticsEvent
import au.com.shiftyjelly.pocketcasts.analytics.SourceView
import au.com.shiftyjelly.pocketcasts.compose.components.SegmentedTabBar
import au.com.shiftyjelly.pocketcasts.compose.components.SegmentedTabBarDefaults
import au.com.shiftyjelly.pocketcasts.compose.theme
Expand All @@ -43,6 +42,7 @@ import au.com.shiftyjelly.pocketcasts.repositories.user.StatsManager
import au.com.shiftyjelly.pocketcasts.ui.extensions.themed
import au.com.shiftyjelly.pocketcasts.ui.helper.StatusBarColor
import au.com.shiftyjelly.pocketcasts.ui.theme.ThemeColor
import au.com.shiftyjelly.pocketcasts.utils.Debouncer
import au.com.shiftyjelly.pocketcasts.utils.extensions.dpToPx
import au.com.shiftyjelly.pocketcasts.utils.extensions.roundedSpeed
import au.com.shiftyjelly.pocketcasts.utils.featureflag.Feature
Expand All @@ -54,6 +54,7 @@ import com.google.android.material.button.MaterialButtonToggleGroup
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch
import au.com.shiftyjelly.pocketcasts.localization.R as LR

@AndroidEntryPoint
Expand All @@ -71,14 +72,24 @@ class EffectsFragment : BaseDialogFragment(), CompoundButton.OnCheckedChangeList
private lateinit var imageRequestFactory: PocketCastsImageRequestFactory
private var binding: FragmentEffectsBinding? = null
private val trimToggleGroupButtonIds = arrayOf(R.id.trimLow, R.id.trimMedium, R.id.trimHigh)
private var updatedSpeed: Double? = null
private var playbackSpeedTrackingDebouncer: Debouncer = Debouncer()

override fun onAttach(context: Context) {
super.onAttach(context)

imageRequestFactory = PocketCastsImageRequestFactory(context, cornerRadius = 4).themed()
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

if (savedInstanceState == null) {
if (FeatureFlag.isEnabled(Feature.CUSTOM_PLAYBACK_SETTINGS)) {
viewModel.trackPlaybackEffectsEvent(AnalyticsEvent.PLAYBACK_EFFECT_SETTINGS_VIEW_APPEARED)
}
}
}

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding = FragmentEffectsBinding.inflate(inflater, container, false)

Expand All @@ -103,7 +114,6 @@ class EffectsFragment : BaseDialogFragment(), CompoundButton.OnCheckedChangeList

override fun onDestroyView() {
super.onDestroyView()
trackSpeedChangeIfNeeded()
binding = null
}

Expand Down Expand Up @@ -179,11 +189,20 @@ class EffectsFragment : BaseDialogFragment(), CompoundButton.OnCheckedChangeList

val speed = amount.roundedSpeed()
effects.playbackSpeed = speed
updatedSpeed = speed
binding.lblSpeed.text = String.format("%.1fx", effects.playbackSpeed)
viewModel.saveEffects(effects, podcast)

binding.btnSpeedUp.announceForAccessibility("Playback speed ${binding.lblSpeed.text}")
launch {
playbackSpeedTrackingDebouncer.debounce {
viewModel.effectsLive.value?.effects?.playbackSpeed?.roundedSpeed()?.let { currentSpeed ->
trackPlaybackEffectsEvent(
event = AnalyticsEvent.PLAYBACK_EFFECT_SPEED_CHANGED,
props = mapOf(PlaybackManager.SPEED_KEY to currentSpeed),
)
}
}
}
}

private fun updateTrimState() {
Expand Down Expand Up @@ -263,18 +282,19 @@ class EffectsFragment : BaseDialogFragment(), CompoundButton.OnCheckedChangeList
}
}

private fun trackSpeedChangeIfNeeded() {
updatedSpeed?.let { trackPlaybackEffectsEvent(AnalyticsEvent.PLAYBACK_EFFECT_SPEED_CHANGED, mapOf(PlaybackManager.SPEED_KEY to it)) }
}

private fun trackPlaybackEffectsEvent(event: AnalyticsEvent, props: Map<String, Any> = emptyMap()) {
playbackManager.trackPlaybackEffectsEvent(event, props, SourceView.PLAYER_PLAYBACK_EFFECTS)
viewModel.trackPlaybackEffectsEvent(event, props)
}

private fun FragmentEffectsBinding.setupEffectsSettingsSegmentedTabBar() {
effectsSettingsSegmentedTabBar.setContent {
val podcastEffectsData by viewModel.effectsLive.asFlow()
.distinctUntilChanged { t1, t2 -> t1.podcast.uuid == t2.podcast.uuid && t1.podcast.playbackEffects.toData() == t2.podcast.playbackEffects.toData() }
.distinctUntilChanged { t1, t2 ->
t1.podcast.uuid == t2.podcast.uuid &&
t1.podcast.playbackEffects.toData() == t2.podcast.playbackEffects.toData() &&
t1.podcast.overrideGlobalEffects == t2.podcast.overrideGlobalEffects &&
t1.podcast.overrideGlobalEffectsModified == t2.podcast.overrideGlobalEffectsModified
}
.collectAsStateWithLifecycle(null)
val podcast = podcastEffectsData?.podcast ?: return@setContent

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -713,6 +713,7 @@ class PlayerViewModel @Inject constructor(
podcast.overrideGlobalEffects = override
saveEffects(effects, podcast)
}
trackPlaybackEffectsEvent(AnalyticsEvent.PLAYBACK_EFFECT_SETTINGS_CHANGED)
}

fun clearPodcastEffects(podcast: Podcast) {
Expand Down Expand Up @@ -762,19 +763,41 @@ class PlayerViewModel @Inject constructor(
}
}

fun trackPlaybackEffectsEvent(
event: AnalyticsEvent,
properties: Map<String, Any> = emptyMap(),
) {
playbackManager.trackPlaybackEffectsEvent(
event = event,
props = buildMap {
putAll(properties)
if (FeatureFlag.isEnabled(Feature.CUSTOM_PLAYBACK_SETTINGS)) {
val settings = if (effectsLive.value?.podcast?.overrideGlobalEffects == true) {
PlaybackEffectsSettingsTab.ThisPodcast.analyticsValue
} else {
PlaybackEffectsSettingsTab.AllPodcasts.analyticsValue
}
put(AnalyticsProp.SETTINGS, settings)
}
},
sourceView = SourceView.PLAYER_PLAYBACK_EFFECTS,
)
}

sealed class TransitionState {
data object OpenTranscript : TransitionState()
data class CloseTranscript(val withTransition: Boolean) : TransitionState()
}

private object AnalyticsProp {
private const val episodeUuid = "episode_uuid"
private const val podcastUuid = "podcast_uuid"
fun transcriptDismissed(episodeId: String, podcastId: String) = mapOf(episodeId to episodeUuid, podcastId to podcastUuid)
private const val EPISODE_UUID = "episode_uuid"
private const val PODCAST_UUID = "podcast_uuid"
const val SETTINGS = "settings"
fun transcriptDismissed(episodeId: String, podcastId: String) = mapOf(episodeId to EPISODE_UUID, podcastId to PODCAST_UUID)
}

enum class PlaybackEffectsSettingsTab(@StringRes val labelResId: Int) {
AllPodcasts(LR.string.podcasts_all),
ThisPodcast(LR.string.podcast_this),
enum class PlaybackEffectsSettingsTab(@StringRes val labelResId: Int, val analyticsValue: String) {
AllPodcasts(LR.string.podcasts_all, "global"),
ThisPodcast(LR.string.podcast_this, "local"),
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -164,9 +164,4 @@ class PodcastEffectsFragment : PreferenceFragmentCompat() {
preferenceTrimSilence?.icon = context.getTintedDrawable(R.drawable.ic_silence, tintColor)
preferenceBoostVolume?.icon = context.getTintedDrawable(R.drawable.ic_volumeboost, tintColor)
}

override fun onDestroyView() {
super.onDestroyView()
viewModel.trackSpeedChangeIfNeeded()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package au.com.shiftyjelly.pocketcasts.podcasts.viewmodel
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.toLiveData
import androidx.lifecycle.viewModelScope
import au.com.shiftyjelly.pocketcasts.analytics.AnalyticsEvent
import au.com.shiftyjelly.pocketcasts.analytics.SourceView
import au.com.shiftyjelly.pocketcasts.models.entity.Podcast
Expand All @@ -11,7 +12,10 @@ import au.com.shiftyjelly.pocketcasts.models.type.TrimMode
import au.com.shiftyjelly.pocketcasts.preferences.Settings
import au.com.shiftyjelly.pocketcasts.repositories.playback.PlaybackManager
import au.com.shiftyjelly.pocketcasts.repositories.podcast.PodcastManager
import au.com.shiftyjelly.pocketcasts.utils.Debouncer
import au.com.shiftyjelly.pocketcasts.utils.extensions.roundedSpeed
import au.com.shiftyjelly.pocketcasts.utils.featureflag.Feature
import au.com.shiftyjelly.pocketcasts.utils.featureflag.FeatureFlag
import dagger.hilt.android.lifecycle.HiltViewModel
import io.reactivex.schedulers.Schedulers
import javax.inject.Inject
Expand All @@ -31,8 +35,8 @@ class PodcastEffectsViewModel
override val coroutineContext: CoroutineContext
get() = Dispatchers.Default

private var updatedSpeed: Double? = null
lateinit var podcast: LiveData<Podcast>
private val playbackSpeedTrackingDebouncer = Debouncer()

fun loadPodcast(uuid: String) {
podcast = podcastManager
Expand Down Expand Up @@ -90,10 +94,14 @@ class PodcastEffectsViewModel
val podcast = this.podcast.value ?: return
val roundedSpeed = speed.roundedSpeed()
podcastManager.updatePlaybackSpeed(podcast, roundedSpeed)
updatedSpeed = roundedSpeed
if (shouldUpdatePlaybackManager()) {
playbackManager.updatePlayerEffects(podcast.playbackEffects)
}
viewModelScope.launch {
playbackSpeedTrackingDebouncer.debounce {
trackPlaybackEffectsEvent(AnalyticsEvent.PLAYBACK_EFFECT_SPEED_CHANGED, mapOf(PlaybackManager.SPEED_KEY to roundedSpeed))
}
}
}

private fun shouldUpdatePlaybackManager(): Boolean {
Expand All @@ -103,11 +111,16 @@ class PodcastEffectsViewModel
return podcastUuid == podcast.uuid
}

fun trackSpeedChangeIfNeeded() {
updatedSpeed?.let { trackPlaybackEffectsEvent(AnalyticsEvent.PLAYBACK_EFFECT_SPEED_CHANGED, mapOf(PlaybackManager.SPEED_KEY to it)) }
}

fun trackPlaybackEffectsEvent(event: AnalyticsEvent, props: Map<String, Any> = emptyMap()) {
playbackManager.trackPlaybackEffectsEvent(event, props, SourceView.PODCAST_SETTINGS)
playbackManager.trackPlaybackEffectsEvent(
event = event,
props = buildMap {
putAll(props)
if (FeatureFlag.isEnabled(Feature.CUSTOM_PLAYBACK_SETTINGS)) {
put("settings", "local")
}
},
sourceView = SourceView.PODCAST_SETTINGS,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,8 @@ enum class AnalyticsEvent(val key: String) {
PLAYER_SLEEP_TIMER_SETTINGS_TAPPED("player_sleep_timer_settings_tapped"),

/* Player - Playback effects */
PLAYBACK_EFFECT_SETTINGS_VIEW_APPEARED("playback_effect_settings_view_appeared"),
PLAYBACK_EFFECT_SETTINGS_CHANGED("playback_effect_settings_changed"),
PLAYBACK_EFFECT_SPEED_CHANGED("playback_effect_speed_changed"),
PLAYBACK_EFFECT_TRIM_SILENCE_TOGGLED("playback_effect_trim_silence_toggled"),
PLAYBACK_EFFECT_TRIM_SILENCE_AMOUNT_CHANGED("playback_effect_trim_silence_amount_changed"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2547,10 +2547,15 @@ open class PlaybackManager @Inject constructor(
props: Map<String, Any> = emptyMap(),
sourceView: SourceView,
) {
val properties = HashMap<String, Any>()
properties[SOURCE_KEY] = sourceView.analyticsValue
properties.putAll(props)
analyticsTracker.track(event, properties)
val contentType = if (getCurrentEpisode()?.isVideo == true) ContentType.VIDEO else ContentType.AUDIO
analyticsTracker.track(
event = event,
properties = buildMap {
put(SOURCE_KEY, sourceView.analyticsValue)
put(CONTENT_TYPE_KEY, contentType.analyticsValue)
putAll(props)
},
)
}

fun setNotificationPermissionChecker(notificationPermissionChecker: NotificationPermissionChecker) {
Expand Down Expand Up @@ -2587,7 +2592,7 @@ open class PlaybackManager @Inject constructor(
this["chapterUuid"] = value
}

private enum class ContentType(val analyticsValue: String) {
enum class ContentType(val analyticsValue: String) {
AUDIO("audio"),
VIDEO("video"),
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package au.com.shiftyjelly.pocketcasts.utils

import kotlin.time.Duration
import kotlin.time.DurationUnit
import kotlin.time.toDuration
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.launch

@OptIn(FlowPreview::class)
class Debouncer(
coroutineScope: CoroutineScope = CoroutineScope(Dispatchers.Default),
private val waitDuration: Duration = (1).toDuration(DurationUnit.SECONDS),
) {
class Work(val work: suspend () -> Unit)
private val stateFlow = MutableStateFlow<Work?>(null)
init {
coroutineScope.launch {
stateFlow.debounce(waitDuration).collect {
it?.let { it.work() }
}
}
}

suspend fun debounce(work: suspend () -> Unit) {
stateFlow.emit(Work(work))
}
}