Skip to content

Commit cae708d

Browse files
Android Auto: Add shuffle and play button for the playlist
Background: - While testing the Android Auto app, I missed having a quick way to shuffle and play the playlist. - This adds support to shuffle and play the playlist directly from the Android Auto app. Changes: - Added a "Shuffle and play" button as the first item in the playlist view. - The button is only displayed for non-empty playlists to prevent confusion. PlayQueue refactor: - Refactored the `PlayQueue.shuffle()` method to support different shuffling modes - Add optional `shuffleAll` parameter to `PlayQueue.shuffle()` method with default value false - When shuffleAll=false (default): preserves currently playing item at the head of the queue (existing behavior) - When shuffleAll=true: shuffles all items without preserving the current item position - Update documentation to clearly explain both modes of operation
1 parent 6e0b7be commit cae708d

7 files changed

Lines changed: 87 additions & 10 deletions

File tree

app/src/main/java/org/schabi/newpipe/player/Player.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1298,7 +1298,7 @@ public void onShuffleModeEnabledChanged(final boolean shuffleModeEnabled) {
12981298

12991299
if (playQueue != null) {
13001300
if (shuffleModeEnabled) {
1301-
playQueue.shuffle();
1301+
playQueue.shuffle(false);
13021302
} else {
13031303
playQueue.unshuffle();
13041304
}

app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserCommon.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ internal const val ID_STREAM = "stream"
1717
internal const val ID_PLAYLIST = "playlist"
1818
internal const val ID_CHANNEL = "channel"
1919

20+
internal const val ID_SHUFFLE = "ID_SHUFFLE"
21+
2022
internal fun infoItemTypeToString(type: InfoType): String {
2123
return when (type) {
2224
InfoType.STREAM -> ID_STREAM

app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserImpl.kt

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,31 @@ class MediaBrowserImpl(
257257
.build().toString()
258258
}
259259

260+
private fun createShuffleAndPlayMediaItem(
261+
isRemote: Boolean,
262+
playlistId: Long
263+
): MediaBrowserCompat.MediaItem {
264+
val resources = context.resources
265+
266+
val builder = MediaDescriptionCompat.Builder()
267+
.setMediaId(createMediaIdForPlaylistShuffle(isRemote, playlistId))
268+
.setTitle(resources.getString(R.string.shuffle_and_play))
269+
270+
@DrawableRes val iconResId = R.drawable.ic_shuffle_white
271+
builder.setIconUri(
272+
Uri.Builder()
273+
.scheme(ContentResolver.SCHEME_ANDROID_RESOURCE)
274+
.authority(resources.getResourcePackageName(iconResId))
275+
.appendPath(resources.getResourceTypeName(iconResId))
276+
.appendPath(resources.getResourceEntryName(iconResId))
277+
.build()
278+
)
279+
280+
return MediaBrowserCompat.MediaItem(
281+
builder.build(), MediaBrowserCompat.MediaItem.FLAG_PLAYABLE
282+
)
283+
}
284+
260285
private fun createLocalPlaylistStreamMediaItem(
261286
playlistId: Long,
262287
item: PlaylistStreamEntry,
@@ -301,6 +326,15 @@ class MediaBrowserImpl(
301326
.build().toString()
302327
}
303328

329+
private fun createMediaIdForPlaylistShuffle(
330+
isRemote: Boolean,
331+
playlistId: Long,
332+
): String {
333+
return buildLocalPlaylistItemMediaId(isRemote, playlistId)
334+
.appendPath(ID_SHUFFLE)
335+
.build().toString()
336+
}
337+
304338
private fun createMediaIdForInfoItem(item: InfoItem): String {
305339
return buildInfoItemMediaId(item).build().toString()
306340
}
@@ -346,7 +380,10 @@ class MediaBrowserImpl(
346380
private fun populateLocalPlaylist(playlistId: Long): Single<List<MediaBrowserCompat.MediaItem>> {
347381
val playlist = LocalPlaylistManager(database).getPlaylistStreams(playlistId).firstOrError()
348382
return playlist.map { items ->
349-
items.mapIndexed { index, item ->
383+
val quickActions = if (items.isEmpty()) emptyList() else listOf(
384+
createShuffleAndPlayMediaItem(false, playlistId)
385+
)
386+
quickActions + items.mapIndexed { index, item ->
350387
createLocalPlaylistStreamMediaItem(playlistId, item, index)
351388
}
352389
}
@@ -356,9 +393,12 @@ class MediaBrowserImpl(
356393
return RemotePlaylistManager(database).getPlaylist(playlistId).firstOrError()
357394
.flatMap { ExtractorHelper.getPlaylistInfo(it.serviceId, it.url, false) }
358395
.map {
396+
val quickActions = if (it.relatedItems.isEmpty()) emptyList() else listOf(
397+
createShuffleAndPlayMediaItem(true, playlistId)
398+
)
359399
// ignore it.errors, i.e. ignore errors about specific items, since there would
360400
// be no way to show the error properly in Android Auto anyway
361-
it.relatedItems.mapIndexed { index, item ->
401+
quickActions + it.relatedItems.mapIndexed { index, item ->
362402
createRemotePlaylistStreamMediaItem(playlistId, item, index)
363403
}
364404
}

app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserPlaybackPreparer.kt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,17 @@ class MediaBrowserPlaybackPreparer(
138138
.map { info -> PlaylistPlayQueue(info, index) }
139139
}
140140

141+
private fun extractShufflePlayQueue(isRemote: Boolean, playlistId: Long): Single<PlayQueue> {
142+
return if (isRemote) {
143+
extractRemotePlayQueue(playlistId, 0)
144+
} else {
145+
extractLocalPlayQueue(playlistId, 0)
146+
}.map { playQueue ->
147+
playQueue.shuffle(true)
148+
playQueue
149+
}
150+
}
151+
141152
private fun extractPlayQueueFromMediaId(mediaId: String): Single<PlayQueue> {
142153
try {
143154
val mediaIdUri = mediaId.toUri()
@@ -184,6 +195,10 @@ class MediaBrowserPlaybackPreparer(
184195
throw parseError(mediaId)
185196
}
186197
val playlistId = path[0].toLong()
198+
if (ID_SHUFFLE == path[1]) {
199+
return extractShufflePlayQueue(playlistType == ID_REMOTE, playlistId)
200+
}
201+
187202
val index = path[1].toInt()
188203
return if (playlistType == ID_LOCAL)
189204
extractLocalPlayQueue(playlistId, index)

app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueue.kt

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -399,17 +399,21 @@ abstract class PlayQueue internal constructor(
399399
/**
400400
* Shuffles the current play queue
401401
*
402-
* This method first backs up the existing play queue and item being played. Then a newly
403-
* shuffled play queue will be generated along with currently playing item placed at the
404-
* beginning of the queue. This item will also be added to the history.
402+
* This method first backs up the existing play queue. By default, the currently playing item
403+
* is preserved at the beginning of the queue, with the remaining items shuffled.
404+
* If [shuffleAll] is true, all items in the queue will be shuffled without preserving the
405+
* currently playing item at the head of the queue.
405406
*
406-
* Will emit a [ReorderEvent] if shuffled.
407+
* When the currently playing item is preserved, it will also be added to the history and will
408+
* emit a [ReorderEvent] if the currently playing item position changes.
407409
*
410+
* @param shuffleAll whether to shuffle all items in the queue or preserve the currently
411+
* playing item at the head
408412
* @implNote Does nothing if the queue has a size <= 2 (the currently playing video must stay on
409413
* top, so shuffling a size-2 list does nothing)
410414
*/
411415
@Synchronized
412-
fun shuffle() {
416+
fun shuffle(shuffleAll: Boolean = false) {
413417
// Create a backup if it doesn't already exist
414418
// Note: The backup-list has to be created at all cost (even when size <= 2).
415419
// Otherwise it's not possible to enter shuffle-mode!
@@ -421,13 +425,18 @@ abstract class PlayQueue internal constructor(
421425
return
422426
}
423427

428+
if (shuffleAll) {
429+
streams.shuffle()
430+
return
431+
}
432+
424433
val originalIndex = this.index
425-
val currentItem = this.item
434+
val currentItem = this.item!!
426435

427436
streams.shuffle()
428437

429438
// Move currentItem to the head of the queue
430-
streams.remove(currentItem!!)
439+
streams.remove(currentItem)
431440
streams.add(0, currentItem)
432441
queueIndex.set(0)
433442

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
2+
android:width="24dp"
3+
android:height="24dp"
4+
android:tint="@color/white"
5+
android:viewportWidth="24"
6+
android:viewportHeight="24">
7+
<path
8+
android:fillColor="#FF000000"
9+
android:pathData="M10.59,9.17L5.41,4 4,5.41l5.17,5.17 1.42,-1.41zM14.5,4l2.04,2.04L4,18.59 5.41,20 17.96,7.46 20,9.5L20,4h-5.5zM14.83,13.41l-1.41,1.41 3.13,3.13L14.5,20L20,20v-5.5l-2.04,2.04 -3.13,-3.13z" />
10+
</vector>

app/src/main/res/values/strings.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -666,6 +666,7 @@
666666
<string name="app_language_title">App language</string>
667667
<string name="systems_language">System default</string>
668668
<string name="remove_watched">Remove watched</string>
669+
<string name="shuffle_and_play">Shuffle and play</string>
669670
<string name="remove_watched_popup_title">Remove watched videos?</string>
670671
<string name="remove_duplicates">Remove duplicates</string>
671672
<string name="remove_duplicates_title">Remove duplicates?</string>

0 commit comments

Comments
 (0)