home: mirror tabs to mediasession browser

This commit is contained in:
Alexander Capehart 2024-09-13 13:35:37 -06:00
parent 29d663f500
commit 3832c4e525
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
7 changed files with 175 additions and 124 deletions

View file

@ -1,6 +1,6 @@
package org.oxycblt.auxio.home.list
package org.oxycblt.auxio.home
import org.oxycblt.auxio.home.HomeSettings
import org.oxycblt.auxio.home.tabs.Tab
import org.oxycblt.auxio.list.ListSettings
import org.oxycblt.auxio.list.adapter.UpdateInstructions
import org.oxycblt.auxio.music.Album
@ -10,39 +10,46 @@ import org.oxycblt.auxio.music.MusicRepository
import org.oxycblt.auxio.music.MusicType
import org.oxycblt.auxio.music.Playlist
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.PlaybackSettings
import org.oxycblt.auxio.util.logD
import javax.inject.Inject
interface HomeListGenerator {
interface HomeGenerator {
fun songs(): List<Song>
fun albums(): List<Album>
fun artists(): List<Artist>
fun genres(): List<Genre>
fun playlists(): List<Playlist>
fun tabs(): List<MusicType>
fun release()
interface Invalidator {
fun invalidate(type: MusicType, instructions: UpdateInstructions)
fun invalidateMusic(type: MusicType, instructions: UpdateInstructions)
fun invalidateTabs()
}
interface Factory {
fun create(invalidator: Invalidator): HomeListGenerator
fun create(invalidator: Invalidator): HomeGenerator
}
}
private class HomeListGeneratorImpl(
private val invalidator: HomeListGenerator.Invalidator,
private class HomeGeneratorImpl(
private val invalidator: HomeGenerator.Invalidator,
private val homeSettings: HomeSettings,
private val listSettings: ListSettings,
private val musicRepository: MusicRepository,
) : HomeListGenerator, HomeSettings.Listener, ListSettings.Listener, MusicRepository.UpdateListener {
) : HomeGenerator, HomeSettings.Listener, ListSettings.Listener, MusicRepository.UpdateListener {
override fun songs() =
musicRepository.deviceLibrary?.let { listSettings.songSort.songs(it.songs) } ?: emptyList()
override fun albums() = musicRepository.deviceLibrary?.let { listSettings.albumSort.albums(it.albums) } ?: emptyList()
override fun artists() = musicRepository.deviceLibrary?.let { listSettings.artistSort.artists(it.artists) } ?: emptyList()
override fun genres() = musicRepository.deviceLibrary?.let { listSettings.genreSort.genres(it.genres) } ?: emptyList()
override fun playlists() = musicRepository.userLibrary?.let { listSettings.playlistSort.playlists(it.playlists) } ?: emptyList()
override fun tabs() =
homeSettings.homeTabs.filterIsInstance<Tab.Visible>().map { it.type }
override fun onTabsChanged() {
invalidator.invalidateTabs()
}
init {
homeSettings.registerListener(this)
@ -51,9 +58,9 @@ private class HomeListGeneratorImpl(
}
override fun release() {
homeSettings.unregisterListener(this)
listSettings.unregisterListener(this)
musicRepository.removeUpdateListener(this)
listSettings.unregisterListener(this)
homeSettings.unregisterListener(this)
}
override fun onHideCollaboratorsChanged() {
@ -65,27 +72,27 @@ private class HomeListGeneratorImpl(
override fun onSongSortChanged() {
super.onSongSortChanged()
invalidator.invalidate(MusicType.SONGS, UpdateInstructions.Replace(0))
invalidator.invalidateMusic(MusicType.SONGS, UpdateInstructions.Replace(0))
}
override fun onAlbumSortChanged() {
super.onAlbumSortChanged()
invalidator.invalidate(MusicType.ALBUMS, UpdateInstructions.Replace(0))
invalidator.invalidateMusic(MusicType.ALBUMS, UpdateInstructions.Replace(0))
}
override fun onArtistSortChanged() {
super.onArtistSortChanged()
invalidator.invalidate(MusicType.ARTISTS, UpdateInstructions.Replace(0))
invalidator.invalidateMusic(MusicType.ARTISTS, UpdateInstructions.Replace(0))
}
override fun onGenreSortChanged() {
super.onGenreSortChanged()
invalidator.invalidate(MusicType.GENRES, UpdateInstructions.Replace(0))
invalidator.invalidateMusic(MusicType.GENRES, UpdateInstructions.Replace(0))
}
override fun onPlaylistSortChanged() {
super.onPlaylistSortChanged()
invalidator.invalidate(MusicType.PLAYLISTS, UpdateInstructions.Replace(0))
invalidator.invalidateMusic(MusicType.PLAYLISTS, UpdateInstructions.Replace(0))
}
override fun onMusicChanges(changes: MusicRepository.Changes) {
@ -94,16 +101,16 @@ private class HomeListGeneratorImpl(
logD("Refreshing library")
// Get the each list of items in the library to use as our list data.
// Applying the preferred sorting to them.
invalidator.invalidate(MusicType.SONGS, UpdateInstructions.Diff)
invalidator.invalidate(MusicType.ALBUMS, UpdateInstructions.Diff)
invalidator.invalidate(MusicType.ARTISTS, UpdateInstructions.Diff)
invalidator.invalidate(MusicType.GENRES, UpdateInstructions.Diff)
invalidator.invalidateMusic(MusicType.SONGS, UpdateInstructions.Diff)
invalidator.invalidateMusic(MusicType.ALBUMS, UpdateInstructions.Diff)
invalidator.invalidateMusic(MusicType.ARTISTS, UpdateInstructions.Diff)
invalidator.invalidateMusic(MusicType.GENRES, UpdateInstructions.Diff)
}
val userLibrary = musicRepository.userLibrary
if (changes.userLibrary && userLibrary != null) {
logD("Refreshing playlists")
invalidator.invalidate(MusicType.PLAYLISTS, UpdateInstructions.Diff)
invalidator.invalidateMusic(MusicType.PLAYLISTS, UpdateInstructions.Diff)
}
}

View file

@ -23,15 +23,14 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import org.oxycblt.auxio.home.list.HomeListGenerator
import org.oxycblt.auxio.home.tabs.Tab
import org.oxycblt.auxio.home.tabs.TabListGenerator
import org.oxycblt.auxio.list.ListSettings
import org.oxycblt.auxio.list.adapter.UpdateInstructions
import org.oxycblt.auxio.list.sort.Sort
import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.MusicRepository
import org.oxycblt.auxio.music.MusicType
import org.oxycblt.auxio.music.Playlist
import org.oxycblt.auxio.music.Song
@ -50,12 +49,11 @@ import org.oxycblt.auxio.util.logD
class HomeViewModel
@Inject
constructor(
private val homeSettings: HomeSettings,
private val listSettings: ListSettings,
private val playbackSettings: PlaybackSettings,
homeGeneratorFactory: HomeListGenerator.Factory
) : ViewModel(), HomeSettings.Listener, HomeListGenerator.Invalidator {
private val generator = homeGeneratorFactory.create(this)
homeGeneratorFactory: HomeGenerator.Factory
) : ViewModel(), HomeGenerator.Invalidator {
private val homeGenerator = homeGeneratorFactory.create(this)
private val _songList = MutableStateFlow(listOf<Song>())
/** A list of [Song]s, sorted by the preferred [Sort], to be shown in the home view. */
@ -138,7 +136,7 @@ constructor(
* A list of [MusicType] corresponding to the current [Tab] configuration, excluding invisible
* [Tab]s.
*/
var currentTabTypes = makeTabTypes()
var currentTabTypes = homeGenerator.tabs()
private set
private val _currentTabType = MutableStateFlow(currentTabTypes[0])
@ -166,45 +164,38 @@ constructor(
val showOuter: Event<Outer>
get() = _showOuter
init {
homeSettings.registerListener(this)
}
override fun onCleared() {
super.onCleared()
homeSettings.unregisterListener(this)
generator.release()
homeGenerator.release()
}
override fun invalidate(type: MusicType, instructions: UpdateInstructions) {
override fun invalidateMusic(type: MusicType, instructions: UpdateInstructions) {
when (type) {
MusicType.SONGS -> {
_songList.value = generator.songs()
_songList.value = homeGenerator.songs()
_songInstructions.put(instructions)
}
MusicType.ALBUMS -> {
_albumList.value = generator.albums()
_albumList.value = homeGenerator.albums()
_albumInstructions.put(instructions)
}
MusicType.ARTISTS -> {
_artistList.value = generator.artists()
_artistList.value = homeGenerator.artists()
_artistInstructions.put(instructions)
}
MusicType.GENRES -> {
_genreList.value = generator.genres()
_genreList.value = homeGenerator.genres()
_genreInstructions.put(instructions)
}
MusicType.PLAYLISTS -> {
_playlistList.value = generator.playlists()
_playlistList.value = homeGenerator.playlists()
_playlistInstructions.put(instructions)
}
}
}
override fun onTabsChanged() {
// Tabs changed, update the current tabs and set up a re-create event.
currentTabTypes = makeTabTypes()
logD("Updating tabs: ${currentTabType.value}")
override fun invalidateTabs() {
currentTabTypes = homeGenerator.tabs()
_shouldRecreate.put(Unit)
}
@ -290,15 +281,6 @@ constructor(
fun showAbout() {
_showOuter.put(Outer.About)
}
/**
* Create a list of [MusicType]s representing a simpler version of the [Tab] configuration.
*
* @return A list of the [MusicType]s for each visible [Tab] in the configuration, ordered in
* the same way as the configuration.
*/
private fun makeTabTypes() =
homeSettings.homeTabs.filterIsInstance<Tab.Visible>().map { it.type }
}
sealed interface Outer {

View file

@ -37,40 +37,23 @@ class AdaptiveTabStrategy(context: Context, private val tabs: List<MusicType>) :
private val width = context.resources.configuration.smallestScreenWidthDp
override fun onConfigureTab(tab: TabLayout.Tab, position: Int) {
val icon: Int
val string: Int
when (tabs[position]) {
MusicType.SONGS -> {
icon = R.drawable.ic_song_24
string = R.string.lbl_songs
}
MusicType.ALBUMS -> {
icon = R.drawable.ic_album_24
string = R.string.lbl_albums
}
MusicType.ARTISTS -> {
icon = R.drawable.ic_artist_24
string = R.string.lbl_artists
}
MusicType.GENRES -> {
icon = R.drawable.ic_genre_24
string = R.string.lbl_genres
}
MusicType.PLAYLISTS -> {
icon = R.drawable.ic_playlist_24
string = R.string.lbl_playlists
}
val homeTab = tabs[position]
val icon = when (homeTab) {
MusicType.SONGS -> R.drawable.ic_song_24
MusicType.ALBUMS -> R.drawable.ic_album_24
MusicType.ARTISTS -> R.drawable.ic_artist_24
MusicType.GENRES -> R.drawable.ic_genre_24
MusicType.PLAYLISTS -> R.drawable.ic_playlist_24
}
// Use expected sw* size thresholds when choosing a configuration.
when {
// On small screens, only display an icon.
width < 370 -> tab.setIcon(icon).setContentDescription(string)
width < 370 -> tab.setIcon(icon).setContentDescription(homeTab.nameRes)
// On large screens, display an icon and text.
width < 600 -> tab.setText(string)
width < 600 -> tab.setText(homeTab.nameRes).setIcon(icon)
// On medium-size screens, display text.
else -> tab.setIcon(icon).setText(string)
else -> tab.setIcon(icon).setText(homeTab.nameRes)
}
}
}

View file

@ -19,6 +19,7 @@
package org.oxycblt.auxio.music
import org.oxycblt.auxio.IntegerTable
import org.oxycblt.auxio.R
/**
* General configuration enum to control what kind of music is being worked with.
@ -52,6 +53,16 @@ enum class MusicType {
PLAYLISTS -> IntegerTable.MUSIC_MODE_PLAYLISTS
}
val nameRes: Int
get() =
when (this) {
SONGS -> R.string.lbl_songs
ALBUMS -> R.string.lbl_albums
ARTISTS -> R.string.lbl_artists
GENRES -> R.string.lbl_genres
PLAYLISTS -> R.string.lbl_playlists
}
companion object {
/**
* Convert a [MusicType] integer representation into an instance.

View file

@ -44,8 +44,8 @@ import org.oxycblt.auxio.playback.formatDurationDs
import org.oxycblt.auxio.util.getPlural
sealed interface MediaSessionUID {
data class CategoryItem(val category: Category) : MediaSessionUID {
override fun toString() = "$ID_CATEGORY:${category.id}"
data class Tab(val node: TabNode) : MediaSessionUID {
override fun toString() = "$ID_CATEGORY:${node.id}"
}
data class SingleItem(val uid: Music.UID) : MediaSessionUID {
@ -66,7 +66,7 @@ sealed interface MediaSessionUID {
return null
}
return when (parts[0]) {
ID_CATEGORY -> CategoryItem(Category.fromString(parts[1]) ?: return null)
ID_CATEGORY -> Tab(TabNode.fromString(parts[1]) ?: return null)
ID_ITEM -> {
val uids = parts[1].split(">", limit = 2)
if (uids.size == 1) {
@ -148,12 +148,12 @@ private fun makeExtras(context: Context, vararg sugars: Sugar): Bundle {
return Bundle().apply { sugars.forEach { this.it(context) } }
}
fun Category.toMediaItem(context: Context): MediaItem {
fun TabNode.toMediaItem(context: Context): MediaItem {
val extras =
makeExtras(
context,
style(MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_CATEGORY_LIST_ITEM))
val mediaSessionUID = MediaSessionUID.CategoryItem(this)
val mediaSessionUID = MediaSessionUID.Tab(this)
val description =
MediaDescriptionCompat.Builder()
.setMediaId(mediaSessionUID.toString())

View file

@ -23,7 +23,7 @@ import android.support.v4.media.MediaBrowserCompat.MediaItem
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
import org.oxycblt.auxio.R
import org.oxycblt.auxio.home.list.HomeListGenerator
import org.oxycblt.auxio.home.HomeGenerator
import org.oxycblt.auxio.list.ListSettings
import org.oxycblt.auxio.list.adapter.UpdateInstructions
import org.oxycblt.auxio.list.sort.Sort
@ -35,8 +35,6 @@ import org.oxycblt.auxio.music.MusicRepository
import org.oxycblt.auxio.music.MusicType
import org.oxycblt.auxio.music.Playlist
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.device.DeviceLibrary
import org.oxycblt.auxio.music.user.UserLibrary
import org.oxycblt.auxio.search.SearchEngine
class MusicBrowser
@ -46,13 +44,13 @@ constructor(
private val musicRepository: MusicRepository,
private val searchEngine: SearchEngine,
private val listSettings: ListSettings,
homeGeneratorFactory: HomeListGenerator.Factory
) : MusicRepository.UpdateListener, HomeListGenerator.Invalidator {
homeGeneratorFactory: HomeGenerator.Factory
) : MusicRepository.UpdateListener, HomeGenerator.Invalidator {
interface Invalidator {
fun invalidateMusic(ids: Set<String>)
}
private val generator = homeGeneratorFactory.create(this)
private val homeGenerator = homeGeneratorFactory.create(this)
private var invalidator: Invalidator? = null
fun attach(invalidator: Invalidator) {
@ -64,26 +62,24 @@ constructor(
musicRepository.removeUpdateListener(this)
}
override fun invalidate(type: MusicType, instructions: UpdateInstructions) {
val category = when (type) {
MusicType.SONGS -> Category.Songs
MusicType.ALBUMS -> Category.Albums
MusicType.ARTISTS -> Category.Artists
MusicType.GENRES -> Category.Genres
MusicType.PLAYLISTS -> Category.Playlists
}
val id = MediaSessionUID.CategoryItem(category).toString()
override fun invalidateMusic(type: MusicType, instructions: UpdateInstructions) {
val id = MediaSessionUID.Tab(TabNode.Home(type)).toString()
invalidator?.invalidateMusic(setOf(id))
}
override fun invalidateTabs() {
for (i in 0..10) {
// TODO: Temporary bodge, move the amount parameter to a bundle extra
val rootId = MediaSessionUID.Tab(TabNode.Root(i)).toString()
val moreId = MediaSessionUID.Tab(TabNode.More(i)).toString()
invalidator?.invalidateMusic(setOf(rootId, moreId))
}
}
override fun onMusicChanges(changes: MusicRepository.Changes) {
val deviceLibrary = musicRepository.deviceLibrary
val invalidate = mutableSetOf<String>()
if (changes.deviceLibrary && deviceLibrary != null) {
Category.DEVICE_MUSIC.forEach {
invalidate.add(MediaSessionUID.CategoryItem(it).toString())
}
deviceLibrary.albums.forEach {
val id = MediaSessionUID.SingleItem(it.uid).toString()
invalidate.add(id)
@ -101,9 +97,6 @@ constructor(
}
val userLibrary = musicRepository.userLibrary
if (changes.userLibrary && userLibrary != null) {
Category.USER_MUSIC.forEach {
invalidate.add(MediaSessionUID.CategoryItem(it).toString())
}
userLibrary.playlists.forEach {
val id = MediaSessionUID.SingleItem(it.uid).toString()
invalidate.add(id)
@ -118,7 +111,7 @@ constructor(
fun getItem(mediaId: String): MediaItem? {
val music =
when (val uid = MediaSessionUID.fromString(mediaId)) {
is MediaSessionUID.CategoryItem -> return uid.category.toMediaItem(context)
is MediaSessionUID.Tab -> return uid.node.toMediaItem(context)
is MediaSessionUID.SingleItem ->
musicRepository.find(uid.uid)?.let { musicRepository.find(it.uid) }
is MediaSessionUID.ChildItem ->
@ -186,8 +179,8 @@ constructor(
id: String
): List<MediaItem>? {
return when (val mediaSessionUID = MediaSessionUID.fromString(id)) {
is MediaSessionUID.CategoryItem -> {
getCategoryMediaItems(mediaSessionUID.category)
is MediaSessionUID.Tab -> {
getCategoryMediaItems(mediaSessionUID.node)
}
is MediaSessionUID.SingleItem -> {
getChildMediaItems(mediaSessionUID.uid)
@ -202,25 +195,30 @@ constructor(
}
private fun getCategoryMediaItems(
category: Category
node: TabNode
) =
when (category) {
is Category.Root -> {
val base = Category.MUSIC.take(category.amount - 1)
if (base.size < Category.MUSIC.size) {
base + Category.More(Category.MUSIC.size - base.size)
when (node) {
is TabNode.Root -> {
val tabs = homeGenerator.tabs()
val base = tabs.take(node.amount - 1).map { TabNode.Home(it) }
if (base.size < tabs.size) {
base + TabNode.More(Category.MUSIC.size - base.size)
} else {
base
}
.map { it.toMediaItem(context) }
}
is Category.More ->
Category.MUSIC.takeLast(category.remainder).map { it.toMediaItem(context) }
is Category.Songs -> generator.songs().map { it.toMediaItem(context) }
is Category.Albums -> generator.albums().map { it.toMediaItem(context) }
is Category.Artists -> generator.artists().map { it.toMediaItem(context) }
is Category.Genres -> generator.genres().map { it.toMediaItem(context) }
is Category.Playlists -> generator.playlists().map { it.toMediaItem(context) }
is TabNode.More ->
homeGenerator.tabs().takeLast(node.remainder).map {
TabNode.Home(it).toMediaItem(context) }
is TabNode.Home ->
when (node.type) {
MusicType.SONGS -> homeGenerator.songs().map { it.toMediaItem(context, null) }
MusicType.ALBUMS -> homeGenerator.albums().map { it.toMediaItem(context) }
MusicType.ARTISTS -> homeGenerator.artists().map { it.toMediaItem(context) }
MusicType.GENRES -> homeGenerator.genres().map { it.toMediaItem(context) }
MusicType.PLAYLISTS -> homeGenerator.playlists().map { it.toMediaItem(context) }
}
}
private fun getChildMediaItems(uid: Music.UID): List<MediaItem>? {

View file

@ -0,0 +1,70 @@
package org.oxycblt.auxio.music.service
import org.oxycblt.auxio.R
import org.oxycblt.auxio.home.tabs.Tab
import org.oxycblt.auxio.music.MusicType
sealed class TabNode {
abstract val id: String
abstract val data: Int
abstract val nameRes: Int
abstract val bitmapRes: Int?
override fun toString() = "${id}/${data}"
data class Root(val amount: Int) : TabNode() {
override val id = ID
override val data = amount
override val nameRes = R.string.info_app_name
override val bitmapRes = null
companion object {
const val ID = "root"
}
}
data class More(val remainder: Int) : TabNode() {
override val id = ID
override val data = remainder
override val nameRes = R.string.lbl_more
override val bitmapRes = null
companion object {
const val ID = "more"
}
}
data class Home(val type: MusicType) : TabNode() {
override val id = ID
override val data = type.intCode
override val bitmapRes: Int
get() = when (type) {
MusicType.SONGS -> R.drawable.ic_song_bitmap_24
MusicType.ALBUMS -> R.drawable.ic_album_bitmap_24
MusicType.ARTISTS -> R.drawable.ic_artist_bitmap_24
MusicType.GENRES -> R.drawable.ic_genre_bitmap_24
MusicType.PLAYLISTS -> R.drawable.ic_playlist_bitmap_24
}
override val nameRes = type.nameRes
companion object {
const val ID = "home"
}
}
companion object {
fun fromString(str: String): TabNode? {
val split = str.split("/", limit = 2)
if (split.size != 2) {
return null
}
val data = split[1].toIntOrNull() ?: return null
return when (split[0]) {
Root.ID -> Root(data)
More.ID -> More(data)
Home.ID -> Home(MusicType.fromIntCode(data) ?: return null)
else -> null
}
}
}
}