@@ -20,6 +20,11 @@ import androidx.media3.common.MediaItem
2020import com.bumptech.glide.load.resource.bitmap.CenterCrop
2121import com.bumptech.glide.load.resource.bitmap.RoundedCorners
2222import 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
2328import org.fossify.commons.extensions.applyColorFilter
2429import org.fossify.commons.extensions.beGone
2530import org.fossify.commons.extensions.beInvisibleIf
@@ -36,7 +41,6 @@ import org.fossify.commons.extensions.updateTextColors
3641import org.fossify.commons.extensions.value
3742import org.fossify.commons.extensions.viewBinding
3843import org.fossify.commons.helpers.MEDIUM_ALPHA
39- import org.fossify.commons.helpers.mydebug
4044import org.fossify.musicplayer.R
4145import org.fossify.musicplayer.databinding.ActivityTrackBinding
4246import org.fossify.musicplayer.extensions.config
@@ -49,6 +53,7 @@ import org.fossify.musicplayer.extensions.loadGlideResource
4953import org.fossify.musicplayer.extensions.nextMediaItem
5054import org.fossify.musicplayer.extensions.sendCommand
5155import org.fossify.musicplayer.extensions.setRepeatMode
56+ import org.fossify.musicplayer.extensions.shuffledMediaItemsIndices
5257import org.fossify.musicplayer.extensions.toTrack
5358import org.fossify.musicplayer.extensions.updatePlayPauseIcon
5459import org.fossify.musicplayer.fragments.PlaybackSpeedFragment
@@ -63,21 +68,29 @@ import kotlin.math.min
6368import kotlin.time.Duration.Companion.milliseconds
6469
6570class 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}
0 commit comments