Compare commits
24 commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
d394b76908 | ||
![]() |
9ef3c41bf8 | ||
![]() |
a7aae6a11e | ||
![]() |
4d28fe51b5 | ||
![]() |
2c87aa5830 | ||
![]() |
f245e33887 | ||
![]() |
b784250fed | ||
![]() |
5d1111b12a | ||
![]() |
e32c687c61 | ||
![]() |
34f7bc4886 | ||
![]() |
acd81d1c57 | ||
![]() |
1f5b202c5a | ||
![]() |
0ef2dafc29 | ||
![]() |
66fad791d5 | ||
![]() |
01bebfe63d | ||
![]() |
c108ec7e12 | ||
![]() |
e2b4f215cb | ||
![]() |
c7e18cdc6a | ||
![]() |
8e6b49c8ec | ||
![]() |
4accfaafaf | ||
![]() |
4917330633 | ||
![]() |
09588b3f38 | ||
![]() |
af812bc840 | ||
![]() |
556ac243f0 |
31 changed files with 844 additions and 530 deletions
|
@ -49,8 +49,9 @@ class AuxioService :
|
|||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
playbackFragment = playbackFragmentFactory.create(this, this)
|
||||
sessionToken = playbackFragment.token
|
||||
sessionToken = playbackFragment.attach()
|
||||
musicFragment = musicFragmentFactory.create(this, this, this)
|
||||
musicFragment.attach()
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
|
@ -80,7 +81,6 @@ class AuxioService :
|
|||
super.onDestroy()
|
||||
musicFragment.release()
|
||||
playbackFragment.release()
|
||||
sessionToken = null
|
||||
}
|
||||
|
||||
override fun onGetRoot(
|
||||
|
@ -88,9 +88,7 @@ class AuxioService :
|
|||
clientUid: Int,
|
||||
rootHints: Bundle?
|
||||
): BrowserRoot {
|
||||
val maximumRootChildLimit =
|
||||
rootHints?.getInt(MediaConstants.BROWSER_ROOT_HINTS_KEY_ROOT_CHILDREN_LIMIT, 4) ?: 4
|
||||
return musicFragment.getRoot(maximumRootChildLimit)
|
||||
return musicFragment.getRoot()
|
||||
}
|
||||
|
||||
override fun onLoadItem(itemId: String, result: Result<MediaItem>) {
|
||||
|
@ -98,7 +96,10 @@ class AuxioService :
|
|||
}
|
||||
|
||||
override fun onLoadChildren(parentId: String, result: Result<MutableList<MediaItem>>) {
|
||||
musicFragment.getChildren(parentId, result)
|
||||
val maximumRootChildLimit =
|
||||
browserRootHints?.getInt(MediaConstants.BROWSER_ROOT_HINTS_KEY_ROOT_CHILDREN_LIMIT, 4)
|
||||
?: 4
|
||||
musicFragment.getChildren(parentId, maximumRootChildLimit, result)
|
||||
}
|
||||
|
||||
override fun onSearch(query: String, extras: Bundle?, result: Result<MutableList<MediaItem>>) {
|
||||
|
|
|
@ -1,9 +1,26 @@
|
|||
/*
|
||||
* Copyright (c) 2024 Auxio Project
|
||||
* DetailGenerator.kt is part of Auxio.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.detail
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.StringRes
|
||||
import javax.inject.Inject
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.detail.list.DiscHeader
|
||||
import org.oxycblt.auxio.list.ListSettings
|
||||
import org.oxycblt.auxio.list.sort.Sort
|
||||
import org.oxycblt.auxio.music.Album
|
||||
|
@ -18,15 +35,20 @@ import org.oxycblt.auxio.music.Song
|
|||
import org.oxycblt.auxio.music.info.Disc
|
||||
import org.oxycblt.auxio.music.info.ReleaseType
|
||||
import org.oxycblt.auxio.util.logD
|
||||
import java.util.SortedMap
|
||||
import javax.inject.Inject
|
||||
|
||||
interface DetailGenerator {
|
||||
fun any(uid: Music.UID): Detail<out MusicParent>?
|
||||
|
||||
fun album(uid: Music.UID): Detail<Album>?
|
||||
|
||||
fun artist(uid: Music.UID): Detail<Artist>?
|
||||
|
||||
fun genre(uid: Music.UID): Detail<Genre>?
|
||||
|
||||
fun playlist(uid: Music.UID): Detail<Playlist>?
|
||||
|
||||
fun attach()
|
||||
|
||||
fun release()
|
||||
|
||||
interface Factory {
|
||||
|
@ -38,10 +60,10 @@ interface DetailGenerator {
|
|||
}
|
||||
}
|
||||
|
||||
class DetailGeneratorFactoryImpl @Inject constructor(
|
||||
private val listSettings: ListSettings,
|
||||
private val musicRepository: MusicRepository
|
||||
) : DetailGenerator.Factory {
|
||||
class DetailGeneratorFactoryImpl
|
||||
@Inject
|
||||
constructor(private val listSettings: ListSettings, private val musicRepository: MusicRepository) :
|
||||
DetailGenerator.Factory {
|
||||
override fun create(invalidator: DetailGenerator.Invalidator): DetailGenerator =
|
||||
DetailGeneratorImpl(invalidator, listSettings, musicRepository)
|
||||
}
|
||||
|
@ -51,7 +73,7 @@ private class DetailGeneratorImpl(
|
|||
private val listSettings: ListSettings,
|
||||
private val musicRepository: MusicRepository
|
||||
) : DetailGenerator, MusicRepository.UpdateListener, ListSettings.Listener {
|
||||
init {
|
||||
override fun attach() {
|
||||
listSettings.registerListener(this)
|
||||
musicRepository.addUpdateListener(this)
|
||||
}
|
||||
|
@ -102,7 +124,8 @@ private class DetailGeneratorImpl(
|
|||
val album = musicRepository.deviceLibrary?.findAlbum(uid) ?: return null
|
||||
val songs = listSettings.albumSongSort.songs(album.songs)
|
||||
val discs = songs.groupBy { it.disc }
|
||||
val section = if (discs.size > 1 || discs.keys.first() != null) {
|
||||
val section =
|
||||
if (discs.size > 1) {
|
||||
DetailSection.Discs(discs)
|
||||
} else {
|
||||
DetailSection.Songs(songs)
|
||||
|
@ -138,11 +161,12 @@ private class DetailGeneratorImpl(
|
|||
// implicit album list into the mapping.
|
||||
logD("Implicit albums present, adding to list")
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
(grouping as MutableMap<DetailSection.Albums.Category, Collection<Album>>)[DetailSection.Albums.Category.APPEARANCES] =
|
||||
artist.implicitAlbums
|
||||
(grouping as MutableMap<DetailSection.Albums.Category, Collection<Album>>)[
|
||||
DetailSection.Albums.Category.APPEARANCES] = artist.implicitAlbums
|
||||
}
|
||||
|
||||
val sections = grouping.mapTo(mutableListOf<DetailSection>()) { (category, albums) ->
|
||||
val sections =
|
||||
grouping.mapTo(mutableListOf<DetailSection>()) { (category, albums) ->
|
||||
DetailSection.Albums(category, ARTIST_ALBUM_SORT.albums(albums))
|
||||
}
|
||||
val songs = DetailSection.Songs(listSettings.artistSongSort.songs(artist.songs))
|
||||
|
@ -184,7 +208,8 @@ sealed interface DetailSection {
|
|||
override val stringRes = R.string.lbl_songs
|
||||
}
|
||||
|
||||
data class Albums(val category: Category, override val items: List<Album>) : PlainSection<Album>() {
|
||||
data class Albums(val category: Category, override val items: List<Album>) :
|
||||
PlainSection<Album>() {
|
||||
override val order = 1 + category.ordinal
|
||||
override val stringRes = category.stringRes
|
||||
|
||||
|
@ -203,7 +228,6 @@ sealed interface DetailSection {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
data class Songs(override val items: List<Song>) : PlainSection<Song>() {
|
||||
override val order = 12
|
||||
override val stringRes = R.string.lbl_songs
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* Copyright (c) 2024 Auxio Project
|
||||
* HomeModule.kt is part of Auxio.
|
||||
* DetailModule.kt is part of Auxio.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
|
|
@ -72,9 +72,6 @@ constructor(
|
|||
private val playbackSettings: PlaybackSettings,
|
||||
detailGeneratorFactory: DetailGenerator.Factory
|
||||
) : ViewModel(), DetailGenerator.Invalidator {
|
||||
private val detailGenerator = detailGeneratorFactory.create(this)
|
||||
|
||||
|
||||
private val _toShow = MutableEvent<Show>()
|
||||
/**
|
||||
* A [Show] command that is awaiting a view capable of responding to it. Null if none currently.
|
||||
|
@ -198,6 +195,12 @@ constructor(
|
|||
playbackSettings.inParentPlaybackMode
|
||||
?: PlaySong.FromPlaylist(unlikelyToBeNull(currentPlaylist.value))
|
||||
|
||||
private val detailGenerator = detailGeneratorFactory.create(this)
|
||||
|
||||
init {
|
||||
detailGenerator.attach()
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
detailGenerator.release()
|
||||
}
|
||||
|
@ -208,21 +211,18 @@ constructor(
|
|||
val album = detailGenerator.album(currentAlbum.value?.uid ?: return)
|
||||
refreshDetail(album, _currentAlbum, _albumSongList, _albumSongInstructions, replace)
|
||||
}
|
||||
|
||||
MusicType.ARTISTS -> {
|
||||
val artist = detailGenerator.artist(currentArtist.value?.uid ?: return)
|
||||
refreshDetail(artist, _currentArtist, _artistSongList, _artistSongInstructions, replace)
|
||||
refreshDetail(
|
||||
artist, _currentArtist, _artistSongList, _artistSongInstructions, replace)
|
||||
}
|
||||
|
||||
MusicType.GENRES -> {
|
||||
val genre = detailGenerator.genre(currentGenre.value?.uid ?: return)
|
||||
refreshDetail(genre, _currentGenre, _genreSongList, _genreSongInstructions, replace)
|
||||
}
|
||||
|
||||
MusicType.PLAYLISTS -> {
|
||||
refreshPlaylist(currentPlaylist.value?.uid ?: return)
|
||||
}
|
||||
|
||||
else -> error("Unexpected music type $type")
|
||||
}
|
||||
}
|
||||
|
@ -522,7 +522,6 @@ constructor(
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
private fun <T : MusicParent> refreshDetail(
|
||||
detail: Detail<T>?,
|
||||
parent: MutableStateFlow<T?>,
|
||||
|
@ -537,22 +536,21 @@ constructor(
|
|||
val newList = mutableListOf<Item>()
|
||||
var newInstructions: UpdateInstructions = UpdateInstructions.Diff
|
||||
for ((i, section) in detail.sections.withIndex()) {
|
||||
val items = when (section) {
|
||||
val items =
|
||||
when (section) {
|
||||
is DetailSection.PlainSection<*> -> {
|
||||
val header = if (section is DetailSection.Songs)
|
||||
SortHeader(section.stringRes) else BasicHeader(section.stringRes)
|
||||
val header =
|
||||
if (section is DetailSection.Songs) SortHeader(section.stringRes)
|
||||
else BasicHeader(section.stringRes)
|
||||
newList.add(Divider(header))
|
||||
newList.add(header)
|
||||
section.items
|
||||
}
|
||||
|
||||
is DetailSection.Discs -> {
|
||||
val header = BasicHeader(section.stringRes)
|
||||
val header = SortHeader(section.stringRes)
|
||||
newList.add(Divider(header))
|
||||
newList.add(header)
|
||||
section.discs.flatMap {
|
||||
listOf(DiscHeader(it.key)) + it.value
|
||||
}
|
||||
section.discs.flatMap { listOf(DiscHeader(it.key)) + it.value }
|
||||
}
|
||||
}
|
||||
// Currently only the final section (songs, which can be sorted) are invalidatable
|
||||
|
@ -564,16 +562,20 @@ constructor(
|
|||
newList.addAll(items)
|
||||
}
|
||||
parent.value = detail.parent
|
||||
list.value = newList
|
||||
instructions.put(newInstructions)
|
||||
list.value = newList
|
||||
}
|
||||
|
||||
private fun refreshPlaylist(uid: Music.UID, instructions: UpdateInstructions = UpdateInstructions.Diff) {
|
||||
private fun refreshPlaylist(
|
||||
uid: Music.UID,
|
||||
instructions: UpdateInstructions = UpdateInstructions.Diff
|
||||
) {
|
||||
logD("Refreshing playlist list")
|
||||
val edited = editedPlaylist.value
|
||||
if (edited == null) {
|
||||
val playlist = detailGenerator.playlist(uid)
|
||||
refreshDetail(playlist, _currentPlaylist, _playlistSongList, _playlistSongInstructions, null)
|
||||
refreshDetail(
|
||||
playlist, _currentPlaylist, _playlistSongList, _playlistSongInstructions, null)
|
||||
return
|
||||
}
|
||||
val list = mutableListOf<Item>()
|
||||
|
@ -583,8 +585,8 @@ constructor(
|
|||
list.add(header)
|
||||
list.addAll(edited)
|
||||
}
|
||||
_playlistSongList.value = list
|
||||
_playlistSongInstructions.put(instructions)
|
||||
_playlistSongList.value = list
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -35,10 +35,10 @@ import org.oxycblt.auxio.list.adapter.SimpleDiffCallback
|
|||
import org.oxycblt.auxio.music.Album
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.music.info.Disc
|
||||
import org.oxycblt.auxio.music.info.resolveNumber
|
||||
import org.oxycblt.auxio.playback.formatDurationMs
|
||||
import org.oxycblt.auxio.util.context
|
||||
import org.oxycblt.auxio.util.inflater
|
||||
import org.oxycblt.auxio.util.logD
|
||||
|
||||
/**
|
||||
* An [DetailListAdapter] implementing the header and sub-items for the [Album] detail view.
|
||||
|
@ -111,16 +111,10 @@ private class DiscHeaderViewHolder(private val binding: ItemDiscHeaderBinding) :
|
|||
*/
|
||||
fun bind(discHeader: DiscHeader) {
|
||||
val disc = discHeader.inner
|
||||
if (disc != null) {
|
||||
binding.discNumber.text = binding.context.getString(R.string.fmt_disc_no, disc.number)
|
||||
binding.discNumber.text = disc.resolveNumber(binding.context)
|
||||
binding.discName.apply {
|
||||
text = disc.name
|
||||
isGone = disc.name == null
|
||||
}
|
||||
} else {
|
||||
logD("Disc is null, defaulting to no disc")
|
||||
binding.discNumber.text = binding.context.getString(R.string.def_disc)
|
||||
binding.discName.isGone = true
|
||||
text = disc?.name
|
||||
isGone = disc?.name == null
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -32,6 +32,10 @@ import org.oxycblt.auxio.music.Song
|
|||
import org.oxycblt.auxio.util.logD
|
||||
|
||||
interface HomeGenerator {
|
||||
fun attach()
|
||||
|
||||
fun release()
|
||||
|
||||
fun songs(): List<Song>
|
||||
|
||||
fun albums(): List<Album>
|
||||
|
@ -44,8 +48,6 @@ interface HomeGenerator {
|
|||
|
||||
fun tabs(): List<MusicType>
|
||||
|
||||
fun release()
|
||||
|
||||
interface Invalidator {
|
||||
fun invalidateMusic(type: MusicType, instructions: UpdateInstructions)
|
||||
|
||||
|
@ -74,41 +76,14 @@ private class HomeGeneratorImpl(
|
|||
private val listSettings: ListSettings,
|
||||
private val musicRepository: MusicRepository,
|
||||
) : 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 {
|
||||
override fun attach() {
|
||||
homeSettings.registerListener(this)
|
||||
listSettings.registerListener(this)
|
||||
musicRepository.addUpdateListener(this)
|
||||
}
|
||||
|
||||
override fun release() {
|
||||
musicRepository.removeUpdateListener(this)
|
||||
listSettings.unregisterListener(this)
|
||||
homeSettings.unregisterListener(this)
|
||||
override fun onTabsChanged() {
|
||||
invalidator.invalidateTabs()
|
||||
}
|
||||
|
||||
override fun onHideCollaboratorsChanged() {
|
||||
|
@ -161,4 +136,31 @@ private class HomeGeneratorImpl(
|
|||
invalidator.invalidateMusic(MusicType.PLAYLISTS, UpdateInstructions.Diff)
|
||||
}
|
||||
}
|
||||
|
||||
override fun release() {
|
||||
musicRepository.removeUpdateListener(this)
|
||||
listSettings.unregisterListener(this)
|
||||
homeSettings.unregisterListener(this)
|
||||
}
|
||||
|
||||
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 }
|
||||
}
|
||||
|
|
|
@ -52,8 +52,6 @@ constructor(
|
|||
private val playbackSettings: PlaybackSettings,
|
||||
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. */
|
||||
val songList: StateFlow<List<Song>>
|
||||
|
@ -131,6 +129,8 @@ constructor(
|
|||
val playlistSort: Sort
|
||||
get() = listSettings.playlistSort
|
||||
|
||||
private val homeGenerator = homeGeneratorFactory.create(this)
|
||||
|
||||
/**
|
||||
* A list of [MusicType] corresponding to the current [Tab] configuration, excluding invisible
|
||||
* [Tab]s.
|
||||
|
@ -163,6 +163,10 @@ constructor(
|
|||
val showOuter: Event<Outer>
|
||||
get() = _showOuter
|
||||
|
||||
init {
|
||||
homeGenerator.attach()
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
homeGenerator.release()
|
||||
|
@ -171,24 +175,24 @@ constructor(
|
|||
override fun invalidateMusic(type: MusicType, instructions: UpdateInstructions) {
|
||||
when (type) {
|
||||
MusicType.SONGS -> {
|
||||
_songList.value = homeGenerator.songs()
|
||||
_songInstructions.put(instructions)
|
||||
_songList.value = homeGenerator.songs()
|
||||
}
|
||||
MusicType.ALBUMS -> {
|
||||
_albumList.value = homeGenerator.albums()
|
||||
_albumInstructions.put(instructions)
|
||||
_albumList.value = homeGenerator.albums()
|
||||
}
|
||||
MusicType.ARTISTS -> {
|
||||
_artistList.value = homeGenerator.artists()
|
||||
_artistInstructions.put(instructions)
|
||||
_artistList.value = homeGenerator.artists()
|
||||
}
|
||||
MusicType.GENRES -> {
|
||||
_genreList.value = homeGenerator.genres()
|
||||
_genreInstructions.put(instructions)
|
||||
_genreList.value = homeGenerator.genres()
|
||||
}
|
||||
MusicType.PLAYLISTS -> {
|
||||
_playlistList.value = homeGenerator.playlists()
|
||||
_playlistInstructions.put(instructions)
|
||||
_playlistList.value = homeGenerator.playlists()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -52,7 +52,7 @@ class AdaptiveTabStrategy(context: Context, private val tabs: List<MusicType>) :
|
|||
// On small screens, only display an icon.
|
||||
width < 370 -> tab.setIcon(icon).setContentDescription(homeTab.nameRes)
|
||||
// On large screens, display an icon and text.
|
||||
width < 600 -> tab.setText(homeTab.nameRes).setIcon(icon)
|
||||
width < 600 -> tab.setText(homeTab.nameRes)
|
||||
// On medium-size screens, display text.
|
||||
else -> tab.setIcon(icon).setText(homeTab.nameRes)
|
||||
}
|
||||
|
|
|
@ -46,12 +46,19 @@ interface ListSettings : Settings<ListSettings.Listener> {
|
|||
|
||||
interface Listener {
|
||||
fun onSongSortChanged() {}
|
||||
|
||||
fun onAlbumSortChanged() {}
|
||||
|
||||
fun onAlbumSongSortChanged() {}
|
||||
|
||||
fun onArtistSortChanged() {}
|
||||
|
||||
fun onArtistSongSortChanged() {}
|
||||
|
||||
fun onGenreSortChanged() {}
|
||||
|
||||
fun onGenreSongSortChanged() {}
|
||||
|
||||
fun onPlaylistSortChanged() {}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,6 +18,8 @@
|
|||
|
||||
package org.oxycblt.auxio.music.info
|
||||
|
||||
import android.content.Context
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.list.Item
|
||||
|
||||
/**
|
||||
|
@ -34,3 +36,7 @@ class Disc(val number: Int, val name: String?) : Item, Comparable<Disc> {
|
|||
|
||||
override fun compareTo(other: Disc) = number.compareTo(other.number)
|
||||
}
|
||||
|
||||
fun Disc?.resolveNumber(context: Context) =
|
||||
this?.run { context.getString(R.string.fmt_disc_no, number) }
|
||||
?: context.getString(R.string.def_disc)
|
||||
|
|
|
@ -80,7 +80,7 @@ private constructor(
|
|||
.newWakeLock(
|
||||
PowerManager.PARTIAL_WAKE_LOCK, BuildConfig.APPLICATION_ID + ":IndexingComponent")
|
||||
|
||||
init {
|
||||
fun attach() {
|
||||
musicSettings.registerListener(this)
|
||||
musicRepository.addUpdateListener(this)
|
||||
musicRepository.addIndexingListener(this)
|
||||
|
|
|
@ -18,17 +18,12 @@
|
|||
|
||||
package org.oxycblt.auxio.music.service
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.graphics.BitmapFactory
|
||||
import android.os.Bundle
|
||||
import android.support.v4.media.MediaBrowserCompat.MediaItem
|
||||
import android.support.v4.media.MediaDescriptionCompat
|
||||
import android.view.MenuInflater
|
||||
import androidx.annotation.MenuRes
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.view.menu.MenuBuilder
|
||||
import androidx.core.view.children
|
||||
import androidx.media.utils.MediaConstants
|
||||
import org.oxycblt.auxio.BuildConfig
|
||||
import org.oxycblt.auxio.R
|
||||
|
@ -83,46 +78,6 @@ sealed interface MediaSessionUID {
|
|||
}
|
||||
}
|
||||
|
||||
enum class BrowserOption(val actionId: String, val labelRes: Int, val iconRes: Int) {
|
||||
PLAY(BuildConfig.APPLICATION_ID + ".menu.PLAY", R.string.lbl_play, R.drawable.ic_play_24),
|
||||
SHUFFLE(
|
||||
BuildConfig.APPLICATION_ID + ".menu.SHUFFLE",
|
||||
R.string.lbl_shuffle,
|
||||
R.drawable.ic_shuffle_off_24),
|
||||
PLAY_NEXT(
|
||||
BuildConfig.APPLICATION_ID + ".menu.PLAY_NEXT",
|
||||
R.string.lbl_play_next,
|
||||
R.drawable.ic_play_next_24),
|
||||
ADD_TO_QUEUE(
|
||||
BuildConfig.APPLICATION_ID + ".menu.ADD_TO_QUEUE",
|
||||
R.string.lbl_queue_add,
|
||||
R.drawable.ic_queue_add_24),
|
||||
DETAILS(
|
||||
BuildConfig.APPLICATION_ID + ".menu.DETAILS",
|
||||
R.string.lbl_parent_detail,
|
||||
R.drawable.ic_details_24),
|
||||
ALBUM_DETAILS(
|
||||
BuildConfig.APPLICATION_ID + ".menu.ALBUM_DETAILS",
|
||||
R.string.lbl_album_details,
|
||||
R.drawable.ic_album_24),
|
||||
ARTIST_DETAILS(
|
||||
BuildConfig.APPLICATION_ID + ".menu.ARTIST_DETAILS",
|
||||
R.string.lbl_artist_details,
|
||||
R.drawable.ic_artist_24);
|
||||
|
||||
companion object {
|
||||
val ITEM_ID_MAP =
|
||||
mapOf(
|
||||
R.id.action_play to PLAY,
|
||||
R.id.action_shuffle to SHUFFLE,
|
||||
R.id.action_play_next to PLAY_NEXT,
|
||||
R.id.action_queue_add to ADD_TO_QUEUE,
|
||||
R.id.action_detail to DETAILS,
|
||||
R.id.action_album_details to ALBUM_DETAILS,
|
||||
R.id.action_artist_details to ARTIST_DETAILS)
|
||||
}
|
||||
}
|
||||
|
||||
typealias Sugar = Bundle.(Context) -> Unit
|
||||
|
||||
fun header(@StringRes nameRes: Int): Sugar = {
|
||||
|
@ -138,16 +93,6 @@ private fun style(style: Int): Sugar = {
|
|||
putInt(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_SINGLE_ITEM, style)
|
||||
}
|
||||
|
||||
private fun menu(@MenuRes res: Int): Sugar = { context ->
|
||||
@SuppressLint("RestrictedApi") val builder = MenuBuilder(context)
|
||||
MenuInflater(context).inflate(res, builder)
|
||||
val menuIds =
|
||||
builder.children.mapNotNullTo(ArrayList()) {
|
||||
BrowserOption.ITEM_ID_MAP[it.itemId]?.actionId
|
||||
}
|
||||
putStringArrayList(MediaConstants.DESCRIPTION_EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ID_LIST, menuIds)
|
||||
}
|
||||
|
||||
private fun makeExtras(context: Context, vararg sugars: Sugar): Bundle {
|
||||
return Bundle().apply { sugars.forEach { this.it(context) } }
|
||||
}
|
||||
|
@ -181,7 +126,7 @@ fun Song.toMediaDescription(
|
|||
} else {
|
||||
MediaSessionUID.ChildItem(parent.uid, uid)
|
||||
}
|
||||
val extras = makeExtras(context, *sugar, menu(R.menu.song))
|
||||
val extras = makeExtras(context, *sugar)
|
||||
return MediaDescriptionCompat.Builder()
|
||||
.setMediaId(mediaSessionUID.toString())
|
||||
.setTitle(name.resolve(context))
|
||||
|
@ -212,7 +157,7 @@ fun Album.toMediaItem(
|
|||
} else {
|
||||
MediaSessionUID.ChildItem(parent.uid, uid)
|
||||
}
|
||||
val extras = makeExtras(context, *sugar, menu(R.menu.album))
|
||||
val extras = makeExtras(context, *sugar)
|
||||
val counts = context.getPlural(R.plurals.fmt_song_count, songs.size)
|
||||
val description =
|
||||
MediaDescriptionCompat.Builder()
|
||||
|
@ -241,7 +186,7 @@ fun Artist.toMediaItem(context: Context, vararg sugar: Sugar): MediaItem {
|
|||
} else {
|
||||
context.getString(R.string.def_song_count)
|
||||
})
|
||||
val extras = makeExtras(context, *sugar, menu(R.menu.parent))
|
||||
val extras = makeExtras(context, *sugar)
|
||||
val description =
|
||||
MediaDescriptionCompat.Builder()
|
||||
.setMediaId(mediaSessionUID.toString())
|
||||
|
@ -262,7 +207,7 @@ fun Genre.toMediaItem(context: Context, vararg sugar: Sugar): MediaItem {
|
|||
} else {
|
||||
context.getString(R.string.def_song_count)
|
||||
}
|
||||
val extras = makeExtras(context, *sugar, menu(R.menu.parent))
|
||||
val extras = makeExtras(context, *sugar)
|
||||
val description =
|
||||
MediaDescriptionCompat.Builder()
|
||||
.setMediaId(mediaSessionUID.toString())
|
||||
|
@ -282,7 +227,7 @@ fun Playlist.toMediaItem(context: Context, vararg sugar: Sugar): MediaItem {
|
|||
} else {
|
||||
context.getString(R.string.def_song_count)
|
||||
}
|
||||
val extras = makeExtras(context, *sugar, menu(R.menu.playlist))
|
||||
val extras = makeExtras(context, *sugar)
|
||||
val description =
|
||||
MediaDescriptionCompat.Builder()
|
||||
.setMediaId(mediaSessionUID.toString())
|
||||
|
|
|
@ -24,13 +24,8 @@ import javax.inject.Inject
|
|||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.detail.DetailGenerator
|
||||
import org.oxycblt.auxio.detail.DetailSection
|
||||
import org.oxycblt.auxio.detail.list.SortHeader
|
||||
import org.oxycblt.auxio.home.HomeGenerator
|
||||
import org.oxycblt.auxio.list.BasicHeader
|
||||
import org.oxycblt.auxio.list.Divider
|
||||
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
|
||||
|
@ -39,6 +34,7 @@ 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.info.resolveNumber
|
||||
import org.oxycblt.auxio.search.SearchEngine
|
||||
|
||||
class MusicBrowser
|
||||
|
@ -76,6 +72,11 @@ private constructor(
|
|||
private val homeGenerator = homeGeneratorFactory.create(this)
|
||||
private val detailGenerator = detailGeneratorFactory.create(this)
|
||||
|
||||
fun attach() {
|
||||
homeGenerator.attach()
|
||||
detailGenerator.attach()
|
||||
}
|
||||
|
||||
fun release() {
|
||||
homeGenerator.release()
|
||||
detailGenerator.release()
|
||||
|
@ -87,18 +88,16 @@ private constructor(
|
|||
}
|
||||
|
||||
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()
|
||||
val rootId = MediaSessionUID.Tab(TabNode.Root).toString()
|
||||
val moreId = MediaSessionUID.Tab(TabNode.More).toString()
|
||||
invalidator.invalidateMusic(setOf(rootId, moreId))
|
||||
}
|
||||
}
|
||||
|
||||
override fun invalidate(type: MusicType, replace: Int?) {
|
||||
val deviceLibrary = musicRepository.deviceLibrary ?: return
|
||||
val userLibrary = musicRepository.userLibrary ?: return
|
||||
val music = when (type) {
|
||||
val music =
|
||||
when (type) {
|
||||
MusicType.ALBUMS -> deviceLibrary.albums
|
||||
MusicType.ARTISTS -> deviceLibrary.artists
|
||||
MusicType.GENRES -> deviceLibrary.genres
|
||||
|
@ -133,14 +132,13 @@ private constructor(
|
|||
}
|
||||
}
|
||||
|
||||
fun getChildren(parentId: String): List<MediaItem>? {
|
||||
fun getChildren(parentId: String, maxTabs: Int): List<MediaItem>? {
|
||||
val deviceLibrary = musicRepository.deviceLibrary
|
||||
val userLibrary = musicRepository.userLibrary
|
||||
if (deviceLibrary == null || userLibrary == null) {
|
||||
return listOf()
|
||||
}
|
||||
|
||||
return getMediaItemList(parentId)
|
||||
return getMediaItemList(parentId, maxTabs)
|
||||
}
|
||||
|
||||
suspend fun search(query: String): MutableList<MediaItem> {
|
||||
|
@ -179,10 +177,10 @@ private constructor(
|
|||
return music
|
||||
}
|
||||
|
||||
private fun getMediaItemList(id: String): List<MediaItem>? {
|
||||
private fun getMediaItemList(id: String, maxTabs: Int): List<MediaItem>? {
|
||||
return when (val mediaSessionUID = MediaSessionUID.fromString(id)) {
|
||||
is MediaSessionUID.Tab -> {
|
||||
getCategoryMediaItems(mediaSessionUID.node)
|
||||
getCategoryMediaItems(mediaSessionUID.node, maxTabs)
|
||||
}
|
||||
is MediaSessionUID.SingleItem -> {
|
||||
getChildMediaItems(mediaSessionUID.uid)
|
||||
|
@ -196,21 +194,21 @@ private constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun getCategoryMediaItems(node: TabNode) =
|
||||
private fun getCategoryMediaItems(node: TabNode, maxTabs: Int) =
|
||||
when (node) {
|
||||
is TabNode.Root -> {
|
||||
val tabs = homeGenerator.tabs()
|
||||
val base = tabs.take(node.amount - 1).map { TabNode.Home(it) }
|
||||
val base = tabs.take(maxTabs - 1).map { TabNode.Home(it) }
|
||||
if (base.size < tabs.size) {
|
||||
base + TabNode.More(tabs.size - base.size)
|
||||
base + TabNode.More
|
||||
} else {
|
||||
base
|
||||
}
|
||||
.map { it.toMediaItem(context) }
|
||||
}
|
||||
is TabNode.More ->
|
||||
homeGenerator.tabs().takeLast(node.remainder).map {
|
||||
TabNode.Home(it).toMediaItem(context)
|
||||
is TabNode.More -> {
|
||||
val tabs = homeGenerator.tabs()
|
||||
tabs.takeLast(tabs.size - maxTabs).map { TabNode.Home(it).toMediaItem(context) }
|
||||
}
|
||||
is TabNode.Home ->
|
||||
when (node.type) {
|
||||
|
@ -226,19 +224,16 @@ private constructor(
|
|||
val detail = detailGenerator.any(uid) ?: return null
|
||||
return detail.sections.flatMap { section ->
|
||||
when (section) {
|
||||
is DetailSection.Songs -> section.items.map { it.toMediaItem(context, null, header(section.stringRes)) }
|
||||
is DetailSection.Albums -> section.items.map { it.toMediaItem(context, null, header(section.stringRes)) }
|
||||
is DetailSection.Artists -> section.items.map { it.toMediaItem(context, header(section.stringRes)) }
|
||||
is DetailSection.Discs -> section.discs.flatMap {
|
||||
section.discs.flatMap { entry ->
|
||||
val disc = entry.key
|
||||
val discString = if (disc != null) {
|
||||
context.getString(R.string.fmt_disc_no, disc.number)
|
||||
} else {
|
||||
context.getString(R.string.def_disc)
|
||||
}
|
||||
entry.value.map { it.toMediaItem(context, null, header(discString)) }
|
||||
}
|
||||
is DetailSection.Songs ->
|
||||
section.items.map { it.toMediaItem(context, null, header(section.stringRes)) }
|
||||
is DetailSection.Albums ->
|
||||
section.items.map { it.toMediaItem(context, null, header(section.stringRes)) }
|
||||
is DetailSection.Artists ->
|
||||
section.items.map { it.toMediaItem(context, header(section.stringRes)) }
|
||||
is DetailSection.Discs ->
|
||||
section.discs.flatMap { (disc, songs) ->
|
||||
val discString = disc.resolveNumber(context)
|
||||
songs.map { it.toMediaItem(context, null, header(discString)) }
|
||||
}
|
||||
else -> error("Unknown section type: $section")
|
||||
}
|
||||
|
|
|
@ -23,7 +23,6 @@ import android.os.Bundle
|
|||
import android.support.v4.media.MediaBrowserCompat.MediaItem
|
||||
import androidx.media.MediaBrowserServiceCompat.BrowserRoot
|
||||
import androidx.media.MediaBrowserServiceCompat.Result
|
||||
import androidx.media.utils.MediaConstants
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
@ -75,6 +74,11 @@ constructor(
|
|||
fun invalidateMusic(mediaId: String)
|
||||
}
|
||||
|
||||
fun attach() {
|
||||
indexer.attach()
|
||||
musicBrowser.attach()
|
||||
}
|
||||
|
||||
fun release() {
|
||||
dispatchJob.cancel()
|
||||
musicBrowser.release()
|
||||
|
@ -95,30 +99,17 @@ constructor(
|
|||
indexer.createNotification(post)
|
||||
}
|
||||
|
||||
fun getRoot(maxItems: Int) =
|
||||
BrowserRoot(
|
||||
MediaSessionUID.Tab(TabNode.Root(maxItems)).toString(),
|
||||
Bundle().apply {
|
||||
val actions =
|
||||
BrowserOption.entries.mapTo(ArrayList()) {
|
||||
Bundle().apply {
|
||||
putString(
|
||||
MediaConstants.EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ID, it.actionId)
|
||||
putString(
|
||||
MediaConstants.EXTRAS_KEY_CUSTOM_BROWSER_ACTION_LABEL,
|
||||
context.getString(it.labelRes))
|
||||
}
|
||||
}
|
||||
putParcelableArrayList(
|
||||
MediaConstants.BROWSER_SERVICE_EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ROOT_LIST,
|
||||
actions)
|
||||
})
|
||||
fun getRoot() = BrowserRoot(MediaSessionUID.Tab(TabNode.Root).toString(), Bundle())
|
||||
|
||||
fun getItem(mediaId: String, result: Result<MediaItem>) =
|
||||
result.dispatch { musicBrowser.getItem(mediaId) }
|
||||
result.dispatch {
|
||||
musicBrowser.getItem(
|
||||
mediaId,
|
||||
)
|
||||
}
|
||||
|
||||
fun getChildren(mediaId: String, result: Result<MutableList<MediaItem>>) =
|
||||
result.dispatch { musicBrowser.getChildren(mediaId)?.toMutableList() }
|
||||
fun getChildren(mediaId: String, maxTabs: Int, result: Result<MutableList<MediaItem>>) =
|
||||
result.dispatch { musicBrowser.getChildren(mediaId, maxTabs)?.toMutableList() }
|
||||
|
||||
fun search(query: String, result: Result<MutableList<MediaItem>>) =
|
||||
result.dispatchAsync { musicBrowser.search(query) }
|
||||
|
|
|
@ -23,37 +23,27 @@ 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}"
|
||||
override fun toString() = id
|
||||
|
||||
data class Root(val amount: Int) : TabNode() {
|
||||
override val id = ID
|
||||
override val data = amount
|
||||
data object Root : TabNode() {
|
||||
override val id = "root"
|
||||
override val nameRes = R.string.info_app_name
|
||||
override val bitmapRes = null
|
||||
|
||||
companion object {
|
||||
const val ID = "root"
|
||||
}
|
||||
override fun toString() = id
|
||||
}
|
||||
|
||||
data class More(val remainder: Int) : TabNode() {
|
||||
override val id = ID
|
||||
override val data = remainder
|
||||
data object More : TabNode() {
|
||||
override val id = "more"
|
||||
override val nameRes = R.string.lbl_more
|
||||
override val bitmapRes = null
|
||||
|
||||
companion object {
|
||||
const val ID = "more"
|
||||
}
|
||||
override val bitmapRes = R.drawable.ic_more_bitmap_24
|
||||
}
|
||||
|
||||
data class Home(val type: MusicType) : TabNode() {
|
||||
override val id = ID
|
||||
override val data = type.intCode
|
||||
override val id = "$ID/${type.intCode}"
|
||||
override val bitmapRes: Int
|
||||
get() =
|
||||
when (type) {
|
||||
|
@ -73,15 +63,15 @@ sealed class TabNode {
|
|||
|
||||
companion object {
|
||||
fun fromString(str: String): TabNode? {
|
||||
val split = str.split("/", limit = 2)
|
||||
if (split.size != 2) {
|
||||
return null
|
||||
return when {
|
||||
str == Root.id -> Root
|
||||
str == More.id -> More
|
||||
str.startsWith(Home.ID) -> {
|
||||
val split = str.split("/")
|
||||
if (split.size != 2) return null
|
||||
val intCode = split[1].toIntOrNull() ?: return null
|
||||
Home(MusicType.fromIntCode(intCode) ?: 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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.playback.service
|
||||
package org.oxycblt.auxio.playback.player
|
||||
|
||||
import androidx.media3.common.C
|
||||
import androidx.media3.exoplayer.source.ShuffleOrder
|
|
@ -0,0 +1,227 @@
|
|||
/*
|
||||
* Copyright (c) 2024 Auxio Project
|
||||
* GaplessQueuer.kt is part of Auxio.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.playback.player
|
||||
|
||||
import androidx.media3.common.C
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.common.Player.RepeatMode
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import javax.inject.Inject
|
||||
import org.oxycblt.auxio.playback.PlaybackSettings
|
||||
|
||||
/** */
|
||||
class GaplessQueuer
|
||||
private constructor(
|
||||
private val exoPlayer: ExoPlayer,
|
||||
private val listener: Queuer.Listener,
|
||||
private val playbackSettings: PlaybackSettings
|
||||
) : Queuer, PlaybackSettings.Listener, Player.Listener {
|
||||
class Factory @Inject constructor(private val playbackSettings: PlaybackSettings) :
|
||||
Queuer.Factory {
|
||||
override fun create(exoPlayer: ExoPlayer, listener: Queuer.Listener) =
|
||||
GaplessQueuer(exoPlayer, listener, playbackSettings)
|
||||
}
|
||||
|
||||
override val currentMediaItem: MediaItem? = exoPlayer.currentMediaItem
|
||||
override val currentMediaItemIndex: Int = exoPlayer.currentMediaItemIndex
|
||||
override val shuffleModeEnabled: Boolean = exoPlayer.shuffleModeEnabled
|
||||
@get:RepeatMode
|
||||
override var repeatMode: Int = exoPlayer.repeatMode
|
||||
set(value) {
|
||||
field = value
|
||||
exoPlayer.repeatMode = value
|
||||
updatePauseOnRepeat()
|
||||
}
|
||||
|
||||
override fun attach() {
|
||||
playbackSettings.registerListener(this)
|
||||
exoPlayer.addListener(this)
|
||||
}
|
||||
|
||||
override fun release() {
|
||||
playbackSettings.unregisterListener(this)
|
||||
exoPlayer.removeListener(this)
|
||||
}
|
||||
|
||||
override fun computeHeap(): List<MediaItem> {
|
||||
return (0 until exoPlayer.mediaItemCount).map { exoPlayer.getMediaItemAt(it) }
|
||||
}
|
||||
|
||||
override fun computeMapping(): List<Int> {
|
||||
val timeline = exoPlayer.currentTimeline
|
||||
if (timeline.isEmpty) {
|
||||
return emptyList()
|
||||
}
|
||||
val queue = mutableListOf<Int>()
|
||||
|
||||
// Add the active queue item.
|
||||
val currentMediaItemIndex = currentMediaItemIndex
|
||||
queue.add(currentMediaItemIndex)
|
||||
|
||||
// Fill queue alternating with next and/or previous queue items.
|
||||
var firstMediaItemIndex = currentMediaItemIndex
|
||||
var lastMediaItemIndex = currentMediaItemIndex
|
||||
val shuffleModeEnabled = exoPlayer.shuffleModeEnabled
|
||||
while ((firstMediaItemIndex != C.INDEX_UNSET || lastMediaItemIndex != C.INDEX_UNSET)) {
|
||||
// Begin with next to have a longer tail than head if an even sized queue needs to be
|
||||
// trimmed.
|
||||
if (lastMediaItemIndex != C.INDEX_UNSET) {
|
||||
lastMediaItemIndex =
|
||||
timeline.getNextWindowIndex(
|
||||
lastMediaItemIndex, Player.REPEAT_MODE_OFF, shuffleModeEnabled)
|
||||
if (lastMediaItemIndex != C.INDEX_UNSET) {
|
||||
queue.add(lastMediaItemIndex)
|
||||
}
|
||||
}
|
||||
if (firstMediaItemIndex != C.INDEX_UNSET) {
|
||||
firstMediaItemIndex =
|
||||
timeline.getPreviousWindowIndex(
|
||||
firstMediaItemIndex, Player.REPEAT_MODE_OFF, shuffleModeEnabled)
|
||||
if (firstMediaItemIndex != C.INDEX_UNSET) {
|
||||
queue.add(0, firstMediaItemIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return queue
|
||||
}
|
||||
|
||||
override fun computeFirstMediaItemIndex() =
|
||||
exoPlayer.currentTimeline.getFirstWindowIndex(exoPlayer.shuffleModeEnabled)
|
||||
|
||||
override fun goto(mediaItemIndex: Int) = exoPlayer.seekTo(mediaItemIndex, C.TIME_UNSET)
|
||||
|
||||
override fun seekToNext() = exoPlayer.seekToNext()
|
||||
|
||||
override fun hasNextMediaItem() = exoPlayer.hasNextMediaItem()
|
||||
|
||||
override fun seekToPrevious() = exoPlayer.seekToPrevious()
|
||||
|
||||
override fun seekToPreviousMediaItem() = exoPlayer.seekToPreviousMediaItem()
|
||||
|
||||
override fun hasPreviousMediaItem() = exoPlayer.hasPreviousMediaItem()
|
||||
|
||||
override fun prepareNew(mediaItems: List<MediaItem>, startIndex: Int?, shuffled: Boolean) {
|
||||
exoPlayer.shuffleModeEnabled = shuffled
|
||||
exoPlayer.setMediaItems(mediaItems)
|
||||
if (shuffled) {
|
||||
exoPlayer.setShuffleOrder(BetterShuffleOrder(mediaItems.size, startIndex ?: -1))
|
||||
}
|
||||
val target = startIndex ?: exoPlayer.currentTimeline.getFirstWindowIndex(shuffled)
|
||||
exoPlayer.seekTo(target, C.TIME_UNSET)
|
||||
exoPlayer.prepare()
|
||||
}
|
||||
|
||||
override fun prepareSaved(
|
||||
mediaItems: List<MediaItem>,
|
||||
mapping: List<Int>,
|
||||
index: Int,
|
||||
shuffled: Boolean
|
||||
) {
|
||||
exoPlayer.setMediaItems(mediaItems)
|
||||
if (shuffled) {
|
||||
exoPlayer.shuffleModeEnabled = true
|
||||
exoPlayer.setShuffleOrder(BetterShuffleOrder(mapping.toIntArray()))
|
||||
} else {
|
||||
exoPlayer.shuffleModeEnabled = false
|
||||
}
|
||||
exoPlayer.seekTo(index, C.TIME_UNSET)
|
||||
exoPlayer.prepare()
|
||||
}
|
||||
|
||||
override fun discard() {
|
||||
exoPlayer.setMediaItems(emptyList())
|
||||
}
|
||||
|
||||
override fun addTopMediaItems(mediaItems: List<MediaItem>) {
|
||||
val currTimeline = exoPlayer.currentTimeline
|
||||
val nextIndex =
|
||||
if (currTimeline.isEmpty) {
|
||||
C.INDEX_UNSET
|
||||
} else {
|
||||
currTimeline.getNextWindowIndex(
|
||||
exoPlayer.currentMediaItemIndex,
|
||||
Player.REPEAT_MODE_OFF,
|
||||
exoPlayer.shuffleModeEnabled)
|
||||
}
|
||||
|
||||
if (nextIndex == C.INDEX_UNSET) {
|
||||
exoPlayer.addMediaItems(mediaItems)
|
||||
} else {
|
||||
exoPlayer.addMediaItems(nextIndex, mediaItems)
|
||||
}
|
||||
}
|
||||
|
||||
override fun addBottomMediaItems(mediaItems: List<MediaItem>) {
|
||||
exoPlayer.addMediaItems(mediaItems)
|
||||
}
|
||||
|
||||
override fun moveMediaItem(fromIndex: Int, toIndex: Int) {
|
||||
// ExoPlayer does not actually update it's ShuffleOrder when moving items. Retain a
|
||||
// semblance of "normalcy" by doing a weird no-op swap that actually moves the item.
|
||||
when {
|
||||
fromIndex > toIndex -> {
|
||||
exoPlayer.moveMediaItem(fromIndex, toIndex)
|
||||
exoPlayer.moveMediaItem(toIndex + 1, fromIndex)
|
||||
}
|
||||
toIndex > fromIndex -> {
|
||||
exoPlayer.moveMediaItem(fromIndex, toIndex)
|
||||
exoPlayer.moveMediaItem(toIndex - 1, fromIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun removeMediaItem(index: Int) = exoPlayer.removeMediaItem(index)
|
||||
|
||||
override fun shuffled(shuffled: Boolean) {
|
||||
exoPlayer.setShuffleModeEnabled(shuffled)
|
||||
if (exoPlayer.shuffleModeEnabled) {
|
||||
// Have to manually refresh the shuffle seed and anchor it to the new current songs
|
||||
exoPlayer.setShuffleOrder(
|
||||
BetterShuffleOrder(exoPlayer.mediaItemCount, exoPlayer.currentMediaItemIndex))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPauseOnRepeatChanged() {
|
||||
super.onPauseOnRepeatChanged()
|
||||
updatePauseOnRepeat()
|
||||
}
|
||||
|
||||
private fun updatePauseOnRepeat() {
|
||||
exoPlayer.pauseAtEndOfMediaItems =
|
||||
exoPlayer.repeatMode == Player.REPEAT_MODE_ONE && playbackSettings.pauseOnRepeat
|
||||
}
|
||||
|
||||
override fun onPlaybackStateChanged(playbackState: Int) {
|
||||
super.onPlaybackStateChanged(playbackState)
|
||||
|
||||
if (playbackState == Player.STATE_ENDED && exoPlayer.repeatMode == Player.REPEAT_MODE_OFF) {
|
||||
goto(0)
|
||||
exoPlayer.pause()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
|
||||
super.onMediaItemTransition(mediaItem, reason)
|
||||
if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_AUTO) {
|
||||
listener.onAutoTransition()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,190 @@
|
|||
/*
|
||||
* Copyright (c) 2024 Auxio Project
|
||||
* PlayerKernel.kt is part of Auxio.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.playback.player
|
||||
|
||||
import android.content.Context
|
||||
import androidx.media3.common.AudioAttributes
|
||||
import androidx.media3.common.C
|
||||
import androidx.media3.common.PlaybackException
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.decoder.ffmpeg.FfmpegAudioRenderer
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.media3.exoplayer.RenderersFactory
|
||||
import androidx.media3.exoplayer.audio.AudioCapabilities
|
||||
import androidx.media3.exoplayer.audio.MediaCodecAudioRenderer
|
||||
import androidx.media3.exoplayer.mediacodec.MediaCodecSelector
|
||||
import androidx.media3.exoplayer.source.MediaSource
|
||||
import javax.inject.Inject
|
||||
import org.oxycblt.auxio.playback.replaygain.ReplayGainAudioProcessor
|
||||
|
||||
interface PlayerKernel {
|
||||
val isPlaying: Boolean
|
||||
var playWhenReady: Boolean
|
||||
val currentPosition: Long
|
||||
val audioSessionId: Int
|
||||
val queuer: Queuer
|
||||
|
||||
fun attach()
|
||||
|
||||
fun release()
|
||||
|
||||
fun play()
|
||||
|
||||
fun pause()
|
||||
|
||||
fun seekTo(positionMs: Long)
|
||||
|
||||
fun replaceQueuer(queuerFactory: Queuer.Factory)
|
||||
|
||||
interface Listener {
|
||||
fun onPlayWhenReadyChanged()
|
||||
|
||||
fun onIsPlayingChanged()
|
||||
|
||||
fun onPositionDiscontinuity()
|
||||
|
||||
fun onError(error: PlaybackException)
|
||||
}
|
||||
|
||||
interface Factory {
|
||||
fun create(
|
||||
context: Context,
|
||||
playerListener: Listener,
|
||||
queuerFactory: Queuer.Factory,
|
||||
queuerListener: Queuer.Listener
|
||||
): PlayerKernel
|
||||
}
|
||||
}
|
||||
|
||||
class PlayerKernelFactoryImpl
|
||||
@Inject
|
||||
constructor(
|
||||
private val mediaSourceFactory: MediaSource.Factory,
|
||||
private val replayGainProcessor: ReplayGainAudioProcessor
|
||||
) : PlayerKernel.Factory {
|
||||
override fun create(
|
||||
context: Context,
|
||||
playerListener: PlayerKernel.Listener,
|
||||
queuerFactory: Queuer.Factory,
|
||||
queuerListener: Queuer.Listener
|
||||
): PlayerKernel {
|
||||
// Since Auxio is a music player, only specify an audio renderer to save
|
||||
// battery/apk size/cache size
|
||||
val audioRenderer = RenderersFactory { handler, _, audioListener, _, _ ->
|
||||
arrayOf(
|
||||
FfmpegAudioRenderer(handler, audioListener, replayGainProcessor),
|
||||
MediaCodecAudioRenderer(
|
||||
context,
|
||||
MediaCodecSelector.DEFAULT,
|
||||
handler,
|
||||
audioListener,
|
||||
AudioCapabilities.DEFAULT_AUDIO_CAPABILITIES,
|
||||
replayGainProcessor))
|
||||
}
|
||||
|
||||
val exoPlayer =
|
||||
ExoPlayer.Builder(context, audioRenderer)
|
||||
.setMediaSourceFactory(mediaSourceFactory)
|
||||
// Enable automatic WakeLock support
|
||||
.setWakeMode(C.WAKE_MODE_LOCAL)
|
||||
.setAudioAttributes(
|
||||
// Signal that we are a music player.
|
||||
AudioAttributes.Builder()
|
||||
.setUsage(C.USAGE_MEDIA)
|
||||
.setContentType(C.AUDIO_CONTENT_TYPE_MUSIC)
|
||||
.build(),
|
||||
true)
|
||||
.build()
|
||||
|
||||
return PlayerKernelImpl(
|
||||
exoPlayer, replayGainProcessor, playerListener, queuerListener, queuerFactory)
|
||||
}
|
||||
}
|
||||
|
||||
private class PlayerKernelImpl(
|
||||
private val exoPlayer: ExoPlayer,
|
||||
private val replayGainProcessor: ReplayGainAudioProcessor,
|
||||
private val playerListener: PlayerKernel.Listener,
|
||||
private val queuerListener: Queuer.Listener,
|
||||
queuerFactory: Queuer.Factory
|
||||
) : PlayerKernel, Player.Listener {
|
||||
override var queuer: Queuer = queuerFactory.create(exoPlayer, queuerListener)
|
||||
override val isPlaying: Boolean
|
||||
get() = exoPlayer.isPlaying
|
||||
|
||||
override var playWhenReady: Boolean
|
||||
get() = exoPlayer.playWhenReady
|
||||
set(value) {
|
||||
exoPlayer.playWhenReady = value
|
||||
}
|
||||
|
||||
override val currentPosition: Long
|
||||
get() = exoPlayer.currentPosition
|
||||
|
||||
override val audioSessionId: Int
|
||||
get() = exoPlayer.audioSessionId
|
||||
|
||||
override fun attach() {
|
||||
exoPlayer.addListener(this)
|
||||
replayGainProcessor.attach()
|
||||
queuer.attach()
|
||||
}
|
||||
|
||||
override fun release() {
|
||||
queuer.release()
|
||||
replayGainProcessor.release()
|
||||
exoPlayer.release()
|
||||
}
|
||||
|
||||
override fun play() = exoPlayer.play()
|
||||
|
||||
override fun pause() = exoPlayer.pause()
|
||||
|
||||
override fun seekTo(positionMs: Long) = exoPlayer.seekTo(positionMs)
|
||||
|
||||
override fun replaceQueuer(queuerFactory: Queuer.Factory) {
|
||||
queuer.release()
|
||||
queuer = queuerFactory.create(exoPlayer, queuerListener)
|
||||
queuer.attach()
|
||||
}
|
||||
|
||||
override fun onPlayWhenReadyChanged(playWhenReady: Boolean, reason: Int) {
|
||||
super.onPlayWhenReadyChanged(playWhenReady, reason)
|
||||
playerListener.onPlayWhenReadyChanged()
|
||||
}
|
||||
|
||||
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
||||
super.onIsPlayingChanged(isPlaying)
|
||||
playerListener.onIsPlayingChanged()
|
||||
}
|
||||
|
||||
override fun onPositionDiscontinuity(
|
||||
oldPosition: Player.PositionInfo,
|
||||
newPosition: Player.PositionInfo,
|
||||
reason: Int
|
||||
) {
|
||||
super.onPositionDiscontinuity(oldPosition, newPosition, reason)
|
||||
playerListener.onPositionDiscontinuity()
|
||||
}
|
||||
|
||||
override fun onPlayerError(error: PlaybackException) {
|
||||
super.onPlayerError(error)
|
||||
playerListener.onError(error)
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* Copyright (c) 2023 Auxio Project
|
||||
* SystemModule.kt is part of Auxio.
|
||||
* PlayerModule.kt is part of Auxio.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
@ -16,7 +16,7 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.playback.service
|
||||
package org.oxycblt.auxio.playback.player
|
||||
|
||||
import android.content.Context
|
||||
import androidx.media3.datasource.ContentDataSource
|
||||
|
@ -32,6 +32,7 @@ import androidx.media3.extractor.mp4.Mp4Extractor
|
|||
import androidx.media3.extractor.ogg.OggExtractor
|
||||
import androidx.media3.extractor.ts.AdtsExtractor
|
||||
import androidx.media3.extractor.wav.WavExtractor
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
|
@ -40,7 +41,13 @@ import dagger.hilt.components.SingletonComponent
|
|||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
class SystemModule {
|
||||
interface PlayerModule {
|
||||
@Binds fun playerKernelFactory(factory: PlayerKernelFactoryImpl): PlayerKernel.Factory
|
||||
}
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
class ExoPlayerModule {
|
||||
@Provides
|
||||
fun mediaSourceFactory(
|
||||
dataSourceFactory: DataSource.Factory,
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* Copyright (c) 2024 Auxio Project
|
||||
* ExoPlaybackStateHolder.kt is part of Auxio.
|
||||
* PlayerStateHolder.kt is part of Auxio.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
@ -16,24 +16,14 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.playback.service
|
||||
package org.oxycblt.auxio.playback.player
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.media.audiofx.AudioEffect
|
||||
import androidx.media3.common.AudioAttributes
|
||||
import androidx.media3.common.C
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.PlaybackException
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.decoder.ffmpeg.FfmpegAudioRenderer
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.media3.exoplayer.RenderersFactory
|
||||
import androidx.media3.exoplayer.audio.AudioCapabilities
|
||||
import androidx.media3.exoplayer.audio.MediaCodecAudioRenderer
|
||||
import androidx.media3.exoplayer.mediacodec.MediaCodecSelector
|
||||
import androidx.media3.exoplayer.source.MediaSource
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
@ -61,9 +51,10 @@ import org.oxycblt.auxio.playback.state.StateAck
|
|||
import org.oxycblt.auxio.util.logD
|
||||
import org.oxycblt.auxio.util.logE
|
||||
|
||||
class ExoPlaybackStateHolder(
|
||||
class PlayerStateHolder(
|
||||
private val context: Context,
|
||||
private val player: ExoPlayer,
|
||||
playerKernelFactory: PlayerKernel.Factory,
|
||||
gaplessQueuerFactory: GaplessQueuer.Factory,
|
||||
private val playbackManager: PlaybackStateManager,
|
||||
private val persistenceRepository: PersistenceRepository,
|
||||
private val playbackSettings: PlaybackSettings,
|
||||
|
@ -73,30 +64,58 @@ class ExoPlaybackStateHolder(
|
|||
private val imageSettings: ImageSettings
|
||||
) :
|
||||
PlaybackStateHolder,
|
||||
Player.Listener,
|
||||
PlayerKernel.Listener,
|
||||
Queuer.Listener,
|
||||
MusicRepository.UpdateListener,
|
||||
PlaybackSettings.Listener,
|
||||
ImageSettings.Listener {
|
||||
class Factory
|
||||
@Inject
|
||||
constructor(
|
||||
private val playbackManager: PlaybackStateManager,
|
||||
private val persistenceRepository: PersistenceRepository,
|
||||
private val playbackSettings: PlaybackSettings,
|
||||
private val playerFactory: PlayerKernel.Factory,
|
||||
private val gaplessQueuerFactory: GaplessQueuer.Factory,
|
||||
private val commandFactory: PlaybackCommand.Factory,
|
||||
private val replayGainProcessor: ReplayGainAudioProcessor,
|
||||
private val musicRepository: MusicRepository,
|
||||
private val imageSettings: ImageSettings,
|
||||
) {
|
||||
fun create(context: Context): PlayerStateHolder {
|
||||
return PlayerStateHolder(
|
||||
context,
|
||||
playerFactory,
|
||||
gaplessQueuerFactory,
|
||||
playbackManager,
|
||||
persistenceRepository,
|
||||
playbackSettings,
|
||||
commandFactory,
|
||||
replayGainProcessor,
|
||||
musicRepository,
|
||||
imageSettings)
|
||||
}
|
||||
}
|
||||
|
||||
private val saveJob = Job()
|
||||
private val saveScope = CoroutineScope(Dispatchers.IO + saveJob)
|
||||
private val restoreScope = CoroutineScope(Dispatchers.IO + saveJob)
|
||||
private var currentSaveJob: Job? = null
|
||||
private var openAudioEffectSession = false
|
||||
private val player = playerKernelFactory.create(context, this, gaplessQueuerFactory, this)
|
||||
|
||||
var sessionOngoing = false
|
||||
private set
|
||||
|
||||
init {
|
||||
fun attach() {
|
||||
player.attach()
|
||||
imageSettings.registerListener(this)
|
||||
player.addListener(this)
|
||||
playbackManager.registerStateHolder(this)
|
||||
playbackSettings.registerListener(this)
|
||||
musicRepository.addUpdateListener(this)
|
||||
}
|
||||
|
||||
fun release() {
|
||||
saveJob.cancel()
|
||||
player.removeListener(this)
|
||||
player.release()
|
||||
playbackManager.unregisterStateHolder(this)
|
||||
musicRepository.removeUpdateListener(this)
|
||||
replayGainProcessor.release()
|
||||
|
@ -109,7 +128,7 @@ class ExoPlaybackStateHolder(
|
|||
|
||||
override val progression: Progression
|
||||
get() {
|
||||
val mediaItem = player.currentMediaItem ?: return Progression.nil()
|
||||
val mediaItem = player.queuer.currentMediaItem ?: return Progression.nil()
|
||||
val duration = mediaItem.mediaMetadata.extras?.getLong("durationMs") ?: Long.MAX_VALUE
|
||||
val clampedPosition = player.currentPosition.coerceAtLeast(0).coerceAtMost(duration)
|
||||
return Progression.from(player.playWhenReady, player.isPlaying, clampedPosition)
|
||||
|
@ -117,7 +136,7 @@ class ExoPlaybackStateHolder(
|
|||
|
||||
override val repeatMode
|
||||
get() =
|
||||
when (val repeatMode = player.repeatMode) {
|
||||
when (val repeatMode = player.queuer.repeatMode) {
|
||||
Player.REPEAT_MODE_OFF -> RepeatMode.NONE
|
||||
Player.REPEAT_MODE_ONE -> RepeatMode.TRACK
|
||||
Player.REPEAT_MODE_ALL -> RepeatMode.ALL
|
||||
|
@ -128,18 +147,11 @@ class ExoPlaybackStateHolder(
|
|||
get() = player.audioSessionId
|
||||
|
||||
override fun resolveQueue(): RawQueue {
|
||||
val deviceLibrary =
|
||||
musicRepository.deviceLibrary
|
||||
// No library, cannot do anything.
|
||||
?: return RawQueue(emptyList(), emptyList(), 0)
|
||||
val heap = (0 until player.mediaItemCount).map { player.getMediaItemAt(it) }
|
||||
val heap = player.queuer.computeHeap()
|
||||
val shuffledMapping =
|
||||
if (player.shuffleModeEnabled) {
|
||||
player.unscrambleQueueIndices()
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
return RawQueue(heap.mapNotNull { it.song }, shuffledMapping, player.currentMediaItemIndex)
|
||||
if (player.queuer.shuffleModeEnabled) player.queuer.computeMapping() else emptyList()
|
||||
return RawQueue(
|
||||
heap.mapNotNull { it.song }, shuffledMapping, player.queuer.currentMediaItemIndex)
|
||||
}
|
||||
|
||||
override fun handleDeferred(action: DeferredPlayback): Boolean {
|
||||
|
@ -202,43 +214,31 @@ class ExoPlaybackStateHolder(
|
|||
}
|
||||
|
||||
override fun repeatMode(repeatMode: RepeatMode) {
|
||||
player.repeatMode =
|
||||
player.queuer.repeatMode =
|
||||
when (repeatMode) {
|
||||
RepeatMode.NONE -> Player.REPEAT_MODE_OFF
|
||||
RepeatMode.ALL -> Player.REPEAT_MODE_ALL
|
||||
RepeatMode.TRACK -> Player.REPEAT_MODE_ONE
|
||||
}
|
||||
updatePauseOnRepeat()
|
||||
playbackManager.ack(this, StateAck.RepeatModeChanged)
|
||||
deferSave()
|
||||
}
|
||||
|
||||
override fun newPlayback(command: PlaybackCommand) {
|
||||
parent = command.parent
|
||||
player.shuffleModeEnabled = command.shuffled
|
||||
player.setMediaItems(command.queue.map { it.buildMediaItem() })
|
||||
val mediaItems = command.queue.map { it.buildMediaItem() }
|
||||
val startIndex =
|
||||
command.song
|
||||
?.let { command.queue.indexOf(it) }
|
||||
.also { check(it != -1) { "Start song not in queue" } }
|
||||
if (command.shuffled) {
|
||||
player.setShuffleOrder(BetterShuffleOrder(command.queue.size, startIndex ?: -1))
|
||||
}
|
||||
val target = startIndex ?: player.currentTimeline.getFirstWindowIndex(command.shuffled)
|
||||
player.seekTo(target, C.TIME_UNSET)
|
||||
player.prepare()
|
||||
player.queuer.prepareNew(mediaItems, startIndex, command.shuffled)
|
||||
player.play()
|
||||
playbackManager.ack(this, StateAck.NewPlayback)
|
||||
deferSave()
|
||||
}
|
||||
|
||||
override fun shuffled(shuffled: Boolean) {
|
||||
player.setShuffleModeEnabled(shuffled)
|
||||
if (player.shuffleModeEnabled) {
|
||||
// Have to manually refresh the shuffle seed and anchor it to the new current songs
|
||||
player.setShuffleOrder(
|
||||
BetterShuffleOrder(player.mediaItemCount, player.currentMediaItemIndex))
|
||||
}
|
||||
player.queuer.shuffled(shuffled)
|
||||
playbackManager.ack(this, StateAck.QueueReordered)
|
||||
deferSave()
|
||||
}
|
||||
|
@ -247,14 +247,14 @@ class ExoPlaybackStateHolder(
|
|||
// Replicate the old pseudo-circular queue behavior when no repeat option is implemented.
|
||||
// Basically, you can't skip back and wrap around the queue, but you can skip forward and
|
||||
// wrap around the queue, albeit playback will be paused.
|
||||
if (player.repeatMode == Player.REPEAT_MODE_ALL || player.hasNextMediaItem()) {
|
||||
player.seekToNext()
|
||||
if (player.queuer.repeatMode == Player.REPEAT_MODE_ALL ||
|
||||
player.queuer.hasNextMediaItem()) {
|
||||
player.queuer.seekToNext()
|
||||
if (!playbackSettings.rememberPause) {
|
||||
player.play()
|
||||
}
|
||||
} else {
|
||||
player.seekTo(
|
||||
player.currentTimeline.getFirstWindowIndex(player.shuffleModeEnabled), C.TIME_UNSET)
|
||||
player.queuer.goto(player.queuer.computeFirstMediaItemIndex())
|
||||
// TODO: Dislike the UX implications of this, I feel should I bite the bullet
|
||||
// and switch to dynamic skip enable/disable?
|
||||
if (!playbackSettings.rememberPause) {
|
||||
|
@ -267,9 +267,9 @@ class ExoPlaybackStateHolder(
|
|||
|
||||
override fun prev() {
|
||||
if (playbackSettings.rewindWithPrev) {
|
||||
player.seekToPrevious()
|
||||
} else if (player.hasPreviousMediaItem()) {
|
||||
player.seekToPreviousMediaItem()
|
||||
player.queuer.seekToPrevious()
|
||||
} else if (player.queuer.hasPreviousMediaItem()) {
|
||||
player.queuer.seekToPreviousMediaItem()
|
||||
} else {
|
||||
player.seekTo(0)
|
||||
}
|
||||
|
@ -281,13 +281,12 @@ class ExoPlaybackStateHolder(
|
|||
}
|
||||
|
||||
override fun goto(index: Int) {
|
||||
val indices = player.unscrambleQueueIndices()
|
||||
val indices = player.queuer.computeMapping()
|
||||
if (indices.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
val trueIndex = indices[index]
|
||||
player.seekTo(trueIndex, C.TIME_UNSET) // Handles remaining custom logic
|
||||
player.queuer.goto(trueIndex)
|
||||
if (!playbackSettings.rememberPause) {
|
||||
player.play()
|
||||
}
|
||||
|
@ -296,63 +295,40 @@ class ExoPlaybackStateHolder(
|
|||
}
|
||||
|
||||
override fun playNext(songs: List<Song>, ack: StateAck.PlayNext) {
|
||||
val currTimeline = player.currentTimeline
|
||||
val nextIndex =
|
||||
if (currTimeline.isEmpty) {
|
||||
C.INDEX_UNSET
|
||||
} else {
|
||||
currTimeline.getNextWindowIndex(
|
||||
player.currentMediaItemIndex, Player.REPEAT_MODE_OFF, player.shuffleModeEnabled)
|
||||
}
|
||||
|
||||
if (nextIndex == C.INDEX_UNSET) {
|
||||
player.addMediaItems(songs.map { it.buildMediaItem() })
|
||||
} else {
|
||||
player.addMediaItems(nextIndex, songs.map { it.buildMediaItem() })
|
||||
}
|
||||
player.queuer.addBottomMediaItems(songs.map { it.buildMediaItem() })
|
||||
playbackManager.ack(this, ack)
|
||||
deferSave()
|
||||
}
|
||||
|
||||
override fun addToQueue(songs: List<Song>, ack: StateAck.AddToQueue) {
|
||||
player.addMediaItems(songs.map { it.buildMediaItem() })
|
||||
player.queuer.addTopMediaItems(songs.map { it.buildMediaItem() })
|
||||
playbackManager.ack(this, ack)
|
||||
deferSave()
|
||||
}
|
||||
|
||||
override fun move(from: Int, to: Int, ack: StateAck.Move) {
|
||||
val indices = player.unscrambleQueueIndices()
|
||||
val indices = player.queuer.computeMapping()
|
||||
if (indices.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
val trueFrom = indices[from]
|
||||
val trueTo = indices[to]
|
||||
// ExoPlayer does not actually update it's ShuffleOrder when moving items. Retain a
|
||||
// semblance of "normalcy" by doing a weird no-op swap that actually moves the item.
|
||||
when {
|
||||
trueFrom > trueTo -> {
|
||||
player.moveMediaItem(trueFrom, trueTo)
|
||||
player.moveMediaItem(trueTo + 1, trueFrom)
|
||||
}
|
||||
trueTo > trueFrom -> {
|
||||
player.moveMediaItem(trueFrom, trueTo)
|
||||
player.moveMediaItem(trueTo - 1, trueFrom)
|
||||
}
|
||||
}
|
||||
|
||||
player.queuer.moveMediaItem(trueFrom, trueTo)
|
||||
playbackManager.ack(this, ack)
|
||||
deferSave()
|
||||
}
|
||||
|
||||
override fun remove(at: Int, ack: StateAck.Remove) {
|
||||
val indices = player.unscrambleQueueIndices()
|
||||
val indices = player.queuer.computeMapping()
|
||||
if (indices.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
val trueIndex = indices[at]
|
||||
val songWillChange = player.currentMediaItemIndex == trueIndex
|
||||
player.removeMediaItem(trueIndex)
|
||||
val songWillChange = player.queuer.currentMediaItemIndex == trueIndex
|
||||
player.queuer.removeMediaItem(trueIndex)
|
||||
if (songWillChange && !playbackSettings.rememberPause) {
|
||||
player.play()
|
||||
}
|
||||
|
@ -372,15 +348,11 @@ class ExoPlaybackStateHolder(
|
|||
sendEvent = true
|
||||
}
|
||||
if (rawQueue != resolveQueue()) {
|
||||
player.setMediaItems(rawQueue.heap.map { it.buildMediaItem() })
|
||||
if (rawQueue.isShuffled) {
|
||||
player.shuffleModeEnabled = true
|
||||
player.setShuffleOrder(BetterShuffleOrder(rawQueue.shuffledMapping.toIntArray()))
|
||||
} else {
|
||||
player.shuffleModeEnabled = false
|
||||
}
|
||||
player.seekTo(rawQueue.heapIndex, C.TIME_UNSET)
|
||||
player.prepare()
|
||||
player.queuer.prepareSaved(
|
||||
rawQueue.heap.map { it.buildMediaItem() },
|
||||
rawQueue.shuffledMapping,
|
||||
rawQueue.heapIndex,
|
||||
rawQueue.isShuffled)
|
||||
player.pause()
|
||||
sendEvent = true
|
||||
}
|
||||
|
@ -404,16 +376,14 @@ class ExoPlaybackStateHolder(
|
|||
}
|
||||
|
||||
override fun reset(ack: StateAck.NewPlayback) {
|
||||
player.setMediaItems(listOf())
|
||||
player.queuer.discard()
|
||||
playbackManager.ack(this, ack)
|
||||
deferSave()
|
||||
}
|
||||
|
||||
// --- PLAYER OVERRIDES ---
|
||||
|
||||
override fun onPlayWhenReadyChanged(playWhenReady: Boolean, reason: Int) {
|
||||
super.onPlayWhenReadyChanged(playWhenReady, reason)
|
||||
|
||||
override fun onPlayWhenReadyChanged() {
|
||||
if (player.playWhenReady) {
|
||||
// Mark that we have started playing so that the notification can now be posted.
|
||||
logD("Player has started playing")
|
||||
|
@ -431,40 +401,27 @@ class ExoPlaybackStateHolder(
|
|||
broadcastAudioEffectAction(AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION)
|
||||
openAudioEffectSession = false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPlaybackStateChanged(playbackState: Int) {
|
||||
super.onPlaybackStateChanged(playbackState)
|
||||
|
||||
if (playbackState == Player.STATE_ENDED && player.repeatMode == Player.REPEAT_MODE_OFF) {
|
||||
goto(0)
|
||||
player.pause()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
|
||||
super.onMediaItemTransition(mediaItem, reason)
|
||||
|
||||
if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_AUTO) {
|
||||
playbackManager.ack(this, StateAck.IndexMoved)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onEvents(player: Player, events: Player.Events) {
|
||||
super.onEvents(player, events)
|
||||
|
||||
// So many actions trigger progression changes that it becomes easier just to handle it
|
||||
// in an ExoPlayer callback anyway. This doesn't really cause issues anywhere.
|
||||
if (events.containsAny(
|
||||
Player.EVENT_PLAY_WHEN_READY_CHANGED,
|
||||
Player.EVENT_IS_PLAYING_CHANGED,
|
||||
Player.EVENT_POSITION_DISCONTINUITY)) {
|
||||
logD("Player state changed, must synchronize state")
|
||||
playbackManager.ack(this, StateAck.ProgressionChanged)
|
||||
}
|
||||
deferSave()
|
||||
}
|
||||
|
||||
override fun onPlayerError(error: PlaybackException) {
|
||||
override fun onIsPlayingChanged() {
|
||||
playbackManager.ack(this, StateAck.ProgressionChanged)
|
||||
deferSave()
|
||||
}
|
||||
|
||||
override fun onPositionDiscontinuity() {
|
||||
playbackManager.ack(this, StateAck.ProgressionChanged)
|
||||
deferSave()
|
||||
}
|
||||
|
||||
override fun onAutoTransition() {
|
||||
playbackManager.ack(this, StateAck.IndexMoved)
|
||||
deferSave()
|
||||
}
|
||||
|
||||
override fun onError(error: PlaybackException) {
|
||||
// TODO: Replace with no skipping and a notification instead
|
||||
// If there's any issue, just go to the next song.
|
||||
logE("Player error occurred")
|
||||
|
@ -491,17 +448,7 @@ class ExoPlaybackStateHolder(
|
|||
}
|
||||
}
|
||||
|
||||
// --- PLAYBACKSETTINGS OVERRIDES ---
|
||||
|
||||
override fun onPauseOnRepeatChanged() {
|
||||
super.onPauseOnRepeatChanged()
|
||||
updatePauseOnRepeat()
|
||||
}
|
||||
|
||||
private fun updatePauseOnRepeat() {
|
||||
player.pauseAtEndOfMediaItems =
|
||||
player.repeatMode == Player.REPEAT_MODE_ONE && playbackSettings.pauseOnRepeat
|
||||
}
|
||||
// --- OVERRIDES ---
|
||||
|
||||
private fun save(cb: () -> Unit) {
|
||||
saveJob {
|
||||
|
@ -533,101 +480,6 @@ class ExoPlaybackStateHolder(
|
|||
private val MediaItem.song: Song?
|
||||
get() = this.localConfiguration?.tag as? Song?
|
||||
|
||||
private fun Player.unscrambleQueueIndices(): List<Int> {
|
||||
val timeline = currentTimeline
|
||||
if (timeline.isEmpty) {
|
||||
return emptyList()
|
||||
}
|
||||
val queue = mutableListOf<Int>()
|
||||
|
||||
// Add the active queue item.
|
||||
val currentMediaItemIndex = currentMediaItemIndex
|
||||
queue.add(currentMediaItemIndex)
|
||||
|
||||
// Fill queue alternating with next and/or previous queue items.
|
||||
var firstMediaItemIndex = currentMediaItemIndex
|
||||
var lastMediaItemIndex = currentMediaItemIndex
|
||||
val shuffleModeEnabled = shuffleModeEnabled
|
||||
while ((firstMediaItemIndex != C.INDEX_UNSET || lastMediaItemIndex != C.INDEX_UNSET)) {
|
||||
// Begin with next to have a longer tail than head if an even sized queue needs to be
|
||||
// trimmed.
|
||||
if (lastMediaItemIndex != C.INDEX_UNSET) {
|
||||
lastMediaItemIndex =
|
||||
timeline.getNextWindowIndex(
|
||||
lastMediaItemIndex, Player.REPEAT_MODE_OFF, shuffleModeEnabled)
|
||||
if (lastMediaItemIndex != C.INDEX_UNSET) {
|
||||
queue.add(lastMediaItemIndex)
|
||||
}
|
||||
}
|
||||
if (firstMediaItemIndex != C.INDEX_UNSET) {
|
||||
firstMediaItemIndex =
|
||||
timeline.getPreviousWindowIndex(
|
||||
firstMediaItemIndex, Player.REPEAT_MODE_OFF, shuffleModeEnabled)
|
||||
if (firstMediaItemIndex != C.INDEX_UNSET) {
|
||||
queue.add(0, firstMediaItemIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return queue
|
||||
}
|
||||
|
||||
class Factory
|
||||
@Inject
|
||||
constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val playbackManager: PlaybackStateManager,
|
||||
private val persistenceRepository: PersistenceRepository,
|
||||
private val playbackSettings: PlaybackSettings,
|
||||
private val commandFactory: PlaybackCommand.Factory,
|
||||
private val mediaSourceFactory: MediaSource.Factory,
|
||||
private val replayGainProcessorFactory: ReplayGainAudioProcessor.Factory,
|
||||
private val musicRepository: MusicRepository,
|
||||
private val imageSettings: ImageSettings,
|
||||
) {
|
||||
fun create(): ExoPlaybackStateHolder {
|
||||
// Since Auxio is a music player, only specify an audio renderer to save
|
||||
// battery/apk size/cache size
|
||||
val replayGainProcessor = replayGainProcessorFactory.create()
|
||||
val audioRenderer = RenderersFactory { handler, _, audioListener, _, _ ->
|
||||
arrayOf(
|
||||
FfmpegAudioRenderer(handler, audioListener, replayGainProcessor),
|
||||
MediaCodecAudioRenderer(
|
||||
context,
|
||||
MediaCodecSelector.DEFAULT,
|
||||
handler,
|
||||
audioListener,
|
||||
AudioCapabilities.DEFAULT_AUDIO_CAPABILITIES,
|
||||
replayGainProcessor))
|
||||
}
|
||||
|
||||
val exoPlayer =
|
||||
ExoPlayer.Builder(context, audioRenderer)
|
||||
.setMediaSourceFactory(mediaSourceFactory)
|
||||
// Enable automatic WakeLock support
|
||||
.setWakeMode(C.WAKE_MODE_LOCAL)
|
||||
.setAudioAttributes(
|
||||
// Signal that we are a music player.
|
||||
AudioAttributes.Builder()
|
||||
.setUsage(C.USAGE_MEDIA)
|
||||
.setContentType(C.AUDIO_CONTENT_TYPE_MUSIC)
|
||||
.build(),
|
||||
true)
|
||||
.build()
|
||||
|
||||
return ExoPlaybackStateHolder(
|
||||
context,
|
||||
exoPlayer,
|
||||
playbackManager,
|
||||
persistenceRepository,
|
||||
playbackSettings,
|
||||
commandFactory,
|
||||
replayGainProcessor,
|
||||
musicRepository,
|
||||
imageSettings)
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val SAVE_BUFFER = 5000L
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
* Copyright (c) 2024 Auxio Project
|
||||
* Queuer.kt is part of Auxio.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.playback.player
|
||||
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.Player.RepeatMode
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
|
||||
interface Queuer {
|
||||
val currentMediaItem: MediaItem?
|
||||
val currentMediaItemIndex: Int
|
||||
val shuffleModeEnabled: Boolean
|
||||
|
||||
@get:RepeatMode var repeatMode: Int
|
||||
|
||||
fun attach()
|
||||
|
||||
fun release()
|
||||
|
||||
fun goto(mediaItemIndex: Int)
|
||||
|
||||
fun seekToNext()
|
||||
|
||||
fun hasNextMediaItem(): Boolean
|
||||
|
||||
fun seekToPrevious()
|
||||
|
||||
fun seekToPreviousMediaItem()
|
||||
|
||||
fun hasPreviousMediaItem(): Boolean
|
||||
|
||||
fun moveMediaItem(fromIndex: Int, toIndex: Int)
|
||||
|
||||
fun removeMediaItem(index: Int)
|
||||
|
||||
// EXTENSIONS
|
||||
fun computeHeap(): List<MediaItem>
|
||||
|
||||
fun computeMapping(): List<Int>
|
||||
|
||||
fun computeFirstMediaItemIndex(): Int
|
||||
|
||||
fun prepareNew(mediaItems: List<MediaItem>, startIndex: Int?, shuffled: Boolean)
|
||||
|
||||
fun prepareSaved(mediaItems: List<MediaItem>, mapping: List<Int>, index: Int, shuffled: Boolean)
|
||||
|
||||
fun discard()
|
||||
|
||||
fun addTopMediaItems(mediaItems: List<MediaItem>)
|
||||
|
||||
fun addBottomMediaItems(mediaItems: List<MediaItem>)
|
||||
|
||||
fun shuffled(shuffled: Boolean)
|
||||
|
||||
interface Listener {
|
||||
fun onAutoTransition()
|
||||
}
|
||||
|
||||
interface Factory {
|
||||
fun create(exoPlayer: ExoPlayer, listener: Listener): Queuer
|
||||
}
|
||||
}
|
|
@ -45,19 +45,11 @@ import org.oxycblt.auxio.util.logD
|
|||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
class ReplayGainAudioProcessor
|
||||
private constructor(
|
||||
@Inject
|
||||
constructor(
|
||||
private val playbackManager: PlaybackStateManager,
|
||||
private val playbackSettings: PlaybackSettings
|
||||
) : BaseAudioProcessor(), PlaybackStateManager.Listener, PlaybackSettings.Listener {
|
||||
class Factory
|
||||
@Inject
|
||||
constructor(
|
||||
private val playbackManager: PlaybackStateManager,
|
||||
private val playbackSettings: PlaybackSettings
|
||||
) {
|
||||
fun create() = ReplayGainAudioProcessor(playbackManager, playbackSettings)
|
||||
}
|
||||
|
||||
private var volume = 1f
|
||||
set(value) {
|
||||
field = value
|
||||
|
@ -65,7 +57,7 @@ private constructor(
|
|||
flush()
|
||||
}
|
||||
|
||||
init {
|
||||
fun attach() {
|
||||
playbackManager.addListener(this)
|
||||
playbackSettings.registerListener(this)
|
||||
}
|
||||
|
|
|
@ -96,7 +96,7 @@ private constructor(
|
|||
val notification: ForegroundServiceNotification
|
||||
get() = _notification
|
||||
|
||||
init {
|
||||
fun attach() {
|
||||
playbackManager.addListener(this)
|
||||
playbackSettings.registerListener(this)
|
||||
imageSettings.registerListener(this)
|
||||
|
|
|
@ -25,6 +25,7 @@ import kotlinx.coroutines.Job
|
|||
import org.oxycblt.auxio.ForegroundListener
|
||||
import org.oxycblt.auxio.ForegroundServiceNotification
|
||||
import org.oxycblt.auxio.IntegerTable
|
||||
import org.oxycblt.auxio.playback.player.PlayerStateHolder
|
||||
import org.oxycblt.auxio.playback.state.DeferredPlayback
|
||||
import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
||||
import org.oxycblt.auxio.util.logD
|
||||
|
@ -35,7 +36,7 @@ private constructor(
|
|||
private val context: Context,
|
||||
private val foregroundListener: ForegroundListener,
|
||||
private val playbackManager: PlaybackStateManager,
|
||||
exoHolderFactory: ExoPlaybackStateHolder.Factory,
|
||||
playerHolderFactory: PlayerStateHolder.Factory,
|
||||
sessionHolderFactory: MediaSessionHolder.Factory,
|
||||
widgetComponentFactory: WidgetComponent.Factory,
|
||||
systemReceiverFactory: SystemPlaybackReceiver.Factory,
|
||||
|
@ -44,7 +45,7 @@ private constructor(
|
|||
@Inject
|
||||
constructor(
|
||||
private val playbackManager: PlaybackStateManager,
|
||||
private val exoHolderFactory: ExoPlaybackStateHolder.Factory,
|
||||
private val exoHolderFactory: PlayerStateHolder.Factory,
|
||||
private val sessionHolderFactory: MediaSessionHolder.Factory,
|
||||
private val widgetComponentFactory: WidgetComponent.Factory,
|
||||
private val systemReceiverFactory: SystemPlaybackReceiver.Factory,
|
||||
|
@ -61,18 +62,20 @@ private constructor(
|
|||
}
|
||||
|
||||
private val waitJob = Job()
|
||||
private val exoHolder = exoHolderFactory.create()
|
||||
private val exoHolder = playerHolderFactory.create(context)
|
||||
private val sessionHolder = sessionHolderFactory.create(context, foregroundListener)
|
||||
private val widgetComponent = widgetComponentFactory.create(context)
|
||||
private val systemReceiver = systemReceiverFactory.create(context)
|
||||
|
||||
val token: MediaSessionCompat.Token
|
||||
get() = sessionHolder.token
|
||||
private val systemReceiver = systemReceiverFactory.create(context, widgetComponent)
|
||||
|
||||
// --- MEDIASESSION CALLBACKS ---
|
||||
|
||||
init {
|
||||
fun attach(): MediaSessionCompat.Token {
|
||||
exoHolder.attach()
|
||||
sessionHolder.attach()
|
||||
widgetComponent.attach()
|
||||
systemReceiver.attach()
|
||||
playbackManager.addListener(this)
|
||||
return sessionHolder.token
|
||||
}
|
||||
|
||||
fun handleTaskRemoved() {
|
||||
|
|
|
@ -37,6 +37,7 @@ import org.oxycblt.auxio.widgets.WidgetProvider
|
|||
*/
|
||||
class SystemPlaybackReceiver
|
||||
private constructor(
|
||||
private val context: Context,
|
||||
private val playbackManager: PlaybackStateManager,
|
||||
private val playbackSettings: PlaybackSettings,
|
||||
private val widgetComponent: WidgetComponent
|
||||
|
@ -47,16 +48,19 @@ private constructor(
|
|||
@Inject
|
||||
constructor(
|
||||
private val playbackManager: PlaybackStateManager,
|
||||
private val playbackSettings: PlaybackSettings,
|
||||
private val widgetComponent: WidgetComponent
|
||||
private val playbackSettings: PlaybackSettings
|
||||
) {
|
||||
fun create(context: Context): SystemPlaybackReceiver {
|
||||
val receiver =
|
||||
SystemPlaybackReceiver(playbackManager, playbackSettings, widgetComponent)
|
||||
ContextCompat.registerReceiver(
|
||||
context, receiver, INTENT_FILTER, ContextCompat.RECEIVER_EXPORTED)
|
||||
return receiver
|
||||
fun create(context: Context, widgetComponent: WidgetComponent) =
|
||||
SystemPlaybackReceiver(context, playbackManager, playbackSettings, widgetComponent)
|
||||
}
|
||||
|
||||
fun attach() {
|
||||
ContextCompat.registerReceiver(
|
||||
context, this, INTENT_FILTER, ContextCompat.RECEIVER_EXPORTED)
|
||||
}
|
||||
|
||||
fun release() {
|
||||
context.unregisterReceiver(this)
|
||||
}
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
|
|
|
@ -67,7 +67,7 @@ private constructor(
|
|||
|
||||
private val widgetProvider = WidgetProvider()
|
||||
|
||||
init {
|
||||
fun attach() {
|
||||
playbackManager.addListener(this)
|
||||
uiSettings.registerListener(this)
|
||||
imageSettings.registerListener(this)
|
||||
|
|
BIN
app/src/main/res/drawable-hdpi/ic_more_bitmap_24.png
Normal file
BIN
app/src/main/res/drawable-hdpi/ic_more_bitmap_24.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 123 B |
BIN
app/src/main/res/drawable-mdpi/ic_more_bitmap_24.png
Normal file
BIN
app/src/main/res/drawable-mdpi/ic_more_bitmap_24.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 97 B |
BIN
app/src/main/res/drawable-xhdpi/ic_more_bitmap_24.png
Normal file
BIN
app/src/main/res/drawable-xhdpi/ic_more_bitmap_24.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 141 B |
BIN
app/src/main/res/drawable-xxhdpi/ic_more_bitmap_24.png
Normal file
BIN
app/src/main/res/drawable-xxhdpi/ic_more_bitmap_24.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 179 B |
BIN
app/src/main/res/drawable-xxxhdpi/ic_more_bitmap_24.png
Normal file
BIN
app/src/main/res/drawable-xxxhdpi/ic_more_bitmap_24.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 217 B |
Loading…
Reference in a new issue