diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListModel.kt index 5a6e1ba8..d58e7662 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListModel.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListModel.kt @@ -18,7 +18,7 @@ class AlbumListModel(application: Application) : GenericListModel(application) { fun getAlbumList( refresh: Boolean, - swipe: SwipeRefreshLayout, + swipe: SwipeRefreshLayout?, args: Bundle ): LiveData> { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListModel.kt index d58e97ca..63e9e907 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListModel.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListModel.kt @@ -30,12 +30,12 @@ import org.moire.ultrasonic.service.MusicService * Provides ViewModel which contains the list of available Artists */ class ArtistListModel(application: Application) : GenericListModel(application) { - private val artists: MutableLiveData> = MutableLiveData() + val artists: MutableLiveData> = MutableLiveData() /** * Retrieves all available Artists in a LiveData */ - fun getItems(refresh: Boolean, swipe: SwipeRefreshLayout): LiveData> { + fun getItems(refresh: Boolean, swipe: SwipeRefreshLayout?): LiveData> { backgroundLoadFromServer(refresh, swipe) return artists } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/GenericListModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/GenericListModel.kt index 468802a2..96008e20 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/GenericListModel.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/GenericListModel.kt @@ -66,20 +66,24 @@ open class GenericListModel(application: Application) : */ fun backgroundLoadFromServer( refresh: Boolean, - swipe: SwipeRefreshLayout, + swipe: SwipeRefreshLayout?, bundle: Bundle = Bundle() ) { viewModelScope.launch { - swipe.isRefreshing = true + if (swipe != null) { + swipe.isRefreshing = true + } loadFromServer(refresh, swipe, bundle) - swipe.isRefreshing = false + if (swipe != null) { + swipe.isRefreshing = false + } } } /** * Calls the load() function with error handling */ - suspend fun loadFromServer(refresh: Boolean, swipe: SwipeRefreshLayout, bundle: Bundle) = + suspend fun loadFromServer(refresh: Boolean, swipe: SwipeRefreshLayout?, bundle: Bundle) = withContext(Dispatchers.IO) { val musicService = MusicServiceFactory.getMusicService() val isOffline = ActiveServerProvider.isOffline() @@ -88,7 +92,9 @@ open class GenericListModel(application: Application) : try { load(isOffline, useId3Tags, musicService, refresh, bundle) } catch (all: Exception) { - handleException(all, swipe.context) + if (swipe != null) { + handleException(all, swipe.context) + } } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerService.kt index 291355f3..1972bfaf 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerService.kt @@ -61,8 +61,8 @@ class MediaPlayerService : MediaBrowserServiceCompat() { private val localMediaPlayer by inject() private val nowPlayingEventDistributor by inject() private val mediaPlayerLifecycleSupport by inject() - private val autoMediaBrowser: AndroidAutoMediaBrowser = AndroidAutoMediaBrowser() + private var autoMediaBrowser: AndroidAutoMediaBrowser? = null private var mediaSession: MediaSessionCompat? = null private var isInForeground = false private var notificationBuilder: NotificationCompat.Builder? = null @@ -73,6 +73,7 @@ class MediaPlayerService : MediaBrowserServiceCompat() { override fun onCreate() { super.onCreate() + autoMediaBrowser = AndroidAutoMediaBrowser(application) updateMediaSession(null, PlayerState.IDLE) downloader.onCreate() @@ -140,14 +141,14 @@ class MediaPlayerService : MediaBrowserServiceCompat() { clientUid: Int, rootHints: Bundle? ): MediaBrowserServiceCompat.BrowserRoot { - return autoMediaBrowser.getRoot(clientPackageName, clientUid, rootHints) + return autoMediaBrowser!!.getRoot(clientPackageName, clientUid, rootHints) } override fun onLoadChildren( parentMediaId: String, result: MediaBrowserServiceCompat.Result> ) { - autoMediaBrowser.loadChildren(parentMediaId, result) + autoMediaBrowser!!.loadChildren(parentMediaId, result) } @Synchronized @@ -832,12 +833,20 @@ class MediaPlayerService : MediaBrowserServiceCompat() { override fun onPlayFromMediaId(mediaId: String?, extras: Bundle?) { super.onPlayFromMediaId(mediaId, extras) - val item: MusicDirectory.Entry? = autoMediaBrowser.getMusicDirectoryEntry(extras) - if (item != null) { + val result = autoMediaBrowser!!.getBundleData(extras) + if (result != null) { + val mediaId = result.first + val directoryList = result.second + resetPlayback() val songs: MutableList = mutableListOf() - songs.add(item) - + var found = false + for (item in directoryList) { + if (found || item.id == mediaId) { + found = true + songs.add(item) + } + } downloader.download(songs, false, false, false, true) getPendingIntentForMediaAction( diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/AndroidAutoMediaBrowser.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/AndroidAutoMediaBrowser.kt index c76bcf35..444219a8 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/AndroidAutoMediaBrowser.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/AndroidAutoMediaBrowser.kt @@ -1,8 +1,11 @@ package org.moire.ultrasonic.util +import android.app.Application import android.os.Bundle import android.support.v4.media.MediaBrowserCompat import android.support.v4.media.MediaDescriptionCompat +import androidx.lifecycle.LiveData +import androidx.lifecycle.Observer import androidx.media.MediaBrowserServiceCompat import androidx.media.utils.MediaConstants import java.io.ByteArrayInputStream @@ -12,28 +15,124 @@ import java.io.ObjectOutputStream import java.util.concurrent.ExecutorService import java.util.concurrent.Executors import org.moire.ultrasonic.api.subsonic.models.AlbumListType -import org.moire.ultrasonic.domain.Genre +import org.moire.ultrasonic.domain.Artist import org.moire.ultrasonic.domain.MusicDirectory +import org.moire.ultrasonic.fragment.AlbumListModel +import org.moire.ultrasonic.fragment.ArtistListModel import org.moire.ultrasonic.service.MusicServiceFactory -class AndroidAutoMediaBrowser() { +class AndroidAutoMediaBrowser(application: Application) { + + val albumListModel: AlbumListModel = AlbumListModel(application) + val artistListModel: ArtistListModel = ArtistListModel(application) val executorService: ExecutorService = Executors.newFixedThreadPool(4) var maximumRootChildLimit: Int = 4 private val MEDIA_BROWSER_ROOT_ID = "_Ultrasonice_mb_root_" - private val MEDIA_BROWSER_GENRE_LIST_ROOT = "_Ultrasonic_mb_genre_list_root_" private val MEDIA_BROWSER_RECENT_LIST_ROOT = "_Ultrasonic_mb_recent_list_root_" private val MEDIA_BROWSER_ALBUM_LIST_ROOT = "_Ultrasonic_mb_album_list_root_" private val MEDIA_BROWSER_ARTIST_LIST_ROOT = "_Ultrasonic_mb_rtist_list_root_" - private val MEDIA_BROWSER_GENRE_PREFIX = "_Ultrasonic_mb_genre_prefix_" private val MEDIA_BROWSER_RECENT_PREFIX = "_Ultrasonic_mb_recent_prefix_" private val MEDIA_BROWSER_ALBUM_PREFIX = "_Ultrasonic_mb_album_prefix_" private val MEDIA_BROWSER_ARTIST_PREFIX = "_Ultrasonic_mb_artist_prefix_" - private val MEDIA_BROWSER_EXTRA_ENTRY_BYTES = "_Ultrasonic_mb_extra_entry_bytes_" + private val MEDIA_BROWSER_EXTRA_ALBUM_LIST = "_Ultrasonic_mb_extra_album_list_" + private val MEDIA_BROWSER_EXTRA_MEDIA_ID = "_Ultrasonic_mb_extra_media_id_" + + class AlbumListObserver( + val idPrefix: String, + val result: MediaBrowserServiceCompat.Result>, + data: LiveData> + ) : + Observer> { + + private var liveData: LiveData>? = null + + init { + // Order is very important here. When observerForever is called onChanged + // will immediately be called with any past data updates. We don't care + // about those. So by having it called *before* liveData is set will + // signal to onChanged to ignore the first input + data.observeForever(this) + liveData = data + } + + override fun onChanged(albumList: List?) { + if (liveData == null) { + // See comment in the initializer + return + } + liveData!!.removeObserver(this) + if (albumList == null) { + return + } + val mediaItems: MutableList = mutableListOf() + for (item in albumList) { + val entryBuilder: MediaDescriptionCompat.Builder = + MediaDescriptionCompat.Builder() + entryBuilder + .setTitle(item.title) + .setMediaId(idPrefix + item.id) + mediaItems.add( + MediaBrowserCompat.MediaItem( + entryBuilder.build(), + MediaBrowserCompat.MediaItem.FLAG_BROWSABLE + ) + ) + } + + result.sendResult(mediaItems) + } + } + + class ArtistListObserver( + val idPrefix: String, + val result: MediaBrowserServiceCompat.Result>, + data: LiveData> + ) : + Observer> { + + private var liveData: LiveData>? = null + + init { + // Order is very important here. When observerForever is called onChanged + // will immediately be called with any past data updates. We don't care + // about those. So by having it called *before* liveData is set will + // signal to onChanged to ignore the first input + data.observeForever(this) + liveData = data + } + + override fun onChanged(artistList: List?) { + if (liveData == null) { + // See comment in the initializer + return + } + liveData!!.removeObserver(this) + if (artistList == null) { + return + } + val mediaItems: MutableList = mutableListOf() + for (item in artistList) { + val entryBuilder: MediaDescriptionCompat.Builder = + MediaDescriptionCompat.Builder() + entryBuilder + .setTitle(item.name) + .setMediaId(idPrefix + item.id) + mediaItems.add( + MediaBrowserCompat.MediaItem( + entryBuilder.build(), + MediaBrowserCompat.MediaItem.FLAG_BROWSABLE + ) + ) + } + + result.sendResult(mediaItems) + } + } fun getRoot( clientPackageName: String, @@ -65,14 +164,6 @@ class AndroidAutoMediaBrowser() { // Build the MediaItem objects for the top level, // and put them in the mediaItems list... - var genreList: MediaDescriptionCompat.Builder = MediaDescriptionCompat.Builder() - genreList.setTitle("Genre").setMediaId(MEDIA_BROWSER_GENRE_LIST_ROOT) - mediaItems.add( - MediaBrowserCompat.MediaItem( - genreList.build(), - MediaBrowserCompat.MediaItem.FLAG_BROWSABLE - ) - ) var recentList: MediaDescriptionCompat.Builder = MediaDescriptionCompat.Builder() recentList.setTitle("Recent").setMediaId(MEDIA_BROWSER_RECENT_LIST_ROOT) mediaItems.add( @@ -97,9 +188,6 @@ class AndroidAutoMediaBrowser() { MediaBrowserCompat.MediaItem.FLAG_BROWSABLE ) ) - } else if (MEDIA_BROWSER_GENRE_LIST_ROOT == parentMediaId) { - fetchGenres(result) - return } else if (MEDIA_BROWSER_RECENT_LIST_ROOT == parentMediaId) { fetchAlbumList(AlbumListType.RECENT, MEDIA_BROWSER_RECENT_PREFIX, result) return @@ -107,43 +195,19 @@ class AndroidAutoMediaBrowser() { fetchAlbumList(AlbumListType.SORTED_BY_NAME, MEDIA_BROWSER_ALBUM_PREFIX, result) return } else if (MEDIA_BROWSER_ARTIST_LIST_ROOT == parentMediaId) { - fetchAlbumList(AlbumListType.SORTED_BY_ARTIST, MEDIA_BROWSER_ARTIST_PREFIX, result) + fetchArtistList(MEDIA_BROWSER_ARTIST_PREFIX, result) + return + } else if (parentMediaId.startsWith(MEDIA_BROWSER_RECENT_PREFIX)) { + fetchTrackList(parentMediaId.substring(MEDIA_BROWSER_RECENT_PREFIX.length), result) return } else if (parentMediaId.startsWith(MEDIA_BROWSER_ALBUM_PREFIX)) { - executorService.execute { - val musicService = MusicServiceFactory.getMusicService() - val id = parentMediaId.substring(MEDIA_BROWSER_ALBUM_PREFIX.length) - - val albumDirectory = musicService.getAlbum( - id, "", false - ) - for (item in albumDirectory.getAllChild()) { - val extras = Bundle() - - // Note that Bundle supports putSerializable and MusicDirectory.Entry - // implements Serializable, but when I try to use it the app crashes - val byteArrayOutputStream = ByteArrayOutputStream() - val objectOutputStream = ObjectOutputStream(byteArrayOutputStream) - objectOutputStream.writeObject(item) - objectOutputStream.close() - extras.putByteArray( - MEDIA_BROWSER_EXTRA_ENTRY_BYTES, - byteArrayOutputStream.toByteArray() - ) - - val entryBuilder: MediaDescriptionCompat.Builder = - MediaDescriptionCompat.Builder() - entryBuilder.setTitle(item.title).setMediaId(item.id).setExtras(extras) - mediaItems.add( - MediaBrowserCompat.MediaItem( - entryBuilder.build(), - MediaBrowserCompat.MediaItem.FLAG_PLAYABLE - ) - ) - } - result.sendResult(mediaItems) - } - result.detach() + fetchTrackList(parentMediaId.substring(MEDIA_BROWSER_ALBUM_PREFIX.length), result) + return + } else if (parentMediaId.startsWith(MEDIA_BROWSER_ARTIST_PREFIX)) { + fetchArtistAlbumList( + parentMediaId.substring(MEDIA_BROWSER_ARTIST_PREFIX.length), + result + ) return } else { // Examine the passed parentMediaId to see which submenu we're at, @@ -152,39 +216,67 @@ class AndroidAutoMediaBrowser() { result.sendResult(mediaItems) } - fun getMusicDirectoryEntry(bundle: Bundle?): MusicDirectory.Entry? { + fun getBundleData(bundle: Bundle?): Pair>? { if (bundle == null) { return null } - if (!bundle.containsKey(MEDIA_BROWSER_EXTRA_ENTRY_BYTES)) { + if (!bundle.containsKey(MEDIA_BROWSER_EXTRA_ALBUM_LIST) || + !bundle.containsKey(MEDIA_BROWSER_EXTRA_MEDIA_ID) + ) { return null } - val bytes = bundle.getByteArray(MEDIA_BROWSER_EXTRA_ENTRY_BYTES) + val bytes = bundle.getByteArray(MEDIA_BROWSER_EXTRA_ALBUM_LIST) val byteArrayInputStream = ByteArrayInputStream(bytes) val objectInputStream = ObjectInputStream(byteArrayInputStream) - return objectInputStream.readObject() as MusicDirectory.Entry + return Pair( + bundle.getString(MEDIA_BROWSER_EXTRA_MEDIA_ID), + objectInputStream.readObject() as List + ) } - fun fetchAlbumList( + private fun fetchAlbumList( type: AlbumListType, idPrefix: String, result: MediaBrowserServiceCompat.Result> + ) { + AlbumListObserver( + idPrefix, result, + albumListModel.albumList + ) + + val args: Bundle = Bundle() + args.putString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE, type.toString()) + albumListModel.getAlbumList(false, null, args) + result.detach() + } + + private fun fetchArtistList( + idPrefix: String, + result: MediaBrowserServiceCompat.Result> + ) { + ArtistListObserver(idPrefix, result, artistListModel.artists) + + artistListModel.getItems(false, null) + result.detach() + } + + private fun fetchArtistAlbumList( + id: String, + result: MediaBrowserServiceCompat.Result> ) { executorService.execute { - val mediaItems: MutableList = mutableListOf() val musicService = MusicServiceFactory.getMusicService() - val musicDirectory: MusicDirectory = musicService.getAlbumList2( - type.toString(), 500, 0, null + val musicDirectory = musicService.getMusicDirectory( + id, "", false ) + val mediaItems: MutableList = mutableListOf() for (item in musicDirectory.getAllChild()) { - var entryBuilder: MediaDescriptionCompat.Builder = + val entryBuilder: MediaDescriptionCompat.Builder = MediaDescriptionCompat.Builder() - entryBuilder - .setTitle(item.title) - .setMediaId(idPrefix + item.id) + entryBuilder.setTitle(item.title).setMediaId(MEDIA_BROWSER_ALBUM_PREFIX + item.id) mediaItems.add( MediaBrowserCompat.MediaItem( entryBuilder.build(), @@ -197,26 +289,48 @@ class AndroidAutoMediaBrowser() { result.detach() } - fun fetchGenres(result: MediaBrowserServiceCompat.Result>) { + private fun fetchTrackList( + id: String, + result: MediaBrowserServiceCompat.Result> + ) { executorService.execute { - val mediaItems: MutableList = mutableListOf() val musicService = MusicServiceFactory.getMusicService() - val genreList: List? = musicService.getGenres(false) - if (genreList != null) { - for (genre in genreList) { - var entryBuilder: MediaDescriptionCompat.Builder = - MediaDescriptionCompat.Builder() - entryBuilder - .setTitle(genre.name) - .setMediaId(MEDIA_BROWSER_GENRE_PREFIX + genre.index) - mediaItems.add( - MediaBrowserCompat.MediaItem( - entryBuilder.build(), - MediaBrowserCompat.MediaItem.FLAG_BROWSABLE - ) + val albumDirectory = musicService.getAlbum( + id, "", false + ) + + // The idea here is that we want to attach the full album list to every song, + // as well as the id of the specific song. This way if someone chooses to play a song + // we can add the song and all subsequent songs in the album + val byteArrayOutputStream = ByteArrayOutputStream() + val objectOutputStream = ObjectOutputStream(byteArrayOutputStream) + objectOutputStream.writeObject(albumDirectory.getAllChild()) + objectOutputStream.close() + val songList = byteArrayOutputStream.toByteArray() + val mediaItems: MutableList = mutableListOf() + + for (item in albumDirectory.getAllChild()) { + val extras = Bundle() + + extras.putByteArray( + MEDIA_BROWSER_EXTRA_ALBUM_LIST, + songList + ) + extras.putString( + MEDIA_BROWSER_EXTRA_MEDIA_ID, + item.id + ) + + val entryBuilder: MediaDescriptionCompat.Builder = + MediaDescriptionCompat.Builder() + entryBuilder.setTitle(item.title).setMediaId(item.id).setExtras(extras) + mediaItems.add( + MediaBrowserCompat.MediaItem( + entryBuilder.build(), + MediaBrowserCompat.MediaItem.FLAG_PLAYABLE ) - } + ) } result.sendResult(mediaItems) }