Skip to content

Commit 846061a

Browse files
authored
fix: prevent seek flicker by reworking lazy seek (#305)
* fix: prevent seek flicker by removing lazy seeking * docs: update changelog * fix: reduce debounce interval * fix: prevent ANRs when seeking quickly
1 parent 9e87966 commit 846061a

File tree

4 files changed

+115
-104
lines changed

4 files changed

+115
-104
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77
## [Unreleased]
88
### Fixed
99
- Fixed another crash when clearing app from recents ([#298])
10+
- Fixed flicker when seeking on the player screen
1011

1112
## [1.5.1] - 2025-11-05
1213
### Fixed

app/src/main/kotlin/org/fossify/musicplayer/activities/TrackActivity.kt

Lines changed: 108 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ import androidx.media3.common.MediaItem
2020
import com.bumptech.glide.load.resource.bitmap.CenterCrop
2121
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
2222
import com.bumptech.glide.request.RequestOptions
23+
import kotlinx.coroutines.CoroutineScope
24+
import kotlinx.coroutines.Dispatchers
25+
import kotlinx.coroutines.Job
26+
import kotlinx.coroutines.delay
27+
import kotlinx.coroutines.launch
2328
import org.fossify.commons.extensions.applyColorFilter
2429
import org.fossify.commons.extensions.beGone
2530
import org.fossify.commons.extensions.beInvisibleIf
@@ -36,7 +41,6 @@ import org.fossify.commons.extensions.updateTextColors
3641
import org.fossify.commons.extensions.value
3742
import org.fossify.commons.extensions.viewBinding
3843
import org.fossify.commons.helpers.MEDIUM_ALPHA
39-
import org.fossify.commons.helpers.mydebug
4044
import org.fossify.musicplayer.R
4145
import org.fossify.musicplayer.databinding.ActivityTrackBinding
4246
import org.fossify.musicplayer.extensions.config
@@ -49,6 +53,7 @@ import org.fossify.musicplayer.extensions.loadGlideResource
4953
import org.fossify.musicplayer.extensions.nextMediaItem
5054
import org.fossify.musicplayer.extensions.sendCommand
5155
import org.fossify.musicplayer.extensions.setRepeatMode
56+
import org.fossify.musicplayer.extensions.shuffledMediaItemsIndices
5257
import org.fossify.musicplayer.extensions.toTrack
5358
import org.fossify.musicplayer.extensions.updatePlayPauseIcon
5459
import org.fossify.musicplayer.fragments.PlaybackSpeedFragment
@@ -63,21 +68,29 @@ import kotlin.math.min
6368
import kotlin.time.Duration.Companion.milliseconds
6469

6570
class TrackActivity : SimpleControllerActivity(), PlaybackSpeedListener {
66-
private val SWIPE_DOWN_THRESHOLD = 100
71+
companion object {
72+
private const val SWIPE_DOWN_THRESHOLD = 100
73+
private const val SEEK_COALESCE_INTERVAL_MS = 200L
74+
private const val UPDATE_INTERVAL_MS = 150L
75+
}
6776

6877
private var isThirdPartyIntent = false
6978
private lateinit var nextTrackPlaceholder: Drawable
7079

7180
private val handler = Handler(Looper.getMainLooper())
72-
private val updateIntervalMillis = 500L
81+
82+
private val scope = CoroutineScope(Dispatchers.Default)
83+
private var seekJob: Job? = null
84+
private var seekCount = 0
7385

7486
private val binding by viewBinding(ActivityTrackBinding::inflate)
7587

7688
override fun onCreate(savedInstanceState: Bundle?) {
7789
super.onCreate(savedInstanceState)
7890
setContentView(binding.root)
7991
setupEdgeToEdge(padBottomSystem = listOf(binding.nextTrackHolder))
80-
nextTrackPlaceholder = resources.getColoredDrawableWithColor(R.drawable.ic_headset, getProperTextColor())
92+
nextTrackPlaceholder =
93+
resources.getColoredDrawableWithColor(R.drawable.ic_headset, getProperTextColor())
8194
setupButtons()
8295
setupFlingListener()
8396

@@ -87,7 +100,12 @@ class TrackActivity : SimpleControllerActivity(), PlaybackSpeedListener {
87100
}
88101

89102
isThirdPartyIntent = intent.action == Intent.ACTION_VIEW
90-
arrayOf(activityTrackToggleShuffle, activityTrackPrevious, activityTrackNext, activityTrackPlaybackSetting).forEach {
103+
arrayOf(
104+
activityTrackToggleShuffle,
105+
activityTrackPrevious,
106+
activityTrackNext,
107+
activityTrackPlaybackSetting
108+
).forEach {
91109
it.beInvisibleIf(isThirdPartyIntent)
92110
}
93111

@@ -98,7 +116,10 @@ class TrackActivity : SimpleControllerActivity(), PlaybackSpeedListener {
98116

99117
setupTrackInfo(PlaybackService.currentMediaItem)
100118
setupNextTrackInfo(PlaybackService.nextMediaItem)
101-
activityTrackPlayPause.updatePlayPauseIcon(PlaybackService.isPlaying, getProperTextColor())
119+
activityTrackPlayPause.updatePlayPauseIcon(
120+
isPlaying = PlaybackService.isPlaying,
121+
color = getProperTextColor()
122+
)
102123
updatePlayerState()
103124

104125
nextTrackHolder.background = getProperBackgroundColor().toDrawable()
@@ -177,9 +198,9 @@ class TrackActivity : SimpleControllerActivity(), PlaybackSpeedListener {
177198

178199
private fun setupButtons() = binding.apply {
179200
activityTrackToggleShuffle.setOnClickListener { withPlayer { toggleShuffle() } }
180-
activityTrackPrevious.setOnClickListener { withPlayer { seekToPrevious() } }
201+
activityTrackPrevious.setOnClickListener { seekWithDelay(previous = true) }
181202
activityTrackPlayPause.setOnClickListener { togglePlayback() }
182-
activityTrackNext.setOnClickListener { withPlayer { seekToNext() } }
203+
activityTrackNext.setOnClickListener { seekWithDelay() }
183204
activityTrackProgressCurrent.setOnClickListener { seekBack() }
184205
activityTrackProgressMax.setOnClickListener { seekForward() }
185206
activityTrackPlaybackSetting.setOnClickListener { togglePlaybackSetting() }
@@ -201,17 +222,20 @@ class TrackActivity : SimpleControllerActivity(), PlaybackSpeedListener {
201222
}
202223

203224
binding.nextTrackHolder.beVisible()
204-
val artist = if (track.artist.trim().isNotEmpty() && track.artist != MediaStore.UNKNOWN_STRING) {
205-
"${track.artist}"
206-
} else {
207-
""
208-
}
225+
val artist =
226+
if (track.artist.trim().isNotEmpty() && track.artist != MediaStore.UNKNOWN_STRING) {
227+
"${track.artist}"
228+
} else {
229+
""
230+
}
209231

210232
@SuppressLint("SetTextI18n")
211233
binding.nextTrackLabel.text = "${getString(R.string.next_track)} ${track.title}$artist"
212234

213235
getTrackCoverArt(track) { coverArt ->
214-
val cornerRadius = resources.getDimension(org.fossify.commons.R.dimen.rounded_corner_radius_small).toInt()
236+
val cornerRadius =
237+
resources.getDimension(org.fossify.commons.R.dimen.rounded_corner_radius_small)
238+
.toInt()
215239
val wantedSize = resources.getDimension(R.dimen.song_image_size).toInt()
216240

217241
// change cover image manually only once loaded successfully to avoid blinking at fails and placeholders
@@ -271,7 +295,12 @@ class TrackActivity : SimpleControllerActivity(), PlaybackSpeedListener {
271295
@SuppressLint("ClickableViewAccessibility")
272296
private fun setupFlingListener() {
273297
val flingListener = object : GestureDetector.SimpleOnGestureListener() {
274-
override fun onFling(e1: MotionEvent?, e2: MotionEvent, velocityX: Float, velocityY: Float): Boolean {
298+
override fun onFling(
299+
e1: MotionEvent?,
300+
e2: MotionEvent,
301+
velocityX: Float,
302+
velocityY: Float
303+
): Boolean {
275304
if (e1 != null) {
276305
if (velocityY > 0 && velocityY > velocityX && e2.y - e1.y > SWIPE_DOWN_THRESHOLD) {
277306
finish()
@@ -305,7 +334,8 @@ class TrackActivity : SimpleControllerActivity(), PlaybackSpeedListener {
305334
binding.activityTrackToggleShuffle.apply {
306335
applyColorFilter(if (isShuffleEnabled) getProperPrimaryColor() else getProperTextColor())
307336
alpha = if (isShuffleEnabled) 1f else MEDIUM_ALPHA
308-
contentDescription = getString(if (isShuffleEnabled) R.string.disable_shuffle else R.string.enable_shuffle)
337+
contentDescription =
338+
getString(if (isShuffleEnabled) R.string.disable_shuffle else R.string.enable_shuffle)
309339
}
310340
}
311341

@@ -351,18 +381,19 @@ class TrackActivity : SimpleControllerActivity(), PlaybackSpeedListener {
351381
binding.activityTrackSpeedIcon.applyColorFilter(getProperTextColor())
352382
updatePlaybackSpeed(config.playbackSpeed)
353383

354-
binding.activityTrackProgressbar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
355-
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
356-
val formattedProgress = progress.getFormattedDuration()
357-
binding.activityTrackProgressCurrent.text = formattedProgress
358-
}
384+
binding.activityTrackProgressbar.setOnSeekBarChangeListener(
385+
object : SeekBar.OnSeekBarChangeListener {
386+
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
387+
val formattedProgress = progress.getFormattedDuration()
388+
binding.activityTrackProgressCurrent.text = formattedProgress
389+
}
359390

360-
override fun onStartTrackingTouch(seekBar: SeekBar) {}
391+
override fun onStartTrackingTouch(seekBar: SeekBar) {}
361392

362-
override fun onStopTrackingTouch(seekBar: SeekBar) = withPlayer {
363-
seekTo(seekBar.progress * 1000L)
364-
}
365-
})
393+
override fun onStopTrackingTouch(seekBar: SeekBar) = withPlayer {
394+
seekTo(seekBar.progress * 1000L)
395+
}
396+
})
366397
}
367398

368399
private fun showPlaybackSpeedPicker() {
@@ -376,7 +407,8 @@ class TrackActivity : SimpleControllerActivity(), PlaybackSpeedListener {
376407
if (isSlow != binding.activityTrackSpeed.tag as? Boolean) {
377408
binding.activityTrackSpeed.tag = isSlow
378409

379-
val drawableId = if (isSlow) R.drawable.ic_playback_speed_slow_vector else R.drawable.ic_playback_speed_vector
410+
val drawableId =
411+
if (isSlow) R.drawable.ic_playback_speed_slow_vector else R.drawable.ic_playback_speed_vector
380412
binding.activityTrackSpeedIcon.setImageDrawable(resources.getDrawable(drawableId))
381413
}
382414

@@ -397,9 +429,13 @@ class TrackActivity : SimpleControllerActivity(), PlaybackSpeedListener {
397429

398430
override fun onIsPlayingChanged(isPlaying: Boolean) = updatePlayerState()
399431

400-
override fun onRepeatModeChanged(repeatMode: Int) = maybeUpdatePlaybackSettingButton(getPlaybackSetting(repeatMode))
432+
override fun onRepeatModeChanged(repeatMode: Int) {
433+
maybeUpdatePlaybackSettingButton(getPlaybackSetting(repeatMode))
434+
}
401435

402-
override fun onShuffleModeEnabledChanged(shuffleModeEnabled: Boolean) = setupShuffleButton(shuffleModeEnabled)
436+
override fun onShuffleModeEnabledChanged(shuffleModeEnabled: Boolean) {
437+
setupShuffleButton(shuffleModeEnabled)
438+
}
403439

404440
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
405441
super.onMediaItemTransition(mediaItem, reason)
@@ -437,7 +473,7 @@ class TrackActivity : SimpleControllerActivity(), PlaybackSpeedListener {
437473
private fun scheduleProgressUpdate() {
438474
cancelProgressUpdate()
439475
withPlayer {
440-
val delayInMillis = (updateIntervalMillis / config.playbackSpeed).toLong()
476+
val delayInMillis = (UPDATE_INTERVAL_MS / config.playbackSpeed).toLong()
441477
handler.postDelayed(delayInMillis = delayInMillis) {
442478
updateProgress(currentPosition)
443479
scheduleProgressUpdate()
@@ -450,10 +486,51 @@ class TrackActivity : SimpleControllerActivity(), PlaybackSpeedListener {
450486
}
451487

452488
private fun updateProgress(currentPosition: Long) {
453-
binding.activityTrackProgressbar.progress = currentPosition.milliseconds.inWholeSeconds.toInt()
489+
binding.activityTrackProgressbar.progress =
490+
currentPosition.milliseconds.inWholeSeconds.toInt()
454491
}
455492

456493
private fun updatePlayPause(isPlaying: Boolean) {
457494
binding.activityTrackPlayPause.updatePlayPauseIcon(isPlaying, getProperTextColor())
458495
}
496+
497+
/**
498+
* This is here so the player can quickly seek next/previous without doing too much work.
499+
* It probably won't be needed once https://github.com/androidx/media/issues/81 is resolved.
500+
*/
501+
private fun seekWithDelay(previous: Boolean = false) {
502+
if (previous) seekCount -= 1 else seekCount += 1
503+
seekJob?.cancel()
504+
seekJob = scope.launch {
505+
delay(timeMillis = SEEK_COALESCE_INTERVAL_MS)
506+
if (seekCount != 0) {
507+
seekByCount(seekCount)
508+
}
509+
}
510+
}
511+
512+
private fun seekByCount(count: Int) {
513+
withPlayer {
514+
if (currentMediaItem == null) {
515+
return@withPlayer
516+
}
517+
518+
val currentIndex = currentMediaItemIndex
519+
val mediaItemCount = mediaItemCount
520+
val seekIndex = if (shuffleModeEnabled) {
521+
val shuffledIndex = shuffledMediaItemsIndices.indexOf(currentIndex)
522+
val seekIndex = rotateIndex(mediaItemCount, shuffledIndex + count)
523+
shuffledMediaItemsIndices.getOrNull(seekIndex) ?: return@withPlayer
524+
} else {
525+
rotateIndex(mediaItemCount, currentIndex + count)
526+
}
527+
528+
seekTo(seekIndex, 0)
529+
seekCount = 0
530+
}
531+
}
532+
533+
private fun rotateIndex(total: Int, index: Int): Int {
534+
return (index % total + total) % total
535+
}
459536
}

app/src/main/kotlin/org/fossify/musicplayer/helpers/MyWidgetProvider.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,8 @@ class MyWidgetProvider : AppWidgetProvider() {
5959
context.startActivity(intent)
6060
} else {
6161
when (action) {
62-
NEXT -> player.seekToNextMediaItem()
63-
PREVIOUS -> if (player.contentPosition > 5000) player.seekTo(0) else player.seekToPreviousMediaItem()
62+
NEXT -> player.seekToNext()
63+
PREVIOUS -> player.seekToPrevious()
6464
PLAYPAUSE -> player.togglePlayback()
6565
}
6666
}

0 commit comments

Comments
 (0)