working version

This commit is contained in:
James Wells 2021-06-19 00:05:19 -04:00
parent 3853fce818
commit 793c4a6ca7
No known key found for this signature in database
GPG Key ID: DB1528F6EED16127
5 changed files with 223 additions and 94 deletions

View File

@ -18,7 +18,7 @@ class AlbumListModel(application: Application) : GenericListModel(application) {
fun getAlbumList( fun getAlbumList(
refresh: Boolean, refresh: Boolean,
swipe: SwipeRefreshLayout, swipe: SwipeRefreshLayout?,
args: Bundle args: Bundle
): LiveData<List<MusicDirectory.Entry>> { ): LiveData<List<MusicDirectory.Entry>> {

View File

@ -30,12 +30,12 @@ import org.moire.ultrasonic.service.MusicService
* Provides ViewModel which contains the list of available Artists * Provides ViewModel which contains the list of available Artists
*/ */
class ArtistListModel(application: Application) : GenericListModel(application) { class ArtistListModel(application: Application) : GenericListModel(application) {
private val artists: MutableLiveData<List<Artist>> = MutableLiveData() val artists: MutableLiveData<List<Artist>> = MutableLiveData()
/** /**
* Retrieves all available Artists in a LiveData * Retrieves all available Artists in a LiveData
*/ */
fun getItems(refresh: Boolean, swipe: SwipeRefreshLayout): LiveData<List<Artist>> { fun getItems(refresh: Boolean, swipe: SwipeRefreshLayout?): LiveData<List<Artist>> {
backgroundLoadFromServer(refresh, swipe) backgroundLoadFromServer(refresh, swipe)
return artists return artists
} }

View File

@ -66,20 +66,24 @@ open class GenericListModel(application: Application) :
*/ */
fun backgroundLoadFromServer( fun backgroundLoadFromServer(
refresh: Boolean, refresh: Boolean,
swipe: SwipeRefreshLayout, swipe: SwipeRefreshLayout?,
bundle: Bundle = Bundle() bundle: Bundle = Bundle()
) { ) {
viewModelScope.launch { viewModelScope.launch {
swipe.isRefreshing = true if (swipe != null) {
swipe.isRefreshing = true
}
loadFromServer(refresh, swipe, bundle) loadFromServer(refresh, swipe, bundle)
swipe.isRefreshing = false if (swipe != null) {
swipe.isRefreshing = false
}
} }
} }
/** /**
* Calls the load() function with error handling * 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) { withContext(Dispatchers.IO) {
val musicService = MusicServiceFactory.getMusicService() val musicService = MusicServiceFactory.getMusicService()
val isOffline = ActiveServerProvider.isOffline() val isOffline = ActiveServerProvider.isOffline()
@ -88,7 +92,9 @@ open class GenericListModel(application: Application) :
try { try {
load(isOffline, useId3Tags, musicService, refresh, bundle) load(isOffline, useId3Tags, musicService, refresh, bundle)
} catch (all: Exception) { } catch (all: Exception) {
handleException(all, swipe.context) if (swipe != null) {
handleException(all, swipe.context)
}
} }
} }

View File

@ -61,8 +61,8 @@ class MediaPlayerService : MediaBrowserServiceCompat() {
private val localMediaPlayer by inject<LocalMediaPlayer>() private val localMediaPlayer by inject<LocalMediaPlayer>()
private val nowPlayingEventDistributor by inject<NowPlayingEventDistributor>() private val nowPlayingEventDistributor by inject<NowPlayingEventDistributor>()
private val mediaPlayerLifecycleSupport by inject<MediaPlayerLifecycleSupport>() private val mediaPlayerLifecycleSupport by inject<MediaPlayerLifecycleSupport>()
private val autoMediaBrowser: AndroidAutoMediaBrowser = AndroidAutoMediaBrowser()
private var autoMediaBrowser: AndroidAutoMediaBrowser? = null
private var mediaSession: MediaSessionCompat? = null private var mediaSession: MediaSessionCompat? = null
private var isInForeground = false private var isInForeground = false
private var notificationBuilder: NotificationCompat.Builder? = null private var notificationBuilder: NotificationCompat.Builder? = null
@ -73,6 +73,7 @@ class MediaPlayerService : MediaBrowserServiceCompat() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
autoMediaBrowser = AndroidAutoMediaBrowser(application)
updateMediaSession(null, PlayerState.IDLE) updateMediaSession(null, PlayerState.IDLE)
downloader.onCreate() downloader.onCreate()
@ -140,14 +141,14 @@ class MediaPlayerService : MediaBrowserServiceCompat() {
clientUid: Int, clientUid: Int,
rootHints: Bundle? rootHints: Bundle?
): MediaBrowserServiceCompat.BrowserRoot { ): MediaBrowserServiceCompat.BrowserRoot {
return autoMediaBrowser.getRoot(clientPackageName, clientUid, rootHints) return autoMediaBrowser!!.getRoot(clientPackageName, clientUid, rootHints)
} }
override fun onLoadChildren( override fun onLoadChildren(
parentMediaId: String, parentMediaId: String,
result: MediaBrowserServiceCompat.Result<List<MediaBrowserCompat.MediaItem>> result: MediaBrowserServiceCompat.Result<List<MediaBrowserCompat.MediaItem>>
) { ) {
autoMediaBrowser.loadChildren(parentMediaId, result) autoMediaBrowser!!.loadChildren(parentMediaId, result)
} }
@Synchronized @Synchronized
@ -832,12 +833,20 @@ class MediaPlayerService : MediaBrowserServiceCompat() {
override fun onPlayFromMediaId(mediaId: String?, extras: Bundle?) { override fun onPlayFromMediaId(mediaId: String?, extras: Bundle?) {
super.onPlayFromMediaId(mediaId, extras) super.onPlayFromMediaId(mediaId, extras)
val item: MusicDirectory.Entry? = autoMediaBrowser.getMusicDirectoryEntry(extras) val result = autoMediaBrowser!!.getBundleData(extras)
if (item != null) { if (result != null) {
val mediaId = result.first
val directoryList = result.second
resetPlayback() resetPlayback()
val songs: MutableList<MusicDirectory.Entry> = mutableListOf() val songs: MutableList<MusicDirectory.Entry> = 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) downloader.download(songs, false, false, false, true)
getPendingIntentForMediaAction( getPendingIntentForMediaAction(

View File

@ -1,8 +1,11 @@
package org.moire.ultrasonic.util package org.moire.ultrasonic.util
import android.app.Application
import android.os.Bundle import android.os.Bundle
import android.support.v4.media.MediaBrowserCompat import android.support.v4.media.MediaBrowserCompat
import android.support.v4.media.MediaDescriptionCompat import android.support.v4.media.MediaDescriptionCompat
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import androidx.media.MediaBrowserServiceCompat import androidx.media.MediaBrowserServiceCompat
import androidx.media.utils.MediaConstants import androidx.media.utils.MediaConstants
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
@ -12,28 +15,124 @@ import java.io.ObjectOutputStream
import java.util.concurrent.ExecutorService import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors import java.util.concurrent.Executors
import org.moire.ultrasonic.api.subsonic.models.AlbumListType 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.domain.MusicDirectory
import org.moire.ultrasonic.fragment.AlbumListModel
import org.moire.ultrasonic.fragment.ArtistListModel
import org.moire.ultrasonic.service.MusicServiceFactory 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) val executorService: ExecutorService = Executors.newFixedThreadPool(4)
var maximumRootChildLimit: Int = 4 var maximumRootChildLimit: Int = 4
private val MEDIA_BROWSER_ROOT_ID = "_Ultrasonice_mb_root_" 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_RECENT_LIST_ROOT = "_Ultrasonic_mb_recent_list_root_"
private val MEDIA_BROWSER_ALBUM_LIST_ROOT = "_Ultrasonic_mb_album_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_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_RECENT_PREFIX = "_Ultrasonic_mb_recent_prefix_"
private val MEDIA_BROWSER_ALBUM_PREFIX = "_Ultrasonic_mb_album_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_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<List<MediaBrowserCompat.MediaItem>>,
data: LiveData<List<MusicDirectory.Entry>>
) :
Observer<List<MusicDirectory.Entry>> {
private var liveData: LiveData<List<MusicDirectory.Entry>>? = 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<MusicDirectory.Entry>?) {
if (liveData == null) {
// See comment in the initializer
return
}
liveData!!.removeObserver(this)
if (albumList == null) {
return
}
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = 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<List<MediaBrowserCompat.MediaItem>>,
data: LiveData<List<Artist>>
) :
Observer<List<Artist>> {
private var liveData: LiveData<List<Artist>>? = 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<Artist>?) {
if (liveData == null) {
// See comment in the initializer
return
}
liveData!!.removeObserver(this)
if (artistList == null) {
return
}
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = 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( fun getRoot(
clientPackageName: String, clientPackageName: String,
@ -65,14 +164,6 @@ class AndroidAutoMediaBrowser() {
// Build the MediaItem objects for the top level, // Build the MediaItem objects for the top level,
// and put them in the mediaItems list... // 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() var recentList: MediaDescriptionCompat.Builder = MediaDescriptionCompat.Builder()
recentList.setTitle("Recent").setMediaId(MEDIA_BROWSER_RECENT_LIST_ROOT) recentList.setTitle("Recent").setMediaId(MEDIA_BROWSER_RECENT_LIST_ROOT)
mediaItems.add( mediaItems.add(
@ -97,9 +188,6 @@ class AndroidAutoMediaBrowser() {
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
) )
) )
} else if (MEDIA_BROWSER_GENRE_LIST_ROOT == parentMediaId) {
fetchGenres(result)
return
} else if (MEDIA_BROWSER_RECENT_LIST_ROOT == parentMediaId) { } else if (MEDIA_BROWSER_RECENT_LIST_ROOT == parentMediaId) {
fetchAlbumList(AlbumListType.RECENT, MEDIA_BROWSER_RECENT_PREFIX, result) fetchAlbumList(AlbumListType.RECENT, MEDIA_BROWSER_RECENT_PREFIX, result)
return return
@ -107,43 +195,19 @@ class AndroidAutoMediaBrowser() {
fetchAlbumList(AlbumListType.SORTED_BY_NAME, MEDIA_BROWSER_ALBUM_PREFIX, result) fetchAlbumList(AlbumListType.SORTED_BY_NAME, MEDIA_BROWSER_ALBUM_PREFIX, result)
return return
} else if (MEDIA_BROWSER_ARTIST_LIST_ROOT == parentMediaId) { } 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 return
} else if (parentMediaId.startsWith(MEDIA_BROWSER_ALBUM_PREFIX)) { } else if (parentMediaId.startsWith(MEDIA_BROWSER_ALBUM_PREFIX)) {
executorService.execute { fetchTrackList(parentMediaId.substring(MEDIA_BROWSER_ALBUM_PREFIX.length), result)
val musicService = MusicServiceFactory.getMusicService() return
val id = parentMediaId.substring(MEDIA_BROWSER_ALBUM_PREFIX.length) } else if (parentMediaId.startsWith(MEDIA_BROWSER_ARTIST_PREFIX)) {
fetchArtistAlbumList(
val albumDirectory = musicService.getAlbum( parentMediaId.substring(MEDIA_BROWSER_ARTIST_PREFIX.length),
id, "", false result
) )
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()
return return
} else { } else {
// Examine the passed parentMediaId to see which submenu we're at, // Examine the passed parentMediaId to see which submenu we're at,
@ -152,39 +216,67 @@ class AndroidAutoMediaBrowser() {
result.sendResult(mediaItems) result.sendResult(mediaItems)
} }
fun getMusicDirectoryEntry(bundle: Bundle?): MusicDirectory.Entry? { fun getBundleData(bundle: Bundle?): Pair<String, List<MusicDirectory.Entry>>? {
if (bundle == null) { if (bundle == null) {
return 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 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 byteArrayInputStream = ByteArrayInputStream(bytes)
val objectInputStream = ObjectInputStream(byteArrayInputStream) val objectInputStream = ObjectInputStream(byteArrayInputStream)
return objectInputStream.readObject() as MusicDirectory.Entry return Pair(
bundle.getString(MEDIA_BROWSER_EXTRA_MEDIA_ID),
objectInputStream.readObject() as List<MusicDirectory.Entry>
)
} }
fun fetchAlbumList( private fun fetchAlbumList(
type: AlbumListType, type: AlbumListType,
idPrefix: String, idPrefix: String,
result: MediaBrowserServiceCompat.Result<List<MediaBrowserCompat.MediaItem>> result: MediaBrowserServiceCompat.Result<List<MediaBrowserCompat.MediaItem>>
) {
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<List<MediaBrowserCompat.MediaItem>>
) {
ArtistListObserver(idPrefix, result, artistListModel.artists)
artistListModel.getItems(false, null)
result.detach()
}
private fun fetchArtistAlbumList(
id: String,
result: MediaBrowserServiceCompat.Result<List<MediaBrowserCompat.MediaItem>>
) { ) {
executorService.execute { executorService.execute {
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = mutableListOf()
val musicService = MusicServiceFactory.getMusicService() val musicService = MusicServiceFactory.getMusicService()
val musicDirectory: MusicDirectory = musicService.getAlbumList2( val musicDirectory = musicService.getMusicDirectory(
type.toString(), 500, 0, null id, "", false
) )
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = mutableListOf()
for (item in musicDirectory.getAllChild()) { for (item in musicDirectory.getAllChild()) {
var entryBuilder: MediaDescriptionCompat.Builder = val entryBuilder: MediaDescriptionCompat.Builder =
MediaDescriptionCompat.Builder() MediaDescriptionCompat.Builder()
entryBuilder entryBuilder.setTitle(item.title).setMediaId(MEDIA_BROWSER_ALBUM_PREFIX + item.id)
.setTitle(item.title)
.setMediaId(idPrefix + item.id)
mediaItems.add( mediaItems.add(
MediaBrowserCompat.MediaItem( MediaBrowserCompat.MediaItem(
entryBuilder.build(), entryBuilder.build(),
@ -197,26 +289,48 @@ class AndroidAutoMediaBrowser() {
result.detach() result.detach()
} }
fun fetchGenres(result: MediaBrowserServiceCompat.Result<List<MediaBrowserCompat.MediaItem>>) { private fun fetchTrackList(
id: String,
result: MediaBrowserServiceCompat.Result<List<MediaBrowserCompat.MediaItem>>
) {
executorService.execute { executorService.execute {
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = mutableListOf()
val musicService = MusicServiceFactory.getMusicService() val musicService = MusicServiceFactory.getMusicService()
val genreList: List<Genre>? = musicService.getGenres(false) val albumDirectory = musicService.getAlbum(
if (genreList != null) { id, "", false
for (genre in genreList) { )
var entryBuilder: MediaDescriptionCompat.Builder =
MediaDescriptionCompat.Builder() // The idea here is that we want to attach the full album list to every song,
entryBuilder // as well as the id of the specific song. This way if someone chooses to play a song
.setTitle(genre.name) // we can add the song and all subsequent songs in the album
.setMediaId(MEDIA_BROWSER_GENRE_PREFIX + genre.index) val byteArrayOutputStream = ByteArrayOutputStream()
mediaItems.add( val objectOutputStream = ObjectOutputStream(byteArrayOutputStream)
MediaBrowserCompat.MediaItem( objectOutputStream.writeObject(albumDirectory.getAllChild())
entryBuilder.build(), objectOutputStream.close()
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE val songList = byteArrayOutputStream.toByteArray()
) val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = 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) result.sendResult(mediaItems)
} }