diff --git a/ultrasonic/build.gradle b/ultrasonic/build.gradle index 74e782ff..89814f83 100644 --- a/ultrasonic/build.gradle +++ b/ultrasonic/build.gradle @@ -34,7 +34,7 @@ android { minifyEnabled false multiDexEnabled true testCoverageEnabled true - applicationIdSuffix ".debug" + applicationIdSuffix '.debug' } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewBinder.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewBinder.kt index 62612082..bca305aa 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewBinder.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewBinder.kt @@ -62,8 +62,6 @@ class TrackViewBinder( diffAdapter.isSelected(item.longId) ) - // Timber.v("Setting listeners") - holder.itemView.setOnLongClickListener { if (onContextMenuClick != null) { val popup = createContextMenu(holder.itemView, track) @@ -116,8 +114,6 @@ class TrackViewBinder( if (newStatus != holder.check.isChecked) holder.check.isChecked = newStatus } - - // Timber.v("Setting listeners done") } override fun onViewRecycled(holder: TrackViewHolder) { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt index 630c00cf..605b66d6 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt @@ -131,7 +131,7 @@ class TrackViewHolder(val view: View) : // Create new Disposable for the new Subscriptions rxBusSubscription = CompositeDisposable() rxBusSubscription!! += RxBus.playerStateObservable.subscribe { - setPlayIcon(it.index == bindingAdapterPosition && it.track?.id == song.id) + setPlayIcon(it.track?.id == song.id && it.index == bindingAdapterPosition) } rxBusSubscription!! += RxBus.trackDownloadStateObservable.subscribe { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/NowPlayingFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/NowPlayingFragment.kt index 9ca26871..de223e35 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/NowPlayingFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/NowPlayingFragment.kt @@ -148,10 +148,10 @@ class NowPlayingFragment : Fragment() { if (abs(deltaX) > MIN_DISTANCE) { // left or right if (deltaX < 0) { - mediaPlayerController.previous() + mediaPlayerController.seekToPrevious() } if (deltaX > 0) { - mediaPlayerController.next() + mediaPlayerController.seekToNext() } } else if (abs(deltaY) > MIN_DISTANCE) { if (deltaY < 0) { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt index 4857f708..8797d48f 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt @@ -154,6 +154,8 @@ class PlayerFragment : private lateinit var pauseButton: View private lateinit var stopButton: View private lateinit var playButton: View + private lateinit var previousButton: MaterialButton + private lateinit var nextButton: MaterialButton private lateinit var shuffleButton: View private lateinit var repeatButton: MaterialButton private lateinit var progressBar: SeekBar @@ -196,6 +198,8 @@ class PlayerFragment : pauseButton = view.findViewById(R.id.button_pause) stopButton = view.findViewById(R.id.button_stop) playButton = view.findViewById(R.id.button_start) + nextButton = view.findViewById(R.id.button_next) + previousButton = view.findViewById(R.id.button_previous) repeatButton = view.findViewById(R.id.button_repeat) fiveStar1ImageView = view.findViewById(R.id.song_five_star_1) fiveStar2ImageView = view.findViewById(R.id.song_five_star_2) @@ -259,9 +263,7 @@ class PlayerFragment : previousButton.setOnClickListener { networkAndStorageChecker.warnIfNetworkOrStorageUnavailable() launch(CommunicationError.getHandler(context)) { - mediaPlayerController.previous() - onCurrentChanged() - onSliderProgressChanged() + mediaPlayerController.seekToPrevious() } } @@ -272,9 +274,7 @@ class PlayerFragment : nextButton.setOnClickListener { networkAndStorageChecker.warnIfNetworkOrStorageUnavailable() launch(CommunicationError.getHandler(context)) { - mediaPlayerController.next() - onCurrentChanged() - onSliderProgressChanged() + mediaPlayerController.seekToNext() } } @@ -285,16 +285,12 @@ class PlayerFragment : pauseButton.setOnClickListener { launch(CommunicationError.getHandler(context)) { mediaPlayerController.pause() - onCurrentChanged() - onSliderProgressChanged() } } stopButton.setOnClickListener { launch(CommunicationError.getHandler(context)) { mediaPlayerController.reset() - onCurrentChanged() - onSliderProgressChanged() } } @@ -304,8 +300,6 @@ class PlayerFragment : launch(CommunicationError.getHandler(context)) { mediaPlayerController.play() - onCurrentChanged() - onSliderProgressChanged() } } @@ -342,7 +336,6 @@ class PlayerFragment : override fun onStopTrackingTouch(seekBar: SeekBar) { launch(CommunicationError.getHandler(context)) { mediaPlayerController.seekTo(progressBar.progress) - onSliderProgressChanged() } } @@ -367,11 +360,13 @@ class PlayerFragment : // Observe playlist changes and update the UI rxBusSubscription += RxBus.playlistObservable.subscribe { onPlaylistChanged() - onSliderProgressChanged() + updateSeekBar() } rxBusSubscription += RxBus.playerStateObservable.subscribe { update() + updateTitle(it.state) + updateButtonStates(it.state) } // Query the Jukebox state in an IO Context @@ -432,7 +427,7 @@ class PlayerFragment : } else { // Download list and Album art must be updated when resumed onPlaylistChanged() - onCurrentChanged() + onTrackChanged() } val handler = Handler(Looper.getMainLooper()) @@ -764,10 +759,9 @@ class PlayerFragment : if (cancel?.isCancellationRequested == true) return val mediaPlayerController = mediaPlayerController if (currentSong?.id != mediaPlayerController.currentMediaItem?.mediaId) { - onCurrentChanged() + onTrackChanged() } - onSliderProgressChanged() - requireActivity().invalidateOptionsMenu() + updateSeekBar() } private fun savePlaylistInBackground(playlistName: String) { @@ -827,12 +821,9 @@ class PlayerFragment : } // Create listener - val clickHandler: ((Track, Int) -> Unit) = { _, pos -> - mediaPlayerController.seekTo(pos, 0) - mediaPlayerController.prepare() - mediaPlayerController.play() - onCurrentChanged() - onSliderProgressChanged() + val clickHandler: ((Track, Int) -> Unit) = { _, listPos -> + val mediaIndex = mediaPlayerController.getUnshuffledIndexOf(listPos) + mediaPlayerController.play(mediaIndex) } viewAdapter.register( @@ -931,6 +922,7 @@ class PlayerFragment : if (actionState == ACTION_STATE_IDLE && dragging) { dragging = false // Move the item in the playlist separately + Timber.i("Moving item %s to %s", startPosition, endPosition) mediaPlayerController.moveItemInPlaylist(startPosition, endPosition) } } @@ -1010,7 +1002,8 @@ class PlayerFragment : private fun onPlaylistChanged() { val mediaPlayerController = mediaPlayerController - val list = mediaPlayerController.playlist + // Try to display playlist in play order + val list = mediaPlayerController.playlistInPlayOrder emptyTextView.setText(R.string.playlist_empty) viewAdapter.submitList(list.map(MediaItem::toTrack)) @@ -1020,7 +1013,7 @@ class PlayerFragment : updateRepeatButtonState(mediaPlayerController.repeatMode) } - private fun onCurrentChanged() { + private fun onTrackChanged() { currentSong = mediaPlayerController.currentMediaItem?.toTrack() scrollToCurrent() @@ -1064,7 +1057,7 @@ class PlayerFragment : it.loadImage(albumArtImageView, currentSong, true, 0) } - displaySongRating() + updateSongRating() } else { currentSong = null songTitleTextView.text = null @@ -1078,24 +1071,27 @@ class PlayerFragment : it.loadImage(albumArtImageView, null, true, 0) } } + + // TODO: It would be a lot nicer if MediaPlayerController would send an event + // when this is necessary instead of updating every time + updateSongRating() + + nextButton.isEnabled = mediaPlayerController.canSeekToNext() + previousButton.isEnabled = mediaPlayerController.canSeekToPrevious() } - @Suppress("LongMethod") @Synchronized - private fun onSliderProgressChanged() { - + private fun updateSeekBar() { + Timber.i("Calling updateSeekBar") val isJukeboxEnabled: Boolean = mediaPlayerController.isJukeboxEnabled val millisPlayed: Int = max(0, mediaPlayerController.playerPosition) val duration: Int = mediaPlayerController.playerDuration val playbackState: Int = mediaPlayerController.playbackState - val isPlaying = mediaPlayerController.isPlaying - if (cancellationToken.isCancellationRequested) return if (currentSong != null) { positionTextView.text = Util.formatTotalDuration(millisPlayed.toLong(), true) durationTextView.text = Util.formatTotalDuration(duration.toLong(), true) - progressBar.max = - if (duration == 0) 100 else duration // Work-around for apparent bug. + progressBar.max = if (duration == 0) 100 else duration // Work-around for apparent bug. progressBar.progress = millisPlayed progressBar.isEnabled = mediaPlayerController.isPlaying || isJukeboxEnabled } else { @@ -1107,18 +1103,18 @@ class PlayerFragment : } val progress = mediaPlayerController.bufferedPercentage + updateBufferProgress(playbackState, progress) + } + private fun updateTitle(playbackState: Int) { when (playbackState) { Player.STATE_BUFFERING -> { - val downloadStatus = resources.getString( R.string.download_playerstate_loading ) - progressBar.secondaryProgress = progress setTitle(this@PlayerFragment, downloadStatus) } Player.STATE_READY -> { - progressBar.secondaryProgress = progress if (mediaPlayerController.isShufflePlayEnabled) { setTitle( this@PlayerFragment, @@ -1128,13 +1124,22 @@ class PlayerFragment : setTitle(this@PlayerFragment, R.string.common_appname) } } - Player.STATE_IDLE, - Player.STATE_ENDED, - -> { - } + Player.STATE_IDLE, Player.STATE_ENDED -> {} else -> setTitle(this@PlayerFragment, R.string.common_appname) } + } + private fun updateBufferProgress(playbackState: Int, progress: Int) { + when (playbackState) { + Player.STATE_BUFFERING, Player.STATE_READY -> { + progressBar.secondaryProgress = progress + } + else -> { } + } + } + + private fun updateButtonStates(playbackState: Int) { + val isPlaying = mediaPlayerController.isPlaying when (playbackState) { Player.STATE_READY -> { pauseButton.isVisible = isPlaying @@ -1152,10 +1157,6 @@ class PlayerFragment : playButton.isVisible = true } } - - // TODO: It would be a lot nicer if MediaPlayerController would send an event - // when this is necessary instead of updating every time - displaySongRating() } private fun seek(forward: Boolean) { @@ -1189,18 +1190,14 @@ class PlayerFragment : // Right to Left swipe if (e1X - e2X > swipeDistance && absX > swipeVelocity) { networkAndStorageChecker.warnIfNetworkOrStorageUnavailable() - mediaPlayerController.next() - onCurrentChanged() - onSliderProgressChanged() + mediaPlayerController.seekToNext() return true } // Left to Right swipe if (e2X - e1X > swipeDistance && absX > swipeVelocity) { networkAndStorageChecker.warnIfNetworkOrStorageUnavailable() - mediaPlayerController.previous() - onCurrentChanged() - onSliderProgressChanged() + mediaPlayerController.seekToPrevious() return true } @@ -1208,7 +1205,6 @@ class PlayerFragment : if (e2Y - e1Y > swipeDistance && absY > swipeVelocity) { networkAndStorageChecker.warnIfNetworkOrStorageUnavailable() mediaPlayerController.seekTo(mediaPlayerController.playerPosition + 30000) - onSliderProgressChanged() return true } @@ -1216,7 +1212,6 @@ class PlayerFragment : if (e1Y - e2Y > swipeDistance && absY > swipeVelocity) { networkAndStorageChecker.warnIfNetworkOrStorageUnavailable() mediaPlayerController.seekTo(mediaPlayerController.playerPosition - 8000) - onSliderProgressChanged() return true } return false @@ -1237,7 +1232,7 @@ class PlayerFragment : return false } - private fun displaySongRating() { + private fun updateSongRating() { var rating = 0 if (currentSong?.userRating != null) { @@ -1253,7 +1248,7 @@ class PlayerFragment : private fun setSongRating(rating: Int) { if (currentSong == null) return - displaySongRating() + updateSongRating() mediaPlayerController.setSongRating(rating) } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt index 019263db..7603f94f 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt @@ -26,9 +26,12 @@ import androidx.media3.datasource.okhttp.OkHttpDataSource import androidx.media3.exoplayer.DefaultRenderersFactory import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.source.DefaultMediaSourceFactory +import androidx.media3.exoplayer.source.ShuffleOrder.DefaultShuffleOrder +import androidx.media3.exoplayer.source.ShuffleOrder.UnshuffledShuffleOrder import androidx.media3.session.MediaLibraryService import androidx.media3.session.MediaSession import io.reactivex.rxjava3.disposables.CompositeDisposable +import java.util.Random import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -175,6 +178,27 @@ class PlaybackService : player.setWakeMode(getWakeModeFlag()) } + // Set a listener to reset the ShuffleOrder + rxBusSubscription += RxBus.shufflePlayObservable.subscribe { shuffle -> + val len = player.currentTimeline.windowCount + + Timber.i("Resetting shuffle order, isShuffled: %s", shuffle) + + // If disabling Shuffle return early + if (!shuffle) { + return@subscribe player.setShuffleOrder(UnshuffledShuffleOrder(len)) + } + + // Get the position of the current track in the unshuffled order + val cur = player.currentMediaItemIndex + val seed = System.currentTimeMillis() + val random = Random(seed) + + val list = createShuffleListFromCurrentIndex(cur, len, random) + Timber.i("New Shuffle order: %s", list.joinToString { it.toString() }) + player.setShuffleOrder(DefaultShuffleOrder(list, seed)) + } + // Listen to the shutdown command rxBusSubscription += RxBus.shutdownCommandObservable.subscribe { Timber.i("Received destroy command via Rx") @@ -185,6 +209,24 @@ class PlaybackService : isStarted = true } + fun createShuffleListFromCurrentIndex( + currentIndex: Int, + length: Int, + random: Random + ): IntArray { + val list = IntArray(length) { it } + + // Shuffle the remaining items using a swapping algorithm + for (i in currentIndex + 1 until length) { + val swapIndex = (currentIndex + 1) + random.nextInt(i - currentIndex) + val swapItem = list[i] + list[i] = list[swapIndex] + list[swapIndex] = swapItem + } + + return list + } + private val listener: Player.Listener = object : Player.Listener { override fun onShuffleModeEnabledChanged(shuffleModeEnabled: Boolean) { cacheNextSongs() diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt index b7f81c09..c421a122 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt @@ -11,11 +11,14 @@ import android.content.Context import android.os.Handler import android.os.Looper import android.widget.Toast +import androidx.annotation.IntRange import androidx.media3.common.C import androidx.media3.common.HeartRating import androidx.media3.common.MediaItem import androidx.media3.common.PlaybackException import androidx.media3.common.Player +import androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT +import androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS import androidx.media3.common.Player.MEDIA_ITEM_TRANSITION_REASON_AUTO import androidx.media3.common.Player.REPEAT_MODE_OFF import androidx.media3.common.Timeline @@ -30,6 +33,7 @@ import io.reactivex.rxjava3.disposables.CompositeDisposable import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import org.koin.core.component.KoinComponent import org.koin.core.component.inject import org.moire.ultrasonic.app.UApp @@ -60,6 +64,7 @@ class MediaPlayerController( private val externalStorageMonitor: ExternalStorageMonitor, val context: Context ) : KoinComponent { + private val activeServerProvider: ActiveServerProvider by inject() private var created = false @@ -96,6 +101,14 @@ class MediaPlayerController( * We run the event through RxBus in order to throttle them */ override fun onTimelineChanged(timeline: Timeline, reason: Int) { + val start = controller?.currentTimeline?.getFirstWindowIndex(isShufflePlayEnabled) + Timber.w("On timeline changed. First shuffle play at index: %s", start) + deferredPlay?.let { + Timber.w("Executing deferred shuffle play") + it() + deferredPlay = null + } + RxBus.playlistPublisher.onNext(playlist.map(MediaItem::toTrack)) } @@ -150,19 +163,21 @@ class MediaPlayerController( override fun onShuffleModeEnabledChanged(shuffleModeEnabled: Boolean) { val timeline: Timeline = controller!!.currentTimeline - var windowIndex = timeline.getFirstWindowIndex( /* shuffleModeEnabled= */true) + var windowIndex = timeline.getFirstWindowIndex(true) var count = 0 Timber.d("Shuffle: windowIndex: $windowIndex, at: $count") while (windowIndex != C.INDEX_UNSET) { count++ windowIndex = timeline.getNextWindowIndex( - windowIndex, REPEAT_MODE_OFF, /* shuffleModeEnabled= */true + windowIndex, REPEAT_MODE_OFF, true ) Timber.d("Shuffle: windowIndex: $windowIndex, at: $count") } } } + private var deferredPlay: (() -> Unit)? = null + private var cachedMediaItem: MediaItem? = null fun onCreate(onCreated: () -> Unit) { @@ -259,7 +274,7 @@ class MediaPlayerController( private fun publishPlaybackState() { val newState = RxBus.StateWithTrack( track = currentMediaItem?.toTrack(), - index = currentMediaItemIndex, + index = if (isShufflePlayEnabled) getCurrentShuffleIndex() else currentMediaItemIndex, isPlaying = isPlaying, state = playbackState ) @@ -316,6 +331,8 @@ class MediaPlayerController( @Synchronized fun play(index: Int) { controller?.seekTo(index, 0L) + // FIXME CHECK ITS NOT MAKING PROBLEMS + controller?.prepare() controller?.play() } @@ -404,6 +421,7 @@ class MediaPlayerController( } if (shuffle) isShufflePlayEnabled = true + Timber.w("Adding ${mediaItems.size} media items") controller?.addMediaItems(insertAt, mediaItems) prepare() @@ -411,10 +429,19 @@ class MediaPlayerController( // Playback doesn't start correctly when the player is in STATE_ENDED. // So we need to call seek before (this is what play(0,0)) does. // We can't just use play(0,0) then all random playlists will start with the first track. - // This means that we need to generate the random first track ourselves. + // Additionally the shuffle order becomes clear on after some time, so we need to wait for + // the right event, and can start playback only then. if (autoPlay) { - val start = controller?.currentTimeline?.getFirstWindowIndex(isShufflePlayEnabled) ?: 0 - play(start) + if (isShufflePlayEnabled) { + deferredPlay = { + val start = controller?.currentTimeline + ?.getFirstWindowIndex(isShufflePlayEnabled) ?: 0 + Timber.i("Deferred shuffle play starting now at index: %s", start) + play(start) + } + } else { + play(0) + } } } @@ -422,6 +449,8 @@ class MediaPlayerController( var isShufflePlayEnabled: Boolean get() = controller?.shuffleModeEnabled == true set(enabled) { + Timber.i("Shuffle is now enabled: %s", enabled) + RxBus.shufflePlayPublisher.onNext(enabled) controller?.shuffleModeEnabled = enabled } @@ -431,11 +460,17 @@ class MediaPlayerController( return isShufflePlayEnabled } + /** + * Returns an estimate of the percentage in the current content up to which data is + * buffered, or 0 if no estimate is available. + */ + @get:IntRange(from = 0, to = 100) val bufferedPercentage: Int get() = controller?.bufferedPercentage ?: 0 @Synchronized fun moveItemInPlaylist(oldPos: Int, newPos: Int) { + // TODO: This currently does not care about shuffle position. controller?.moveMediaItem(oldPos, newPos) } @@ -494,15 +529,25 @@ class MediaPlayerController( } @Synchronized - fun previous() { + fun seekToPrevious() { controller?.seekToPrevious() } @Synchronized - operator fun next() { + fun canSeekToPrevious(): Boolean { + return controller?.isCommandAvailable(COMMAND_SEEK_TO_PREVIOUS) == true + } + + @Synchronized + fun seekToNext() { controller?.seekToNext() } + @Synchronized + fun canSeekToNext(): Boolean { + return controller?.isCommandAvailable(COMMAND_SEEK_TO_NEXT) == true + } + @Synchronized fun reset() { controller?.clearMediaItems() @@ -693,15 +738,15 @@ class MediaPlayerController( if (currentMediaItem == null) return val song = currentMediaItem!!.toTrack() song.userRating = rating - Thread { - try { - getMusicService().setRating(song.id, rating) - } catch (e: Exception) { - Timber.e(e) + mainScope.launch { + withContext(Dispatchers.IO) { + try { + getMusicService().setRating(song.id, rating) + } catch (e: Exception) { + Timber.e(e) + } } - }.start() - // TODO this would be better handled with a Rx command - // updateNotification() + } } val currentMediaItem: MediaItem? @@ -710,9 +755,65 @@ class MediaPlayerController( val currentMediaItemIndex: Int get() = controller?.currentMediaItemIndex ?: -1 + fun getCurrentShuffleIndex(): Int { + val currentMediaItemIndex = controller?.currentMediaItemIndex ?: return -1 + return getShuffledIndexOf(currentMediaItemIndex) + } + + /** + * Loops over the timeline windows to find the entry which matches the given closure. + * + * @param searchClosure Determines the condition which the searched for window needs to match. + * @param timeline the timeline to search in. + * @return the index of the window that satisfies the search condition, + * or [C.INDEX_UNSET] if not found. + */ + private fun getWindowIndexWhere(searchClosure: (Int, Int) -> Boolean): Int { + val timeline = controller?.currentTimeline!! + var windowIndex = timeline.getFirstWindowIndex(true) + var count = 0 + while (windowIndex != C.INDEX_UNSET) { + if (searchClosure(count, windowIndex)) return count + count++ + windowIndex = timeline.getNextWindowIndex( + windowIndex, REPEAT_MODE_OFF, true + ) + } + + return C.INDEX_UNSET + } + + /** + * Returns the index of the shuffled position of the current playback item given its original + * position in the unshuffled timeline. + * + * @param searchPosition The index of the item in the unshuffled timeline to search for + * in the shuffled timeline. + * @return The index of the item in the shuffled timeline, or [C.INDEX_UNSET] if not found. + */ + fun getShuffledIndexOf(searchPosition: Int): Int { + return getWindowIndexWhere { _, windowIndex -> windowIndex == searchPosition } + } + + /** + * Returns the index of the unshuffled position of the current playback item given its shuffled + * position in the shuffled timeline. + * + * @param shufflePosition the index of the item in the shuffled timeline to search for in the + * unshuffled timeline. + * @return the index of the item in the unshuffled timeline, or [C.INDEX_UNSET] if not found. + */ + fun getUnshuffledIndexOf(shufflePosition: Int): Int { + return getWindowIndexWhere { count, _ -> count == shufflePosition } + } + val mediaItemCount: Int get() = controller?.mediaItemCount ?: 0 + fun getMediaItemAt(index: Int): MediaItem? { + return controller?.getMediaItemAt(index) + } + val playlistSize: Int get() = controller?.currentTimeline?.windowCount ?: 0 @@ -721,10 +822,6 @@ class MediaPlayerController( return Util.getPlayListFromTimeline(controller?.currentTimeline, false) } - fun getMediaItemAt(index: Int): MediaItem? { - return controller?.getMediaItemAt(index) - } - val playlistInPlayOrder: List get() { return Util.getPlayListFromTimeline( diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.kt index d412d0e6..c05805e5 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.kt @@ -182,8 +182,8 @@ class MediaPlayerLifecycleSupport : KoinComponent { when (keyCode) { KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, KeyEvent.KEYCODE_HEADSETHOOK -> mediaPlayerController.togglePlayPause() - KeyEvent.KEYCODE_MEDIA_PREVIOUS -> mediaPlayerController.previous() - KeyEvent.KEYCODE_MEDIA_NEXT -> mediaPlayerController.next() + KeyEvent.KEYCODE_MEDIA_PREVIOUS -> mediaPlayerController.seekToPrevious() + KeyEvent.KEYCODE_MEDIA_NEXT -> mediaPlayerController.seekToNext() KeyEvent.KEYCODE_MEDIA_STOP -> mediaPlayerController.stop() KeyEvent.KEYCODE_MEDIA_PLAY -> mediaPlayerController.play() KeyEvent.KEYCODE_MEDIA_PAUSE -> mediaPlayerController.pause() @@ -226,8 +226,8 @@ class MediaPlayerLifecycleSupport : KoinComponent { // no need to call anything if (isRunning) mediaPlayerController.resumeOrPlay() - Constants.CMD_NEXT -> mediaPlayerController.next() - Constants.CMD_PREVIOUS -> mediaPlayerController.previous() + Constants.CMD_NEXT -> mediaPlayerController.seekToNext() + Constants.CMD_PREVIOUS -> mediaPlayerController.seekToPrevious() Constants.CMD_TOGGLEPAUSE -> mediaPlayerController.togglePlayPause() Constants.CMD_STOP -> mediaPlayerController.stop() Constants.CMD_PAUSE -> mediaPlayerController.pause() diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt index 48a1061f..f4b62de0 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt @@ -20,9 +20,13 @@ class RxBus { private fun mainThread() = AndroidSchedulers.from(Looper.getMainLooper()) + val shufflePlayPublisher: PublishSubject = + PublishSubject.create() + val shufflePlayObservable: Observable = + shufflePlayPublisher + var activeServerChangingPublisher: PublishSubject = PublishSubject.create() - // Subscribers should be called synchronously, not on another thread var activeServerChangingObservable: Observable = activeServerChangingPublisher diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt index 6ba28594..cdcd5443 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt @@ -757,7 +757,7 @@ object Util { fun getPlayListFromTimeline( timeline: Timeline?, - shuffle: Boolean, + isShuffled: Boolean, firstIndex: Int? = null, count: Int? = null ): List { @@ -765,13 +765,13 @@ object Util { if (timeline.windowCount < 1) return emptyList() val playlist: MutableList = mutableListOf() - var i = firstIndex ?: timeline.getFirstWindowIndex(false) + var i = firstIndex ?: timeline.getFirstWindowIndex(isShuffled) if (i == C.INDEX_UNSET) return emptyList() while (i != C.INDEX_UNSET && (count != playlist.count())) { val window = timeline.getWindow(i, Timeline.Window()) playlist.add(window.mediaItem) - i = timeline.getNextWindowIndex(i, Player.REPEAT_MODE_OFF, shuffle) + i = timeline.getNextWindowIndex(i, Player.REPEAT_MODE_OFF, isShuffled) } return playlist }