diff --git a/application/vlc-android/src/org/videolan/vlc/MediaSessionCallback.kt b/application/vlc-android/src/org/videolan/vlc/MediaSessionCallback.kt
index c76dcd56f4843ab1b7d5084cb3806c0ff1d21b7a..11eff2a391de2ba27fe417161010394e2faa72d9 100644
--- a/application/vlc-android/src/org/videolan/vlc/MediaSessionCallback.kt
+++ b/application/vlc-android/src/org/videolan/vlc/MediaSessionCallback.kt
@@ -1,6 +1,7 @@
 package org.videolan.vlc
 
 import android.annotation.SuppressLint
+import android.content.ContentUris
 import android.content.Intent
 import android.net.Uri
 import android.os.Bundle
@@ -14,11 +15,13 @@ import kotlinx.coroutines.*
 import org.videolan.medialibrary.interfaces.Medialibrary
 import org.videolan.medialibrary.interfaces.media.MediaWrapper
 import org.videolan.medialibrary.media.MediaLibraryItem
-import org.videolan.resources.AndroidDevices
-import org.videolan.resources.MEDIALIBRARY_PAGE_SIZE
+import org.videolan.resources.*
 import org.videolan.resources.util.getFromMl
 import org.videolan.tools.Settings
+import org.videolan.tools.removeQuery
+import org.videolan.tools.retrieveParent
 import org.videolan.vlc.extensions.ExtensionsManager
+import org.videolan.vlc.gui.helpers.MediaComparators
 import org.videolan.vlc.media.MediaSessionBrowser
 import org.videolan.vlc.util.VoiceSearchParams
 import org.videolan.vlc.util.awaitMedialibraryStarted
