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(
refresh: Boolean,
swipe: SwipeRefreshLayout,
swipe: SwipeRefreshLayout?,
args: Bundle
): 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
*/
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
*/
fun getItems(refresh: Boolean, swipe: SwipeRefreshLayout): LiveData<List<Artist>> {
fun getItems(refresh: Boolean, swipe: SwipeRefreshLayout?): LiveData<List<Artist>> {
backgroundLoadFromServer(refresh, swipe)
return artists
}

View File

@ -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)
}
}
}

View File

@ -61,8 +61,8 @@ class MediaPlayerService : MediaBrowserServiceCompat() {
private val localMediaPlayer by inject<LocalMediaPlayer>()
private val nowPlayingEventDistributor by inject<NowPlayingEventDistributor>()
private val mediaPlayerLifecycleSupport by inject<MediaPlayerLifecycleSupport>()
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<List<MediaBrowserCompat.MediaItem>>
) {
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<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)
getPendingIntentForMediaAction(

View File

@ -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<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(
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<String, List<MusicDirectory.Entry>>? {
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<MusicDirectory.Entry>
)
}
fun fetchAlbumList(
private fun fetchAlbumList(
type: AlbumListType,
idPrefix: String,
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 {
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = mutableListOf()
val musicService = MusicServiceFactory.getMusicService()
val musicDirectory: MusicDirectory = musicService.getAlbumList2(
type.toString(), 500, 0, null
val musicDirectory = musicService.getMusicDirectory(
id, "", false
)
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = 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<List<MediaBrowserCompat.MediaItem>>) {
private fun fetchTrackList(
id: String,
result: MediaBrowserServiceCompat.Result<List<MediaBrowserCompat.MediaItem>>
) {
executorService.execute {
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = mutableListOf()
val musicService = MusicServiceFactory.getMusicService()
val genreList: List<Genre>? = 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<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)
}