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 {
if (swipe != null) {
swipe.isRefreshing = true swipe.isRefreshing = true
}
loadFromServer(refresh, swipe, bundle) loadFromServer(refresh, swipe, bundle)
if (swipe != null) {
swipe.isRefreshing = false 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,9 +92,11 @@ 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) {
if (swipe != null) {
handleException(all, swipe.context) handleException(all, swipe.context)
} }
} }
}
private fun handleException(exception: Exception, context: Context) { private fun handleException(exception: Exception, context: Context) {
Handler(Looper.getMainLooper()).post { Handler(Looper.getMainLooper()).post {

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()
var found = false
for (item in directoryList) {
if (found || item.id == mediaId) {
found = true
songs.add(item) 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,28 +195,131 @@ 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)) {
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,
// and put the children of that menu in the mediaItems list...
}
result.sendResult(mediaItems)
}
fun getBundleData(bundle: Bundle?): Pair<String, List<MusicDirectory.Entry>>? {
if (bundle == null) {
return null
}
if (!bundle.containsKey(MEDIA_BROWSER_EXTRA_ALBUM_LIST) ||
!bundle.containsKey(MEDIA_BROWSER_EXTRA_MEDIA_ID)
) {
return null
}
val bytes = bundle.getByteArray(MEDIA_BROWSER_EXTRA_ALBUM_LIST)
val byteArrayInputStream = ByteArrayInputStream(bytes)
val objectInputStream = ObjectInputStream(byteArrayInputStream)
return Pair(
bundle.getString(MEDIA_BROWSER_EXTRA_MEDIA_ID),
objectInputStream.readObject() as List<MusicDirectory.Entry>
)
}
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 musicService = MusicServiceFactory.getMusicService()
val musicDirectory = musicService.getMusicDirectory(
id, "", false
)
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = mutableListOf()
for (item in musicDirectory.getAllChild()) {
val entryBuilder: MediaDescriptionCompat.Builder =
MediaDescriptionCompat.Builder()
entryBuilder.setTitle(item.title).setMediaId(MEDIA_BROWSER_ALBUM_PREFIX + item.id)
mediaItems.add(
MediaBrowserCompat.MediaItem(
entryBuilder.build(),
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
)
)
}
result.sendResult(mediaItems)
}
result.detach()
}
private fun fetchTrackList(
id: String,
result: MediaBrowserServiceCompat.Result<List<MediaBrowserCompat.MediaItem>>
) {
executorService.execute { executorService.execute {
val musicService = MusicServiceFactory.getMusicService() val musicService = MusicServiceFactory.getMusicService()
val id = parentMediaId.substring(MEDIA_BROWSER_ALBUM_PREFIX.length)
val albumDirectory = musicService.getAlbum( val albumDirectory = musicService.getAlbum(
id, "", false 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()) { for (item in albumDirectory.getAllChild()) {
val extras = Bundle() 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( extras.putByteArray(
MEDIA_BROWSER_EXTRA_ENTRY_BYTES, MEDIA_BROWSER_EXTRA_ALBUM_LIST,
byteArrayOutputStream.toByteArray() songList
)
extras.putString(
MEDIA_BROWSER_EXTRA_MEDIA_ID,
item.id
) )
val entryBuilder: MediaDescriptionCompat.Builder = val entryBuilder: MediaDescriptionCompat.Builder =
@ -144,82 +335,5 @@ class AndroidAutoMediaBrowser() {
result.sendResult(mediaItems) result.sendResult(mediaItems)
} }
result.detach() result.detach()
return
} else {
// Examine the passed parentMediaId to see which submenu we're at,
// and put the children of that menu in the mediaItems list...
}
result.sendResult(mediaItems)
}
fun getMusicDirectoryEntry(bundle: Bundle?): MusicDirectory.Entry? {
if (bundle == null) {
return null
}
if (!bundle.containsKey(MEDIA_BROWSER_EXTRA_ENTRY_BYTES)) {
return null
}
val bytes = bundle.getByteArray(MEDIA_BROWSER_EXTRA_ENTRY_BYTES)
val byteArrayInputStream = ByteArrayInputStream(bytes)
val objectInputStream = ObjectInputStream(byteArrayInputStream)
return objectInputStream.readObject() as MusicDirectory.Entry
}
fun fetchAlbumList(
type: AlbumListType,
idPrefix: 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
)
for (item in musicDirectory.getAllChild()) {
var 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)
}
result.detach()
}
fun fetchGenres(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
)
)
}
}
result.sendResult(mediaItems)
}
result.detach()
} }
} }