@@ -117,59 +120,97 @@ internal class MediaSessionCallback(private val playbackService: PlaybackService
     override fun onPlayFromMediaId(mediaId: String, extras: Bundle?) {
         playbackService.lifecycleScope.launch {
             val context = playbackService.applicationContext
-            when {
-                mediaId == MediaSessionBrowser.ID_NO_MEDIA -> playbackService.displayPlaybackError(R.string.search_no_result)
-                mediaId == MediaSessionBrowser.ID_NO_PLAYLIST -> playbackService.displayPlaybackError(R.string.noplaylist)
-                mediaId == MediaSessionBrowser.ID_SHUFFLE_ALL -> {
-                    val tracks = context.getFromMl { audio }
-                    if (tracks.isNotEmpty() && isActive) {
-                        loadMedia(tracks.toList(), Random().nextInt(min(tracks.size, MEDIALIBRARY_PAGE_SIZE)))
-                        if (!playbackService.isShuffling) playbackService.shuffle()
-                    } else {
-                        playbackService.displayPlaybackError(R.string.search_no_result)
-                    }
-                }
-                mediaId == MediaSessionBrowser.ID_LAST_ADDED -> {
-                    val tracks = context.getFromMl { getPagedAudio(Medialibrary.SORT_INSERTIONDATE, true, false, MediaSessionBrowser.MAX_HISTORY_SIZE, 0) }
-                    if (tracks.isNotEmpty() && isActive) {
-                        loadMedia(tracks.toList())
-                    }
-                }
-                mediaId == MediaSessionBrowser.ID_HISTORY -> {
-                    val tracks = context.getFromMl { lastMediaPlayed()?.toList()?.filter { MediaSessionBrowser.isMediaAudio(it) } }
-                    if (!tracks.isNullOrEmpty() && isActive) {
-                        val mediaList = tracks.subList(0, tracks.size.coerceAtMost(MediaSessionBrowser.MAX_HISTORY_SIZE))
-                        loadMedia(mediaList)
-                    }
-                }
-                mediaId.startsWith(MediaSessionBrowser.ALBUM_PREFIX) -> {
-                    val tracks = context.getFromMl { getAlbum(mediaId.extractId())?.tracks }
-                    if (isActive) tracks?.let { loadMedia(it.toList()) }
-                }
-                mediaId.startsWith(MediaSessionBrowser.ARTIST_PREFIX) -> {
-                    val tracks = context.getFromMl { getArtist(mediaId.extractId())?.tracks }
-                    if (isActive) tracks?.let { loadMedia(it.toList()) }
-                }
-                mediaId.startsWith(MediaSessionBrowser.GENRE_PREFIX) -> {
-                    val tracks = context.getFromMl { getGenre(mediaId.extractId())?.albums?.flatMap { it.tracks.toList() } }
-                    if (isActive) tracks?.let { loadMedia(it.toList()) }
-                }
-                mediaId.startsWith(MediaSessionBrowser.PLAYLIST_PREFIX) -> {
-                    val tracks = context.getFromMl { getPlaylist(mediaId.extractId(), Settings.includeMissing)?.tracks }
-                    if (isActive) tracks?.let { loadMedia(it.toList()) }
-                }
-                mediaId.startsWith(MediaSessionBrowser.SEARCH_PREFIX) -> {
-                    val tracks = context.getFromMl { search(mediaId.extractParam(), false)?.tracks }
-                    if (isActive) tracks?.let { loadMedia(it.toList()) }
-                }
-                mediaId.startsWith(ExtensionsManager.EXTENSION_PREFIX) -> {
+            try {
+                if (mediaId.startsWith(ExtensionsManager.EXTENSION_PREFIX)) {
                     val id = mediaId.replace(ExtensionsManager.EXTENSION_PREFIX + "_" + mediaId.split("_".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()[1] + "_", "")
                     onPlayFromUri(id.toUri(), null)
+                } else {
+                    val mediaIdUri = Uri.parse(mediaId)
+                    val position = mediaIdUri.getQueryParameter("i")?.toInt() ?: 0
+                    val page = mediaIdUri.getQueryParameter("p")
+                    val pageOffset = page?.toInt()?.times(MediaSessionBrowser.MAX_RESULT_SIZE) ?: 0
+                    when (mediaIdUri.removeQuery().toString()) {
+                        MediaSessionBrowser.ID_NO_MEDIA -> playbackService.displayPlaybackError(R.string.search_no_result)
+                        MediaSessionBrowser.ID_NO_PLAYLIST -> playbackService.displayPlaybackError(R.string.noplaylist)
+                        MediaSessionBrowser.ID_SHUFFLE_ALL -> {
+                            val tracks = context.getFromMl { audio }
+                            if (tracks.isNotEmpty() && isActive) {
+                                tracks.sortWith(MediaComparators.ANDROID_AUTO)
+                                loadMedia(tracks.toList(), Random().nextInt(min(tracks.size, MEDIALIBRARY_PAGE_SIZE)))
+                                if (!playbackService.isShuffling) playbackService.shuffle()
+                            } else {
+                                playbackService.displayPlaybackError(R.string.search_no_result)
+                            }
+                        }
+                        MediaSessionBrowser.ID_LAST_ADDED -> {
+                            val tracks = context.getFromMl { getPagedAudio(Medialibrary.SORT_INSERTIONDATE, true, false, MediaSessionBrowser.MAX_HISTORY_SIZE, 0) }
+                            if (tracks.isNotEmpty() && isActive) {
+                                loadMedia(tracks.toList(), position)
+                            }
+                        }
+                        MediaSessionBrowser.ID_HISTORY -> {
+                            val tracks = context.getFromMl { lastMediaPlayed()?.toList()?.filter { MediaSessionBrowser.isMediaAudio(it) } }
+                            if (!tracks.isNullOrEmpty() && isActive) {
+                                val mediaList = tracks.subList(0, tracks.size.coerceAtMost(MediaSessionBrowser.MAX_HISTORY_SIZE))
+                                loadMedia(mediaList, position)
+                            }
+                        }
+                        MediaSessionBrowser.ID_STREAM -> {
+                            val tracks = context.getFromMl { lastStreamsPlayed() }
+                            if (tracks.isNotEmpty() && isActive) {
+                                tracks.sortWith(MediaComparators.ANDROID_AUTO)
+                                loadMedia(tracks.toList(), position)
+                            }
+                        }
+                        MediaSessionBrowser.ID_TRACK -> {
+                            val tracks = context.getFromMl { audio }
+                            if (tracks.isNotEmpty() && isActive) {
+                                tracks.sortWith(MediaComparators.ANDROID_AUTO)
+                                loadMedia(tracks.toList(), pageOffset + position)
+                            }
+                        }
+                        MediaSessionBrowser.ID_SEARCH -> {
+                            val query = mediaIdUri.getQueryParameter("query") ?: ""
+                            val tracks = context.getFromMl {
+                                search(query, false)?.tracks?.toList() ?: emptyList()
+                            }
+                            if (tracks.isNotEmpty() && isActive) {
+                                loadMedia(tracks, position)
+                            }
+                        }
+                        else -> {
+                            val id = ContentUris.parseId(mediaIdUri)
+                            when (mediaIdUri.retrieveParent().toString()) {
+                                MediaSessionBrowser.ID_ALBUM -> {
+                                    val tracks = context.getFromMl { getAlbum(id)?.tracks }
+                                    if (isActive) tracks?.let { loadMedia(it.toList(), position) }
+                                }
+                                MediaSessionBrowser.ID_ARTIST -> {
+                                    val tracks = context.getFromMl { getArtist(id)?.tracks }
+                                    if (isActive) tracks?.let { loadMedia(it.toList()) }
+                                }
+                                MediaSessionBrowser.ID_GENRE -> {
+                                    val tracks = context.getFromMl { getGenre(id)?.albums?.flatMap { it.tracks.toList() } }
+                                    if (isActive) tracks?.let { loadMedia(it.toList()) }
+                                }
+                                MediaSessionBrowser.ID_PLAYLIST -> {
+                                    val tracks = context.getFromMl { getPlaylist(id, Settings.includeMissing)?.tracks }
+                                    if (isActive) tracks?.let { loadMedia(it.toList()) }
+                                }
+                                MediaSessionBrowser.ID_MEDIA -> {
+                                    val tracks = context.getFromMl { getMedia(id)?.tracks }
+                                    if (isActive) tracks?.let { loadMedia(it.toList()) }
+                                }
+                                else -> throw IllegalStateException("Failed to load: $mediaId")
+                            }
+                        }
+                    }
                 }
-                else -> try {
-                    context.getFromMl { getMedia(mediaId.toLong()) }?.let { if (isActive) loadMedia(listOf(it)) }
-                } catch (e: NumberFormatException) {
-                    if (isActive) playbackService.loadLocation(mediaId)
+            } catch (e: Exception) {
+                Log.e(TAG, "Could not play media: $mediaId", e)
+                when {
+                    playbackService.hasMedia() -> playbackService.play()
+                    else -> playbackService.displayPlaybackError(R.string.search_no_result)
                 }
             }
         }
@@ -183,10 +224,6 @@ internal class MediaSessionCallback(private val playbackService: PlaybackService
         }
     }
 
-    private fun String.extractId() = extractParam().toLong()
-
-    private fun String.extractParam() = split("_".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()[1]
-
     override fun onPlayFromUri(uri: Uri?, extras: Bundle?) = playbackService.loadUri(uri)
 
     override fun onPlayFromSearch(query: String?, extras: Bundle?) {
diff --git a/application/vlc-android/src/org/videolan/vlc/media/MediaSessionBrowser.kt b/application/vlc-android/src/org/videolan/vlc/media/MediaSessionBrowser.kt
index 70bb1c239f430ddada045446b5d799d752777a08..71c39570366124ae87eebf1eb506dddcf1669e83 100644
--- a/application/vlc-android/src/org/videolan/vlc/media/MediaSessionBrowser.kt
+++ b/application/vlc-android/src/org/videolan/vlc/media/MediaSessionBrowser.kt
@@ -23,10 +23,7 @@
  */
 package org.videolan.vlc.media
 
-import android.content.ComponentName
-import android.content.Context
-import android.content.Intent
-import android.content.ServiceConnection
+import android.content.*
 import android.content.pm.PackageManager
 import android.content.res.Resources
 import android.graphics.Bitmap
@@ -44,9 +41,7 @@ import org.videolan.medialibrary.interfaces.media.MediaWrapper
 import org.videolan.medialibrary.media.MediaLibraryItem
 import org.videolan.resources.*
 import org.videolan.resources.AppContextProvider.appContext
-import org.videolan.tools.KEY_ARTISTS_SHOW_ALL
-import org.videolan.tools.PLAYBACK_HISTORY
-import org.videolan.tools.Settings
+import org.videolan.tools.*
 import org.videolan.vlc.ArtworkProvider
 import org.videolan.vlc.BuildConfig
 import org.videolan.vlc.R
@@ -54,6 +49,8 @@ import org.videolan.vlc.extensions.ExtensionManagerService
 import org.videolan.vlc.extensions.ExtensionManagerService.ExtensionManagerActivity
 import org.videolan.vlc.extensions.ExtensionsManager
 import org.videolan.vlc.extensions.api.VLCExtensionItem
+import org.videolan.vlc.gui.helpers.MediaComparators
+import org.videolan.vlc.gui.helpers.MediaComparators.formatArticles
 import org.videolan.vlc.gui.helpers.UiTools.getDefaultAudioDrawable
 import org.videolan.vlc.gui.helpers.getBitmapFromDrawable
 import org.videolan.vlc.isPathValid
@@ -66,6 +63,46 @@ import org.videolan.vlc.util.isSchemeStreaming
 import java.util.*
 import java.util.concurrent.Semaphore
 
+/**
+ * The mediaId used in the media session browser is defined as an opaque string token which is left
+ * up to the application developer to define. In practicality, mediaIds from multiple applications
+ * may be combined into a single data structure, so we use a valid uri, and have have intentionally
+ * prefixed it with a namespace. The value is stored as a string to avoid repeated type conversion;
+ * however, it may be parsed by the uri class as needed. The uri starts with two forward slashes to
+ * disambiguate the authority from the path, per RFC 3986, section 3.
+ *
+ * The mediaId structure is documented below for reference. The first (or second) letter of each
+ * section is used in lieu of the entire word in order to shorten the id throughout the library.
+ * The reduction of space consumed by the mediaId enables an increased number of records per page.
+ *
+ * Root node
+ * //org.videolan.vlc/{r}oot
+ * Root menu
+ * //org.videolan.vlc/{r}oot/home
+ * //org.videolan.vlc/{r}oot/playlist/<id>
+ * //org.videolan.vlc/{r}oot/{l}ib
+ * //org.videolan.vlc/{r}oot/stream
+ * Home menu
+ * //org.videolan.vlc/{r}oot/home/shuffle_all
+ * //org.videolan.vlc/{r}oot/home/last_added[?{i}ndex=<track num>]
+ * //org.videolan.vlc/{r}oot/home/history[?{i}ndex=<track num>]
+ * Library menu
+ * //org.videolan.vlc/{r}oot/{l}ib/a{r}tist[?{p}age=<page num>]
+ * //org.videolan.vlc/{r}oot/{l}ib/a{r}tist/<id>
+ * //org.videolan.vlc/{r}oot/{l}ib/a{l}bum[?{p}age=<page num>]
+ * //org.videolan.vlc/{r}oot/{l}ib/a{l}bum/<id>
+ * //org.videolan.vlc/{r}oot/{l}ib/{t}rack[?{p}age=<page num>]
+ * //org.videolan.vlc/{r}oot/{l}ib/{t}rack[?{p}age=<page num>][&{i}ndex=<track num>]
+ * //org.videolan.vlc/{r}oot/{l}ib/{g}enre[?{p}age=<page num>]
+ * //org.videolan.vlc/{r}oot/{l}ib/{g}enre/<id>
+ * Media
+ * //org.videolan.vlc/media/<id>
+ * Errors
+ * //org.videolan.vlc/error/media
+ * //org.videolan.vlc/error/playlist
+ * Search
+ * //org.videolan.vlc/search?query=<query>
+ */
 class MediaSessionBrowser : ExtensionManagerActivity {
     override fun displayExtensionItems(extensionId: Int, title: String, items: List<VLCExtensionItem>, showParams: Boolean, isRefresh: Boolean) {
         if (showParams && items.size == 1 && items[0].getType() == VLCExtensionItem.TYPE_DIRECTORY) {
@@ -109,29 +146,35 @@ class MediaSessionBrowser : ExtensionManagerActivity {
         private val DEFAULT_PLAYALL_ICON = "${BASE_DRAWABLE_URI}/${R.drawable.ic_auto_playall}".toUri()
         val DEFAULT_TRACK_ICON = "${BASE_DRAWABLE_URI}/${R.drawable.ic_auto_nothumb}".toUri()
         private val instance = MediaSessionBrowser()
-        const val ID_ROOT = "ID_ROOT"
-        private const val ID_ARTISTS = "ID_ARTISTS"
-        private const val ID_ALBUMS = "ID_ALBUMS"
-        private const val ID_TRACKS = "ID_TRACKS"
-        private const val ID_GENRES = "ID_GENRES"
-        private const val ID_PLAYLISTS = "ID_PLAYLISTS"
-        private const val ID_HOME = "ID_HOME"
-        const val ID_HISTORY = "ID_HISTORY"
-        const val ID_LAST_ADDED = "ID_RECENT"
-        private const val ID_STREAMS = "ID_STREAMS"
-        private const val ID_LIBRARY = "ID_LIBRARY"
-        const val ID_SHUFFLE_ALL = "ID_SHUFFLE_ALL"
-        const val ID_NO_MEDIA = "ID_NO_MEDIA"
-        const val ID_NO_PLAYLIST = "ID_NO_PLAYLIST"
-        const val ALBUM_PREFIX = "album"
-        const val ARTIST_PREFIX = "artist"
-        const val GENRE_PREFIX = "genre"
-        const val PLAYLIST_PREFIX = "playlist"
-        const val SEARCH_PREFIX = "search"
+
+        // Root item
+        // MediaIds are all strings. Maintain in uri parsable format.
+        const val ID_ROOT = "//${BuildConfig.APP_ID}/r"
+        const val ID_MEDIA = "$ID_ROOT/media"
+        const val ID_SEARCH = "$ID_ROOT/search"
+        const val ID_NO_MEDIA = "$ID_ROOT/error/media"
+        const val ID_NO_PLAYLIST = "$ID_ROOT/error/playlist"
+
+        // Top-level menu
+        private const val ID_HOME = "$ID_ROOT/home"
+        const val ID_PLAYLIST = "$ID_ROOT/playlist"
+        private const val ID_LIBRARY = "$ID_ROOT/l"
+        const val ID_STREAM = "$ID_ROOT/stream"
+
+        // Home menu
+        const val ID_SHUFFLE_ALL = "$ID_HOME/shuffle_all"
+        const val ID_LAST_ADDED = "$ID_HOME/last_added"
+        const val ID_HISTORY = "$ID_HOME/history"
+
+        // Library menu
+        const val ID_ARTIST = "$ID_LIBRARY/r"
+        const val ID_ALBUM = "$ID_LIBRARY/l"
+        const val ID_TRACK = "$ID_LIBRARY/t"
+        const val ID_GENRE = "$ID_LIBRARY/g"
         const val MAX_HISTORY_SIZE = 100
         const val MAX_COVER_ART_ITEMS = 50
         private const val MAX_EXTENSION_SIZE = 100
-        private const val MAX_RESULT_SIZE = 800
+        const val MAX_RESULT_SIZE = 800
 
         // Extensions management
         private var extensionServiceConnection: ServiceConnection? = null
@@ -145,7 +188,6 @@ class MediaSessionBrowser : ExtensionManagerActivity {
             var list: Array<out MediaLibraryItem>? = null
             var limitSize = false
             val res = context.resources
-
             //Extensions
             if (parentId.startsWith(ExtensionsManager.EXTENSION_PREFIX)) {
                 if (extensionServiceConnection == null) {
@@ -176,7 +218,10 @@ class MediaSessionBrowser : ExtensionManagerActivity {
                 results = extensionItems
             } else {
                 val ml = Medialibrary.getInstance()
-                when (parentId) {
+                val parentIdUri = parentId.toUri()
+                val page = parentIdUri.getQueryParameter("p")
+                val pageOffset = page?.toInt()?.times(MAX_RESULT_SIZE) ?: 0
+                when (parentIdUri.removeQuery().toString()) {
                     ID_ROOT -> {
                         //List of Extensions
                         val extensions = ExtensionsManager.getInstance().getExtensions(context, true)
@@ -217,7 +262,7 @@ class MediaSessionBrowser : ExtensionManagerActivity {
                         results.add(MediaBrowserCompat.MediaItem(homeMediaDesc, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE))
                         //Playlists
                         val playlistMediaDesc = MediaDescriptionCompat.Builder()
-                                .setMediaId(ID_PLAYLISTS)
+                                .setMediaId(ID_PLAYLIST)
                                 .setTitle(res.getString(R.string.playlists))
                                 .setIconUri("${BASE_DRAWABLE_URI}/${R.drawable.ic_auto_playlist}".toUri())
                                 .setExtras(getContentStyle(CONTENT_STYLE_GRID_ITEM_HINT_VALUE, CONTENT_STYLE_GRID_ITEM_HINT_VALUE))
@@ -232,7 +277,7 @@ class MediaSessionBrowser : ExtensionManagerActivity {
                         results.add(MediaBrowserCompat.MediaItem(libraryMediaDesc, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE))
                         //Streams
                         val streamsMediaDesc = MediaDescriptionCompat.Builder()
-                                .setMediaId(ID_STREAMS)
+                                .setMediaId(ID_STREAM)
                                 .setTitle(res.getString(R.string.streams))
                                 .setIconUri("${BASE_DRAWABLE_URI}/${R.drawable.ic_auto_stream}".toUri())
                                 .build()
@@ -288,69 +333,82 @@ class MediaSessionBrowser : ExtensionManagerActivity {
                     ID_LIBRARY -> {
                         //Artists
                         val artistsMediaDesc = MediaDescriptionCompat.Builder()
-                                .setMediaId(ID_ARTISTS)
+                                .setMediaId(ID_ARTIST)
                                 .setTitle(res.getString(R.string.artists))
                                 .setIconUri(MENU_ARTIST_ICON)
                                 .build()
                         results.add(MediaBrowserCompat.MediaItem(artistsMediaDesc, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE))
                         //Albums
                         val albumsMediaDesc = MediaDescriptionCompat.Builder()
-                                .setMediaId(ID_ALBUMS)
+                                .setMediaId(ID_ALBUM)
                                 .setTitle(res.getString(R.string.albums))
                                 .setIconUri(MENU_ALBUM_ICON)
-                                .setExtras(getContentStyle(CONTENT_STYLE_GRID_ITEM_HINT_VALUE, CONTENT_STYLE_LIST_ITEM_HINT_VALUE))
+                                .setExtras(if (ml.albumsCount <= MAX_RESULT_SIZE) getContentStyle(CONTENT_STYLE_GRID_ITEM_HINT_VALUE, CONTENT_STYLE_LIST_ITEM_HINT_VALUE) else null)
                                 .build()
                         results.add(MediaBrowserCompat.MediaItem(albumsMediaDesc, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE))
                         //Tracks
                         val tracksMediaDesc = MediaDescriptionCompat.Builder()
-                                .setMediaId(ID_TRACKS)
+                                .setMediaId(ID_TRACK)
                                 .setTitle(res.getString(R.string.tracks))
                                 .setIconUri(MENU_AUDIO_ICON)
                                 .build()
                         results.add(MediaBrowserCompat.MediaItem(tracksMediaDesc, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE))
                         //Genres
                         val genresMediaDesc = MediaDescriptionCompat.Builder()
-                                .setMediaId(ID_GENRES)
+                                .setMediaId(ID_GENRE)
                                 .setTitle(res.getString(R.string.genres))
                                 .setIconUri(MENU_GENRE_ICON)
                                 .build()
                         results.add(MediaBrowserCompat.MediaItem(genresMediaDesc, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE))
                         return results
                     }
-                    ID_ARTISTS -> list = ml.getArtists(Settings.getInstance(context).getBoolean(KEY_ARTISTS_SHOW_ALL, false), false)
-                    ID_ALBUMS -> list = ml.getAlbums(false)
-                    ID_GENRES -> list = ml.getGenres(false)
-                    ID_TRACKS -> list = ml.audio
-                    ID_PLAYLISTS -> list = ml.playlists
-                    ID_STREAMS -> list = ml.lastStreamsPlayed()
+                    ID_ARTIST -> {
+                        val artistsShowAll = Settings.getInstance(context).getBoolean(KEY_ARTISTS_SHOW_ALL, false)
+                        val artists = ml.getArtists(artistsShowAll, Medialibrary.SORT_ALPHA, false, false)
+                        artists.sortWith(MediaComparators.ANDROID_AUTO)
+                        if (page == null && artists.size > MAX_RESULT_SIZE) return paginateLibrary(artists, parentIdUri, MENU_ARTIST_ICON)
+                        list = artists.copyOfRange(pageOffset.coerceAtMost(artists.size), (pageOffset + MAX_RESULT_SIZE).coerceAtMost(artists.size))
+                    }
+                    ID_ALBUM -> {
+                        val albums = ml.getAlbums(Medialibrary.SORT_ALPHA, false, false)
+                        albums.sortWith(MediaComparators.ANDROID_AUTO)
+                        if (page == null && albums.size > MAX_RESULT_SIZE) return paginateLibrary(albums, parentIdUri, MENU_ALBUM_ICON,
+                                    getContentStyle(CONTENT_STYLE_GRID_ITEM_HINT_VALUE, CONTENT_STYLE_LIST_ITEM_HINT_VALUE))
+                        list = albums.copyOfRange(pageOffset.coerceAtMost(albums.size), (pageOffset + MAX_RESULT_SIZE).coerceAtMost(albums.size))
+                    }
+                    ID_TRACK -> {
+                        val tracks = ml.getAudio(Medialibrary.SORT_ALPHA, false, false)
+                        tracks.sortWith(MediaComparators.ANDROID_AUTO)
+                        if (page == null && tracks.size > MAX_RESULT_SIZE) return paginateLibrary(tracks, parentIdUri, MENU_AUDIO_ICON)
+                        list = tracks.copyOfRange(pageOffset.coerceAtMost(tracks.size), (pageOffset + MAX_RESULT_SIZE).coerceAtMost(tracks.size))
+                    }
+                    ID_GENRE -> {
+                        val genres = ml.getGenres(Medialibrary.SORT_ALPHA, false, false)
+                        genres.sortWith(MediaComparators.ANDROID_AUTO)
+                        if (page == null && genres.size > MAX_RESULT_SIZE) return paginateLibrary(genres, parentIdUri, MENU_GENRE_ICON)
+                        list = genres.copyOfRange(pageOffset.coerceAtMost(genres.size), (pageOffset + MAX_RESULT_SIZE).coerceAtMost(genres.size))
+                    }
+                    ID_PLAYLIST -> {
+                        list = ml.playlists
+                        list.sortWith(MediaComparators.ANDROID_AUTO)
+                    }
+                    ID_STREAM -> {
+                        list = ml.lastStreamsPlayed()
+                        list.sortWith(MediaComparators.ANDROID_AUTO)
+                    }
                     ID_LAST_ADDED -> {
                         limitSize = true
                         list = ml.getPagedAudio(Medialibrary.SORT_INSERTIONDATE, true, false, MAX_HISTORY_SIZE, 0)
-                        if (list != null && list.size > 1) {
-                            val playAllMediaDesc = getPlayAllBuilder(res, parentId, list.size).build()
-                            results.add(MediaBrowserCompat.MediaItem(playAllMediaDesc, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE))
-                        }
                     }
                     ID_HISTORY -> {
                         limitSize = true
                         list = ml.lastMediaPlayed()?.toList()?.filter { isMediaAudio(it) }?.toTypedArray()
-                        if (list != null && list.size > 1) {
-                            val playAllMediaDesc = getPlayAllBuilder(res, parentId, list.size.coerceAtMost(MAX_HISTORY_SIZE)).build()
-                            results.add(MediaBrowserCompat.MediaItem(playAllMediaDesc, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE))
-                        }
                     }
                     else -> {
-                        val idSections = parentId.split("_").toTypedArray()
-                        val id = idSections[1].toLong()
-                        when (idSections[0]) {
-                            ALBUM_PREFIX -> {
-                                list = ml.getAlbum(id).tracks
-                                if (list != null && list.size > 1) {
-                                    val playAllMediaDesc = getPlayAllBuilder(res, parentId, list.size).build()
-                                    results.add(MediaBrowserCompat.MediaItem(playAllMediaDesc, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE))
-                                }
-                            }
-                            ARTIST_PREFIX -> {
+                        val id = ContentUris.parseId(parentIdUri)
+                        when (parentIdUri.retrieveParent().toString()) {
+                            ID_ALBUM -> list = ml.getAlbum(id).tracks
+                            ID_ARTIST -> {
                                 val artist = ml.getArtist(id)
                                 list = artist.albums
                                 if (list != null && list.size > 1) {
@@ -367,17 +425,18 @@ class MediaSessionBrowser : ExtensionManagerActivity {
                                     results.add(MediaBrowserCompat.MediaItem(playAllMediaDesc, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE))
                                 }
                             }
-                            GENRE_PREFIX -> {
+                            ID_GENRE -> {
                                 val genre = ml.getGenre(id)
                                 list = genre.albums
+                                val tracksCount = list.sumOf { it.tracksCount }
                                 if (list != null && list.size > 1) {
                                     val playAllPath = Uri.Builder()
                                             .appendPath(ArtworkProvider.PLAY_ALL)
                                             .appendPath(ArtworkProvider.GENRE)
-                                            .appendPath("${genre.tracksCount}")
+                                            .appendPath("$tracksCount")
                                             .appendPath("$id")
                                             .build()
-                                    val playAllMediaDesc = getPlayAllBuilder(res, parentId, genre.tracksCount, playAllPath).build()
+                                    val playAllMediaDesc = getPlayAllBuilder(res, parentId, tracksCount, playAllPath).build()
                                     results.add(MediaBrowserCompat.MediaItem(playAllMediaDesc, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE))
                                 }
                             }
@@ -392,14 +451,14 @@ class MediaSessionBrowser : ExtensionManagerActivity {
                         .setIconUri(DEFAULT_TRACK_ICON)
                         .setTitle(context.getString(R.string.search_no_result))
                 when (parentId) {
-                    ID_ARTISTS -> emptyMediaDesc.setIconUri(DEFAULT_ARTIST_ICON)
-                    ID_ALBUMS -> emptyMediaDesc.setIconUri(DEFAULT_ALBUM_ICON)
-                    ID_GENRES -> emptyMediaDesc.setIconUri(null)
-                    ID_PLAYLISTS -> {
+                    ID_ARTIST -> emptyMediaDesc.setIconUri(DEFAULT_ARTIST_ICON)
+                    ID_ALBUM -> emptyMediaDesc.setIconUri(DEFAULT_ALBUM_ICON)
+                    ID_GENRE -> emptyMediaDesc.setIconUri(null)
+                    ID_PLAYLIST -> {
                         emptyMediaDesc.setMediaId(ID_NO_PLAYLIST)
                         emptyMediaDesc.setTitle(context.getString(R.string.noplaylist))
                     }
-                    ID_STREAMS -> emptyMediaDesc.setIconUri(DEFAULT_STREAM_ICON)
+                    ID_STREAM -> emptyMediaDesc.setIconUri(DEFAULT_STREAM_ICON)
                 }
                 results.add(MediaBrowserCompat.MediaItem(emptyMediaDesc.build(), MediaBrowserCompat.MediaItem.FLAG_PLAYABLE))
             }
@@ -417,20 +476,11 @@ class MediaSessionBrowser : ExtensionManagerActivity {
             val res = context.resources
             val results: MutableList<MediaBrowserCompat.MediaItem> = ArrayList()
             val searchAggregate = Medialibrary.getInstance().search(query, false)
-            results.addAll(buildMediaItems(context, ID_PLAYLISTS, searchAggregate.playlists, res.getString(R.string.playlists)))
-            results.addAll(buildMediaItems(context, ARTIST_PREFIX, searchAggregate.artists, res.getString(R.string.artists)))
-            results.addAll(buildMediaItems(context, ALBUM_PREFIX, searchAggregate.albums, res.getString(R.string.albums)))
-            val trackLst = buildMediaItems(context, ID_TRACKS, searchAggregate.tracks, res.getString(R.string.tracks))
-            if (trackLst.size > 1) {
-                val extras = Bundle().apply {
-                    putString(EXTRA_CONTENT_STYLE_GROUP_TITLE_HINT, res.getString(R.string.tracks))
-                }
-                val playAllMediaDesc = getPlayAllBuilder(res, SEARCH_PREFIX + "_$query", trackLst.size)
-                        .setExtras(extras)
-                        .build()
-                results.add(MediaBrowserCompat.MediaItem(playAllMediaDesc, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE))
-            }
-            if (trackLst.isNotEmpty()) results.addAll(trackLst)
+            val searchMediaId = ID_SEARCH.toUri().buildUpon().appendQueryParameter("query", query).toString()
+            results.addAll(buildMediaItems(context, ID_PLAYLIST, searchAggregate.playlists, res.getString(R.string.playlists)))
+            results.addAll(buildMediaItems(context, ID_ARTIST, searchAggregate.artists, res.getString(R.string.artists)))
+            results.addAll(buildMediaItems(context, ID_ALBUM, searchAggregate.albums, res.getString(R.string.albums)))
+            results.addAll(buildMediaItems(context, searchMediaId, searchAggregate.tracks, res.getString(R.string.tracks)))
             if (results.isEmpty()) {
                 val emptyMediaDesc = MediaDescriptionCompat.Builder()
                         .setMediaId(ID_NO_MEDIA)
@@ -462,7 +512,8 @@ class MediaSessionBrowser : ExtensionManagerActivity {
             val results: ArrayList<MediaBrowserCompat.MediaItem> = ArrayList()
             results.ensureCapacity(list.size.coerceAtMost(MAX_RESULT_SIZE))
             /* Iterate over list */
-            for (libraryItem in list) {
+            val parentIdUri = parentId.toUri()
+            for ((index, libraryItem) in list.withIndex()) {
                 if (libraryItem.itemType == MediaLibraryItem.TYPE_MEDIA
                         && ((libraryItem as MediaWrapper).type == MediaWrapper.TYPE_STREAM || isSchemeStreaming(libraryItem.uri.scheme))) {
                     libraryItem.type = MediaWrapper.TYPE_STREAM
@@ -470,7 +521,10 @@ class MediaSessionBrowser : ExtensionManagerActivity {
                     continue
 
                 /* Media ID */
-                val mediaId = generateMediaId(libraryItem)
+                val mediaId = when (libraryItem.itemType) {
+                    MediaLibraryItem.TYPE_MEDIA -> parentIdUri.buildUpon().appendQueryParameter("i", "$index").toString()
+                    else -> generateMediaId(libraryItem)
+                }
 
                 /* Subtitle */
                 val subtitle = when (libraryItem.itemType) {
@@ -478,7 +532,7 @@ class MediaSessionBrowser : ExtensionManagerActivity {
                         val media = libraryItem as MediaWrapper
                         when {
                             media.type == MediaWrapper.TYPE_STREAM -> media.uri.toString()
-                            parentId.startsWith(ALBUM_PREFIX) -> getMediaSubtitle(media)
+                            parentId.startsWith(ID_ALBUM) -> getMediaSubtitle(media)
                             else -> getMediaDescription(getMediaArtist(context, media), getMediaAlbum(context, media))
                         }
                     }
@@ -492,7 +546,7 @@ class MediaSessionBrowser : ExtensionManagerActivity {
                         res.getQuantityString(R.plurals.albums_quantity, albumsCount, albumsCount)
                     }
                     MediaLibraryItem.TYPE_ALBUM -> {
-                        if (parentId.startsWith(ARTIST_PREFIX))
+                        if (parentId.startsWith(ID_ARTIST))
                             res.getString(R.string.track_number, libraryItem.tracksCount)
                         else
                             libraryItem.description
@@ -585,19 +639,61 @@ class MediaSessionBrowser : ExtensionManagerActivity {
 
         fun generateMediaId(libraryItem: MediaLibraryItem): String {
             val prefix = when (libraryItem.itemType) {
-                MediaLibraryItem.TYPE_ALBUM -> ALBUM_PREFIX
-                MediaLibraryItem.TYPE_ARTIST -> ARTIST_PREFIX
-                MediaLibraryItem.TYPE_GENRE -> GENRE_PREFIX
-                MediaLibraryItem.TYPE_PLAYLIST -> PLAYLIST_PREFIX
-                else -> return libraryItem.id.toString()
+                MediaLibraryItem.TYPE_ALBUM -> ID_ALBUM
+                MediaLibraryItem.TYPE_ARTIST -> ID_ARTIST
+                MediaLibraryItem.TYPE_GENRE -> ID_GENRE
+                MediaLibraryItem.TYPE_PLAYLIST -> ID_PLAYLIST
+                else -> ID_MEDIA
             }
-            return "${prefix}_${libraryItem.id}"
+            return "${prefix}/${libraryItem.id}"
         }
 
         fun isMediaAudio(libraryItem: MediaLibraryItem): Boolean {
             return libraryItem.itemType == MediaLibraryItem.TYPE_MEDIA && (libraryItem as MediaWrapper).type == MediaWrapper.TYPE_AUDIO
         }
 
+        /**
+         * At present Android Auto has no ability to directly handle paging so we must limit the size of the result
+         * to avoid returning a parcel which exceeds the size limitations. We break the results into another
+         * layer of browsable drill-downs labeled "start - finish" for each entry type.
+         */
+        private fun paginateLibrary(mediaList: Array<out MediaLibraryItem>, parentIdUri: Uri, iconUri: Uri, extras: Bundle? = null): List<MediaBrowserCompat.MediaItem> {
+            val results: MutableList<MediaBrowserCompat.MediaItem> = ArrayList()
+            /* Build menu items per group */
+            for (page in 0..(mediaList.size / MAX_RESULT_SIZE)) {
+                val offset = (page * MAX_RESULT_SIZE)
+                val lastOffset = (offset + MAX_RESULT_SIZE - 1).coerceAtMost(mediaList.size - 1)
+                if (offset >= lastOffset) break
+                val mediaDesc = MediaDescriptionCompat.Builder()
+                        .setTitle(buildRangeLabel(mediaList[offset].title, mediaList[lastOffset].title))
+                        .setMediaId(parentIdUri.buildUpon().appendQueryParameter("p", "$page").toString())
+                        .setIconUri(iconUri)
+                        .setExtras(extras)
+                        .build()
+                results.add(MediaBrowserCompat.MediaItem(mediaDesc, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE))
+                if (results.size == MAX_RESULT_SIZE) break
+            }
+            return results
+        }
+
+        private fun buildRangeLabel(firstTitle: String, lastTitle: String): String {
+            val beginTitle = formatArticles(firstTitle, true)
+            val endTitle = formatArticles(lastTitle, true)
+            var beginTitleSize = beginTitle.length
+            var endTitleSize = endTitle.length
+            val halfLabelSize = 10
+            val maxLabelSize = 20
+            if (beginTitleSize > halfLabelSize && endTitleSize > halfLabelSize) {
+                beginTitleSize = halfLabelSize
+                endTitleSize = halfLabelSize
+            } else if (beginTitleSize > halfLabelSize) {
+                beginTitleSize = (maxLabelSize - endTitleSize).coerceAtMost(beginTitleSize)
+            } else if (endTitleSize > halfLabelSize) {
+                endTitleSize = (maxLabelSize - beginTitleSize).coerceAtMost(endTitleSize)
+            }
+            return "${beginTitle.abbreviate(beginTitleSize).markBidi()} â‹… ${endTitle.abbreviate(endTitleSize).markBidi()}"
+        }
+
         private fun getPlayAllBuilder(res: Resources, mediaId: String, trackCount: Int, uri: Uri? = null): MediaDescriptionCompat.Builder {
             return MediaDescriptionCompat.Builder()
                     .setMediaId(mediaId)