diff --git a/CHANGELOG.md b/CHANGELOG.md index f0913606..38922a9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Fixed - Fixed another crash when clearing app from recents ([#298]) +- Fixed flicker when seeking on the player screen ## [1.5.1] - 2025-11-05 ### Fixed diff --git a/app/src/main/kotlin/org/fossify/musicplayer/activities/TrackActivity.kt b/app/src/main/kotlin/org/fossify/musicplayer/activities/TrackActivity.kt index 2371a632..ee431829 100644 --- a/app/src/main/kotlin/org/fossify/musicplayer/activities/TrackActivity.kt +++ b/app/src/main/kotlin/org/fossify/musicplayer/activities/TrackActivity.kt @@ -20,6 +20,11 @@ import androidx.media3.common.MediaItem import com.bumptech.glide.load.resource.bitmap.CenterCrop import com.bumptech.glide.load.resource.bitmap.RoundedCorners import com.bumptech.glide.request.RequestOptions +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch import org.fossify.commons.extensions.applyColorFilter import org.fossify.commons.extensions.beGone import org.fossify.commons.extensions.beInvisibleIf @@ -36,7 +41,6 @@ import org.fossify.commons.extensions.updateTextColors import org.fossify.commons.extensions.value import org.fossify.commons.extensions.viewBinding import org.fossify.commons.helpers.MEDIUM_ALPHA -import org.fossify.commons.helpers.mydebug import org.fossify.musicplayer.R import org.fossify.musicplayer.databinding.ActivityTrackBinding import org.fossify.musicplayer.extensions.config @@ -49,6 +53,7 @@ import org.fossify.musicplayer.extensions.loadGlideResource import org.fossify.musicplayer.extensions.nextMediaItem import org.fossify.musicplayer.extensions.sendCommand import org.fossify.musicplayer.extensions.setRepeatMode +import org.fossify.musicplayer.extensions.shuffledMediaItemsIndices import org.fossify.musicplayer.extensions.toTrack import org.fossify.musicplayer.extensions.updatePlayPauseIcon import org.fossify.musicplayer.fragments.PlaybackSpeedFragment @@ -63,13 +68,20 @@ import kotlin.math.min import kotlin.time.Duration.Companion.milliseconds class TrackActivity : SimpleControllerActivity(), PlaybackSpeedListener { - private val SWIPE_DOWN_THRESHOLD = 100 + companion object { + private const val SWIPE_DOWN_THRESHOLD = 100 + private const val SEEK_COALESCE_INTERVAL_MS = 200L + private const val UPDATE_INTERVAL_MS = 150L + } private var isThirdPartyIntent = false private lateinit var nextTrackPlaceholder: Drawable private val handler = Handler(Looper.getMainLooper()) - private val updateIntervalMillis = 500L + + private val scope = CoroutineScope(Dispatchers.Default) + private var seekJob: Job? = null + private var seekCount = 0 private val binding by viewBinding(ActivityTrackBinding::inflate) @@ -77,7 +89,8 @@ class TrackActivity : SimpleControllerActivity(), PlaybackSpeedListener { super.onCreate(savedInstanceState) setContentView(binding.root) setupEdgeToEdge(padBottomSystem = listOf(binding.nextTrackHolder)) - nextTrackPlaceholder = resources.getColoredDrawableWithColor(R.drawable.ic_headset, getProperTextColor()) + nextTrackPlaceholder = + resources.getColoredDrawableWithColor(R.drawable.ic_headset, getProperTextColor()) setupButtons() setupFlingListener() @@ -87,7 +100,12 @@ class TrackActivity : SimpleControllerActivity(), PlaybackSpeedListener { } isThirdPartyIntent = intent.action == Intent.ACTION_VIEW - arrayOf(activityTrackToggleShuffle, activityTrackPrevious, activityTrackNext, activityTrackPlaybackSetting).forEach { + arrayOf( + activityTrackToggleShuffle, + activityTrackPrevious, + activityTrackNext, + activityTrackPlaybackSetting + ).forEach { it.beInvisibleIf(isThirdPartyIntent) } @@ -98,7 +116,10 @@ class TrackActivity : SimpleControllerActivity(), PlaybackSpeedListener { setupTrackInfo(PlaybackService.currentMediaItem) setupNextTrackInfo(PlaybackService.nextMediaItem) - activityTrackPlayPause.updatePlayPauseIcon(PlaybackService.isPlaying, getProperTextColor()) + activityTrackPlayPause.updatePlayPauseIcon( + isPlaying = PlaybackService.isPlaying, + color = getProperTextColor() + ) updatePlayerState() nextTrackHolder.background = getProperBackgroundColor().toDrawable() @@ -177,9 +198,9 @@ class TrackActivity : SimpleControllerActivity(), PlaybackSpeedListener { private fun setupButtons() = binding.apply { activityTrackToggleShuffle.setOnClickListener { withPlayer { toggleShuffle() } } - activityTrackPrevious.setOnClickListener { withPlayer { seekToPrevious() } } + activityTrackPrevious.setOnClickListener { seekWithDelay(previous = true) } activityTrackPlayPause.setOnClickListener { togglePlayback() } - activityTrackNext.setOnClickListener { withPlayer { seekToNext() } } + activityTrackNext.setOnClickListener { seekWithDelay() } activityTrackProgressCurrent.setOnClickListener { seekBack() } activityTrackProgressMax.setOnClickListener { seekForward() } activityTrackPlaybackSetting.setOnClickListener { togglePlaybackSetting() } @@ -201,17 +222,20 @@ class TrackActivity : SimpleControllerActivity(), PlaybackSpeedListener { } binding.nextTrackHolder.beVisible() - val artist = if (track.artist.trim().isNotEmpty() && track.artist != MediaStore.UNKNOWN_STRING) { - " • ${track.artist}" - } else { - "" - } + val artist = + if (track.artist.trim().isNotEmpty() && track.artist != MediaStore.UNKNOWN_STRING) { + " • ${track.artist}" + } else { + "" + } @SuppressLint("SetTextI18n") binding.nextTrackLabel.text = "${getString(R.string.next_track)} ${track.title}$artist" getTrackCoverArt(track) { coverArt -> - val cornerRadius = resources.getDimension(org.fossify.commons.R.dimen.rounded_corner_radius_small).toInt() + val cornerRadius = + resources.getDimension(org.fossify.commons.R.dimen.rounded_corner_radius_small) + .toInt() val wantedSize = resources.getDimension(R.dimen.song_image_size).toInt() // change cover image manually only once loaded successfully to avoid blinking at fails and placeholders @@ -271,7 +295,12 @@ class TrackActivity : SimpleControllerActivity(), PlaybackSpeedListener { @SuppressLint("ClickableViewAccessibility") private fun setupFlingListener() { val flingListener = object : GestureDetector.SimpleOnGestureListener() { - override fun onFling(e1: MotionEvent?, e2: MotionEvent, velocityX: Float, velocityY: Float): Boolean { + override fun onFling( + e1: MotionEvent?, + e2: MotionEvent, + velocityX: Float, + velocityY: Float + ): Boolean { if (e1 != null) { if (velocityY > 0 && velocityY > velocityX && e2.y - e1.y > SWIPE_DOWN_THRESHOLD) { finish() @@ -305,7 +334,8 @@ class TrackActivity : SimpleControllerActivity(), PlaybackSpeedListener { binding.activityTrackToggleShuffle.apply { applyColorFilter(if (isShuffleEnabled) getProperPrimaryColor() else getProperTextColor()) alpha = if (isShuffleEnabled) 1f else MEDIUM_ALPHA - contentDescription = getString(if (isShuffleEnabled) R.string.disable_shuffle else R.string.enable_shuffle) + contentDescription = + getString(if (isShuffleEnabled) R.string.disable_shuffle else R.string.enable_shuffle) } } @@ -351,18 +381,19 @@ class TrackActivity : SimpleControllerActivity(), PlaybackSpeedListener { binding.activityTrackSpeedIcon.applyColorFilter(getProperTextColor()) updatePlaybackSpeed(config.playbackSpeed) - binding.activityTrackProgressbar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { - override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) { - val formattedProgress = progress.getFormattedDuration() - binding.activityTrackProgressCurrent.text = formattedProgress - } + binding.activityTrackProgressbar.setOnSeekBarChangeListener( + object : SeekBar.OnSeekBarChangeListener { + override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) { + val formattedProgress = progress.getFormattedDuration() + binding.activityTrackProgressCurrent.text = formattedProgress + } - override fun onStartTrackingTouch(seekBar: SeekBar) {} + override fun onStartTrackingTouch(seekBar: SeekBar) {} - override fun onStopTrackingTouch(seekBar: SeekBar) = withPlayer { - seekTo(seekBar.progress * 1000L) - } - }) + override fun onStopTrackingTouch(seekBar: SeekBar) = withPlayer { + seekTo(seekBar.progress * 1000L) + } + }) } private fun showPlaybackSpeedPicker() { @@ -376,7 +407,8 @@ class TrackActivity : SimpleControllerActivity(), PlaybackSpeedListener { if (isSlow != binding.activityTrackSpeed.tag as? Boolean) { binding.activityTrackSpeed.tag = isSlow - val drawableId = if (isSlow) R.drawable.ic_playback_speed_slow_vector else R.drawable.ic_playback_speed_vector + val drawableId = + if (isSlow) R.drawable.ic_playback_speed_slow_vector else R.drawable.ic_playback_speed_vector binding.activityTrackSpeedIcon.setImageDrawable(resources.getDrawable(drawableId)) } @@ -397,9 +429,13 @@ class TrackActivity : SimpleControllerActivity(), PlaybackSpeedListener { override fun onIsPlayingChanged(isPlaying: Boolean) = updatePlayerState() - override fun onRepeatModeChanged(repeatMode: Int) = maybeUpdatePlaybackSettingButton(getPlaybackSetting(repeatMode)) + override fun onRepeatModeChanged(repeatMode: Int) { + maybeUpdatePlaybackSettingButton(getPlaybackSetting(repeatMode)) + } - override fun onShuffleModeEnabledChanged(shuffleModeEnabled: Boolean) = setupShuffleButton(shuffleModeEnabled) + override fun onShuffleModeEnabledChanged(shuffleModeEnabled: Boolean) { + setupShuffleButton(shuffleModeEnabled) + } override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) { super.onMediaItemTransition(mediaItem, reason) @@ -437,7 +473,7 @@ class TrackActivity : SimpleControllerActivity(), PlaybackSpeedListener { private fun scheduleProgressUpdate() { cancelProgressUpdate() withPlayer { - val delayInMillis = (updateIntervalMillis / config.playbackSpeed).toLong() + val delayInMillis = (UPDATE_INTERVAL_MS / config.playbackSpeed).toLong() handler.postDelayed(delayInMillis = delayInMillis) { updateProgress(currentPosition) scheduleProgressUpdate() @@ -450,10 +486,51 @@ class TrackActivity : SimpleControllerActivity(), PlaybackSpeedListener { } private fun updateProgress(currentPosition: Long) { - binding.activityTrackProgressbar.progress = currentPosition.milliseconds.inWholeSeconds.toInt() + binding.activityTrackProgressbar.progress = + currentPosition.milliseconds.inWholeSeconds.toInt() } private fun updatePlayPause(isPlaying: Boolean) { binding.activityTrackPlayPause.updatePlayPauseIcon(isPlaying, getProperTextColor()) } + + /** + * This is here so the player can quickly seek next/previous without doing too much work. + * It probably won't be needed once https://github.com/androidx/media/issues/81 is resolved. + */ + private fun seekWithDelay(previous: Boolean = false) { + if (previous) seekCount -= 1 else seekCount += 1 + seekJob?.cancel() + seekJob = scope.launch { + delay(timeMillis = SEEK_COALESCE_INTERVAL_MS) + if (seekCount != 0) { + seekByCount(seekCount) + } + } + } + + private fun seekByCount(count: Int) { + withPlayer { + if (currentMediaItem == null) { + return@withPlayer + } + + val currentIndex = currentMediaItemIndex + val mediaItemCount = mediaItemCount + val seekIndex = if (shuffleModeEnabled) { + val shuffledIndex = shuffledMediaItemsIndices.indexOf(currentIndex) + val seekIndex = rotateIndex(mediaItemCount, shuffledIndex + count) + shuffledMediaItemsIndices.getOrNull(seekIndex) ?: return@withPlayer + } else { + rotateIndex(mediaItemCount, currentIndex + count) + } + + seekTo(seekIndex, 0) + seekCount = 0 + } + } + + private fun rotateIndex(total: Int, index: Int): Int { + return (index % total + total) % total + } } diff --git a/app/src/main/kotlin/org/fossify/musicplayer/helpers/MyWidgetProvider.kt b/app/src/main/kotlin/org/fossify/musicplayer/helpers/MyWidgetProvider.kt index 0f567def..fd4cfcbf 100644 --- a/app/src/main/kotlin/org/fossify/musicplayer/helpers/MyWidgetProvider.kt +++ b/app/src/main/kotlin/org/fossify/musicplayer/helpers/MyWidgetProvider.kt @@ -59,8 +59,8 @@ class MyWidgetProvider : AppWidgetProvider() { context.startActivity(intent) } else { when (action) { - NEXT -> player.seekToNextMediaItem() - PREVIOUS -> if (player.contentPosition > 5000) player.seekTo(0) else player.seekToPreviousMediaItem() + NEXT -> player.seekToNext() + PREVIOUS -> player.seekToPrevious() PLAYPAUSE -> player.togglePlayback() } } diff --git a/app/src/main/kotlin/org/fossify/musicplayer/playback/player/SimpleMusicPlayer.kt b/app/src/main/kotlin/org/fossify/musicplayer/playback/player/SimpleMusicPlayer.kt index 70f32bc7..1dd74a63 100644 --- a/app/src/main/kotlin/org/fossify/musicplayer/playback/player/SimpleMusicPlayer.kt +++ b/app/src/main/kotlin/org/fossify/musicplayer/playback/player/SimpleMusicPlayer.kt @@ -6,16 +6,10 @@ import androidx.media3.common.Player import androidx.media3.common.util.UnstableApi import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.source.ShuffleOrder.DefaultShuffleOrder -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch import org.fossify.musicplayer.extensions.currentMediaItems import org.fossify.musicplayer.extensions.maybeForceNext import org.fossify.musicplayer.extensions.maybeForcePrevious import org.fossify.musicplayer.extensions.move -import org.fossify.musicplayer.extensions.runOnPlayerThread import org.fossify.musicplayer.extensions.shuffledMediaItemsIndices import org.fossify.musicplayer.inlines.indexOfFirstOrNull @@ -24,12 +18,6 @@ private const val DEFAULT_SHUFFLE_ORDER_SEED = 42L @UnstableApi class SimpleMusicPlayer(private val exoPlayer: ExoPlayer) : ForwardingPlayer(exoPlayer) { - private var seekToNextCount = 0 - private var seekToPreviousCount = 0 - - private val scope = CoroutineScope(Dispatchers.Default) - private var seekJob: Job? = null - /** * The default implementation only advertises the seek to next and previous item in the case * that it's not the first or last track. We manually advertise that these @@ -69,8 +57,7 @@ class SimpleMusicPlayer(private val exoPlayer: ExoPlayer) : ForwardingPlayer(exo override fun seekToNext() { play() if (!maybeForceNext()) { - seekToNextCount += 1 - seekWithDelay() + super.seekToNext() } } @@ -79,26 +66,13 @@ class SimpleMusicPlayer(private val exoPlayer: ExoPlayer) : ForwardingPlayer(exo if (currentPosition > 5000) { seekTo(0) } else if (!maybeForcePrevious()) { - seekToPreviousCount += 1 - seekWithDelay() + super.seekToPrevious() } } - override fun seekToNextMediaItem() { - play() - if (!maybeForceNext()) { - seekToNextCount += 1 - seekWithDelay() - } - } + override fun seekToNextMediaItem() = seekToNext() - override fun seekToPreviousMediaItem() { - play() - if (!maybeForcePrevious()) { - seekToPreviousCount += 1 - seekWithDelay() - } - } + override fun seekToPreviousMediaItem() = seekToPrevious() fun getAudioSessionId(): Int { return exoPlayer.audioSessionId @@ -147,45 +121,4 @@ class SimpleMusicPlayer(private val exoPlayer: ExoPlayer) : ForwardingPlayer(exo ) } } - - /** - * This is here so the player can quickly seek next/previous without doing too much work. - * It probably won't be needed once https://github.com/androidx/media/issues/81 is resolved. - */ - private fun seekWithDelay() { - seekJob?.cancel() - seekJob = scope.launch { - delay(timeMillis = 300) - val seekCount = seekToNextCount - seekToPreviousCount - if (seekCount != 0) { - seekByCount(seekCount) - } - } - } - - private fun seekByCount(seekCount: Int) { - runOnPlayerThread { - if (currentMediaItem == null) { - return@runOnPlayerThread - } - - val currentIndex = currentMediaItemIndex - val seekIndex = if (shuffleModeEnabled) { - val shuffledIndex = shuffledMediaItemsIndices.indexOf(currentIndex) - val seekIndex = rotateIndex(shuffledIndex + seekCount) - shuffledMediaItemsIndices.getOrNull(seekIndex) ?: return@runOnPlayerThread - } else { - rotateIndex(currentIndex + seekCount) - } - - seekTo(seekIndex, 0) - seekToNextCount = 0 - seekToPreviousCount = 0 - } - } - - private fun rotateIndex(index: Int): Int { - val count = mediaItemCount - return (index % count + count) % count - } }