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() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
playbackFragment = playbackFragmentFactory.create(this, this)
|
playbackFragment = playbackFragmentFactory.create(this, this)
|
||||||
sessionToken = playbackFragment.token
|
sessionToken = playbackFragment.attach()
|
||||||
musicFragment = musicFragmentFactory.create(this, this, this)
|
musicFragment = musicFragmentFactory.create(this, this, this)
|
||||||
|
musicFragment.attach()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
|
@ -80,7 +81,6 @@ class AuxioService :
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
musicFragment.release()
|
musicFragment.release()
|
||||||
playbackFragment.release()
|
playbackFragment.release()
|
||||||
sessionToken = null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onGetRoot(
|
override fun onGetRoot(
|
||||||
|
@ -88,9 +88,7 @@ class AuxioService :
|
||||||
clientUid: Int,
|
clientUid: Int,
|
||||||
rootHints: Bundle?
|
rootHints: Bundle?
|
||||||
): BrowserRoot {
|
): BrowserRoot {
|
||||||
val maximumRootChildLimit =
|
return musicFragment.getRoot()
|
||||||
rootHints?.getInt(MediaConstants.BROWSER_ROOT_HINTS_KEY_ROOT_CHILDREN_LIMIT, 4) ?: 4
|
|
||||||
return musicFragment.getRoot(maximumRootChildLimit)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onLoadItem(itemId: String, result: Result<MediaItem>) {
|
override fun onLoadItem(itemId: String, result: Result<MediaItem>) {
|
||||||
|
@ -98,7 +96,10 @@ class AuxioService :
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onLoadChildren(parentId: String, result: Result<MutableList<MediaItem>>) {
|
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>>) {
|
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
|
package org.oxycblt.auxio.detail
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
|
import javax.inject.Inject
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.detail.list.DiscHeader
|
|
||||||
import org.oxycblt.auxio.list.ListSettings
|
import org.oxycblt.auxio.list.ListSettings
|
||||||
import org.oxycblt.auxio.list.sort.Sort
|
import org.oxycblt.auxio.list.sort.Sort
|
||||||
import org.oxycblt.auxio.music.Album
|
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.Disc
|
||||||
import org.oxycblt.auxio.music.info.ReleaseType
|
import org.oxycblt.auxio.music.info.ReleaseType
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
import java.util.SortedMap
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
interface DetailGenerator {
|
interface DetailGenerator {
|
||||||
fun any(uid: Music.UID): Detail<out MusicParent>?
|
fun any(uid: Music.UID): Detail<out MusicParent>?
|
||||||
|
|
||||||
fun album(uid: Music.UID): Detail<Album>?
|
fun album(uid: Music.UID): Detail<Album>?
|
||||||
|
|
||||||
fun artist(uid: Music.UID): Detail<Artist>?
|
fun artist(uid: Music.UID): Detail<Artist>?
|
||||||
|
|
||||||
fun genre(uid: Music.UID): Detail<Genre>?
|
fun genre(uid: Music.UID): Detail<Genre>?
|
||||||
|
|
||||||
fun playlist(uid: Music.UID): Detail<Playlist>?
|
fun playlist(uid: Music.UID): Detail<Playlist>?
|
||||||
|
|
||||||
|
fun attach()
|
||||||
|
|
||||||
fun release()
|
fun release()
|
||||||
|
|
||||||
interface Factory {
|
interface Factory {
|
||||||
|
@ -38,10 +60,10 @@ interface DetailGenerator {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class DetailGeneratorFactoryImpl @Inject constructor(
|
class DetailGeneratorFactoryImpl
|
||||||
private val listSettings: ListSettings,
|
@Inject
|
||||||
private val musicRepository: MusicRepository
|
constructor(private val listSettings: ListSettings, private val musicRepository: MusicRepository) :
|
||||||
) : DetailGenerator.Factory {
|
DetailGenerator.Factory {
|
||||||
override fun create(invalidator: DetailGenerator.Invalidator): DetailGenerator =
|
override fun create(invalidator: DetailGenerator.Invalidator): DetailGenerator =
|
||||||
DetailGeneratorImpl(invalidator, listSettings, musicRepository)
|
DetailGeneratorImpl(invalidator, listSettings, musicRepository)
|
||||||
}
|
}
|
||||||
|
@ -51,7 +73,7 @@ private class DetailGeneratorImpl(
|
||||||
private val listSettings: ListSettings,
|
private val listSettings: ListSettings,
|
||||||
private val musicRepository: MusicRepository
|
private val musicRepository: MusicRepository
|
||||||
) : DetailGenerator, MusicRepository.UpdateListener, ListSettings.Listener {
|
) : DetailGenerator, MusicRepository.UpdateListener, ListSettings.Listener {
|
||||||
init {
|
override fun attach() {
|
||||||
listSettings.registerListener(this)
|
listSettings.registerListener(this)
|
||||||
musicRepository.addUpdateListener(this)
|
musicRepository.addUpdateListener(this)
|
||||||
}
|
}
|
||||||
|
@ -102,11 +124,12 @@ private class DetailGeneratorImpl(
|
||||||
val album = musicRepository.deviceLibrary?.findAlbum(uid) ?: return null
|
val album = musicRepository.deviceLibrary?.findAlbum(uid) ?: return null
|
||||||
val songs = listSettings.albumSongSort.songs(album.songs)
|
val songs = listSettings.albumSongSort.songs(album.songs)
|
||||||
val discs = songs.groupBy { it.disc }
|
val discs = songs.groupBy { it.disc }
|
||||||
val section = if (discs.size > 1 || discs.keys.first() != null) {
|
val section =
|
||||||
DetailSection.Discs(discs)
|
if (discs.size > 1) {
|
||||||
} else {
|
DetailSection.Discs(discs)
|
||||||
DetailSection.Songs(songs)
|
} else {
|
||||||
}
|
DetailSection.Songs(songs)
|
||||||
|
}
|
||||||
return Detail(album, listOf(section))
|
return Detail(album, listOf(section))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -138,13 +161,14 @@ private class DetailGeneratorImpl(
|
||||||
// implicit album list into the mapping.
|
// implicit album list into the mapping.
|
||||||
logD("Implicit albums present, adding to list")
|
logD("Implicit albums present, adding to list")
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
(grouping as MutableMap<DetailSection.Albums.Category, Collection<Album>>)[DetailSection.Albums.Category.APPEARANCES] =
|
(grouping as MutableMap<DetailSection.Albums.Category, Collection<Album>>)[
|
||||||
artist.implicitAlbums
|
DetailSection.Albums.Category.APPEARANCES] = artist.implicitAlbums
|
||||||
}
|
}
|
||||||
|
|
||||||
val sections = grouping.mapTo(mutableListOf<DetailSection>()) { (category, albums) ->
|
val sections =
|
||||||
DetailSection.Albums(category, ARTIST_ALBUM_SORT.albums(albums))
|
grouping.mapTo(mutableListOf<DetailSection>()) { (category, albums) ->
|
||||||
}
|
DetailSection.Albums(category, ARTIST_ALBUM_SORT.albums(albums))
|
||||||
|
}
|
||||||
val songs = DetailSection.Songs(listSettings.artistSongSort.songs(artist.songs))
|
val songs = DetailSection.Songs(listSettings.artistSongSort.songs(artist.songs))
|
||||||
sections.add(songs)
|
sections.add(songs)
|
||||||
return Detail(artist, sections)
|
return Detail(artist, sections)
|
||||||
|
@ -184,7 +208,8 @@ sealed interface DetailSection {
|
||||||
override val stringRes = R.string.lbl_songs
|
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 order = 1 + category.ordinal
|
||||||
override val stringRes = category.stringRes
|
override val stringRes = category.stringRes
|
||||||
|
|
||||||
|
@ -203,7 +228,6 @@ sealed interface DetailSection {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
data class Songs(override val items: List<Song>) : PlainSection<Song>() {
|
data class Songs(override val items: List<Song>) : PlainSection<Song>() {
|
||||||
override val order = 12
|
override val order = 12
|
||||||
override val stringRes = R.string.lbl_songs
|
override val stringRes = R.string.lbl_songs
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2024 Auxio Project
|
* 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
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
|
@ -72,9 +72,6 @@ constructor(
|
||||||
private val playbackSettings: PlaybackSettings,
|
private val playbackSettings: PlaybackSettings,
|
||||||
detailGeneratorFactory: DetailGenerator.Factory
|
detailGeneratorFactory: DetailGenerator.Factory
|
||||||
) : ViewModel(), DetailGenerator.Invalidator {
|
) : ViewModel(), DetailGenerator.Invalidator {
|
||||||
private val detailGenerator = detailGeneratorFactory.create(this)
|
|
||||||
|
|
||||||
|
|
||||||
private val _toShow = MutableEvent<Show>()
|
private val _toShow = MutableEvent<Show>()
|
||||||
/**
|
/**
|
||||||
* A [Show] command that is awaiting a view capable of responding to it. Null if none currently.
|
* A [Show] command that is awaiting a view capable of responding to it. Null if none currently.
|
||||||
|
@ -198,6 +195,12 @@ constructor(
|
||||||
playbackSettings.inParentPlaybackMode
|
playbackSettings.inParentPlaybackMode
|
||||||
?: PlaySong.FromPlaylist(unlikelyToBeNull(currentPlaylist.value))
|
?: PlaySong.FromPlaylist(unlikelyToBeNull(currentPlaylist.value))
|
||||||
|
|
||||||
|
private val detailGenerator = detailGeneratorFactory.create(this)
|
||||||
|
|
||||||
|
init {
|
||||||
|
detailGenerator.attach()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
detailGenerator.release()
|
detailGenerator.release()
|
||||||
}
|
}
|
||||||
|
@ -208,21 +211,18 @@ constructor(
|
||||||
val album = detailGenerator.album(currentAlbum.value?.uid ?: return)
|
val album = detailGenerator.album(currentAlbum.value?.uid ?: return)
|
||||||
refreshDetail(album, _currentAlbum, _albumSongList, _albumSongInstructions, replace)
|
refreshDetail(album, _currentAlbum, _albumSongList, _albumSongInstructions, replace)
|
||||||
}
|
}
|
||||||
|
|
||||||
MusicType.ARTISTS -> {
|
MusicType.ARTISTS -> {
|
||||||
val artist = detailGenerator.artist(currentArtist.value?.uid ?: return)
|
val artist = detailGenerator.artist(currentArtist.value?.uid ?: return)
|
||||||
refreshDetail(artist, _currentArtist, _artistSongList, _artistSongInstructions, replace)
|
refreshDetail(
|
||||||
|
artist, _currentArtist, _artistSongList, _artistSongInstructions, replace)
|
||||||
}
|
}
|
||||||
|
|
||||||
MusicType.GENRES -> {
|
MusicType.GENRES -> {
|
||||||
val genre = detailGenerator.genre(currentGenre.value?.uid ?: return)
|
val genre = detailGenerator.genre(currentGenre.value?.uid ?: return)
|
||||||
refreshDetail(genre, _currentGenre, _genreSongList, _genreSongInstructions, replace)
|
refreshDetail(genre, _currentGenre, _genreSongList, _genreSongInstructions, replace)
|
||||||
}
|
}
|
||||||
|
|
||||||
MusicType.PLAYLISTS -> {
|
MusicType.PLAYLISTS -> {
|
||||||
refreshPlaylist(currentPlaylist.value?.uid ?: return)
|
refreshPlaylist(currentPlaylist.value?.uid ?: return)
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> error("Unexpected music type $type")
|
else -> error("Unexpected music type $type")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -522,7 +522,6 @@ constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun <T : MusicParent> refreshDetail(
|
private fun <T : MusicParent> refreshDetail(
|
||||||
detail: Detail<T>?,
|
detail: Detail<T>?,
|
||||||
parent: MutableStateFlow<T?>,
|
parent: MutableStateFlow<T?>,
|
||||||
|
@ -537,24 +536,23 @@ constructor(
|
||||||
val newList = mutableListOf<Item>()
|
val newList = mutableListOf<Item>()
|
||||||
var newInstructions: UpdateInstructions = UpdateInstructions.Diff
|
var newInstructions: UpdateInstructions = UpdateInstructions.Diff
|
||||||
for ((i, section) in detail.sections.withIndex()) {
|
for ((i, section) in detail.sections.withIndex()) {
|
||||||
val items = when (section) {
|
val items =
|
||||||
is DetailSection.PlainSection<*> -> {
|
when (section) {
|
||||||
val header = if (section is DetailSection.Songs)
|
is DetailSection.PlainSection<*> -> {
|
||||||
SortHeader(section.stringRes) else BasicHeader(section.stringRes)
|
val header =
|
||||||
newList.add(Divider(header))
|
if (section is DetailSection.Songs) SortHeader(section.stringRes)
|
||||||
newList.add(header)
|
else BasicHeader(section.stringRes)
|
||||||
section.items
|
newList.add(Divider(header))
|
||||||
}
|
newList.add(header)
|
||||||
|
section.items
|
||||||
is DetailSection.Discs -> {
|
}
|
||||||
val header = BasicHeader(section.stringRes)
|
is DetailSection.Discs -> {
|
||||||
newList.add(Divider(header))
|
val header = SortHeader(section.stringRes)
|
||||||
newList.add(header)
|
newList.add(Divider(header))
|
||||||
section.discs.flatMap {
|
newList.add(header)
|
||||||
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
|
// Currently only the final section (songs, which can be sorted) are invalidatable
|
||||||
// and thus need to be replaced.
|
// and thus need to be replaced.
|
||||||
if (replace == -1 && i == detail.sections.lastIndex) {
|
if (replace == -1 && i == detail.sections.lastIndex) {
|
||||||
|
@ -564,16 +562,20 @@ constructor(
|
||||||
newList.addAll(items)
|
newList.addAll(items)
|
||||||
}
|
}
|
||||||
parent.value = detail.parent
|
parent.value = detail.parent
|
||||||
list.value = newList
|
|
||||||
instructions.put(newInstructions)
|
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")
|
logD("Refreshing playlist list")
|
||||||
val edited = editedPlaylist.value
|
val edited = editedPlaylist.value
|
||||||
if (edited == null) {
|
if (edited == null) {
|
||||||
val playlist = detailGenerator.playlist(uid)
|
val playlist = detailGenerator.playlist(uid)
|
||||||
refreshDetail(playlist, _currentPlaylist, _playlistSongList, _playlistSongInstructions, null)
|
refreshDetail(
|
||||||
|
playlist, _currentPlaylist, _playlistSongList, _playlistSongInstructions, null)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val list = mutableListOf<Item>()
|
val list = mutableListOf<Item>()
|
||||||
|
@ -583,8 +585,8 @@ constructor(
|
||||||
list.add(header)
|
list.add(header)
|
||||||
list.addAll(edited)
|
list.addAll(edited)
|
||||||
}
|
}
|
||||||
_playlistSongList.value = list
|
|
||||||
_playlistSongInstructions.put(instructions)
|
_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.Album
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.music.info.Disc
|
import org.oxycblt.auxio.music.info.Disc
|
||||||
|
import org.oxycblt.auxio.music.info.resolveNumber
|
||||||
import org.oxycblt.auxio.playback.formatDurationMs
|
import org.oxycblt.auxio.playback.formatDurationMs
|
||||||
import org.oxycblt.auxio.util.context
|
import org.oxycblt.auxio.util.context
|
||||||
import org.oxycblt.auxio.util.inflater
|
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.
|
* 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) {
|
fun bind(discHeader: DiscHeader) {
|
||||||
val disc = discHeader.inner
|
val disc = discHeader.inner
|
||||||
if (disc != null) {
|
binding.discNumber.text = disc.resolveNumber(binding.context)
|
||||||
binding.discNumber.text = binding.context.getString(R.string.fmt_disc_no, disc.number)
|
binding.discName.apply {
|
||||||
binding.discName.apply {
|
text = disc?.name
|
||||||
text = disc.name
|
isGone = disc?.name == null
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -32,6 +32,10 @@ import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
|
|
||||||
interface HomeGenerator {
|
interface HomeGenerator {
|
||||||
|
fun attach()
|
||||||
|
|
||||||
|
fun release()
|
||||||
|
|
||||||
fun songs(): List<Song>
|
fun songs(): List<Song>
|
||||||
|
|
||||||
fun albums(): List<Album>
|
fun albums(): List<Album>
|
||||||
|
@ -44,8 +48,6 @@ interface HomeGenerator {
|
||||||
|
|
||||||
fun tabs(): List<MusicType>
|
fun tabs(): List<MusicType>
|
||||||
|
|
||||||
fun release()
|
|
||||||
|
|
||||||
interface Invalidator {
|
interface Invalidator {
|
||||||
fun invalidateMusic(type: MusicType, instructions: UpdateInstructions)
|
fun invalidateMusic(type: MusicType, instructions: UpdateInstructions)
|
||||||
|
|
||||||
|
@ -74,41 +76,14 @@ private class HomeGeneratorImpl(
|
||||||
private val listSettings: ListSettings,
|
private val listSettings: ListSettings,
|
||||||
private val musicRepository: MusicRepository,
|
private val musicRepository: MusicRepository,
|
||||||
) : HomeGenerator, HomeSettings.Listener, ListSettings.Listener, MusicRepository.UpdateListener {
|
) : HomeGenerator, HomeSettings.Listener, ListSettings.Listener, MusicRepository.UpdateListener {
|
||||||
override fun songs() =
|
override fun attach() {
|
||||||
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)
|
homeSettings.registerListener(this)
|
||||||
listSettings.registerListener(this)
|
listSettings.registerListener(this)
|
||||||
musicRepository.addUpdateListener(this)
|
musicRepository.addUpdateListener(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun release() {
|
override fun onTabsChanged() {
|
||||||
musicRepository.removeUpdateListener(this)
|
invalidator.invalidateTabs()
|
||||||
listSettings.unregisterListener(this)
|
|
||||||
homeSettings.unregisterListener(this)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onHideCollaboratorsChanged() {
|
override fun onHideCollaboratorsChanged() {
|
||||||
|
@ -161,4 +136,31 @@ private class HomeGeneratorImpl(
|
||||||
invalidator.invalidateMusic(MusicType.PLAYLISTS, UpdateInstructions.Diff)
|
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,
|
private val playbackSettings: PlaybackSettings,
|
||||||
homeGeneratorFactory: HomeGenerator.Factory
|
homeGeneratorFactory: HomeGenerator.Factory
|
||||||
) : ViewModel(), HomeGenerator.Invalidator {
|
) : ViewModel(), HomeGenerator.Invalidator {
|
||||||
private val homeGenerator = homeGeneratorFactory.create(this)
|
|
||||||
|
|
||||||
private val _songList = MutableStateFlow(listOf<Song>())
|
private val _songList = MutableStateFlow(listOf<Song>())
|
||||||
/** A list of [Song]s, sorted by the preferred [Sort], to be shown in the home view. */
|
/** A list of [Song]s, sorted by the preferred [Sort], to be shown in the home view. */
|
||||||
val songList: StateFlow<List<Song>>
|
val songList: StateFlow<List<Song>>
|
||||||
|
@ -131,6 +129,8 @@ constructor(
|
||||||
val playlistSort: Sort
|
val playlistSort: Sort
|
||||||
get() = listSettings.playlistSort
|
get() = listSettings.playlistSort
|
||||||
|
|
||||||
|
private val homeGenerator = homeGeneratorFactory.create(this)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A list of [MusicType] corresponding to the current [Tab] configuration, excluding invisible
|
* A list of [MusicType] corresponding to the current [Tab] configuration, excluding invisible
|
||||||
* [Tab]s.
|
* [Tab]s.
|
||||||
|
@ -163,6 +163,10 @@ constructor(
|
||||||
val showOuter: Event<Outer>
|
val showOuter: Event<Outer>
|
||||||
get() = _showOuter
|
get() = _showOuter
|
||||||
|
|
||||||
|
init {
|
||||||
|
homeGenerator.attach()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
super.onCleared()
|
super.onCleared()
|
||||||
homeGenerator.release()
|
homeGenerator.release()
|
||||||
|
@ -171,24 +175,24 @@ constructor(
|
||||||
override fun invalidateMusic(type: MusicType, instructions: UpdateInstructions) {
|
override fun invalidateMusic(type: MusicType, instructions: UpdateInstructions) {
|
||||||
when (type) {
|
when (type) {
|
||||||
MusicType.SONGS -> {
|
MusicType.SONGS -> {
|
||||||
_songList.value = homeGenerator.songs()
|
|
||||||
_songInstructions.put(instructions)
|
_songInstructions.put(instructions)
|
||||||
|
_songList.value = homeGenerator.songs()
|
||||||
}
|
}
|
||||||
MusicType.ALBUMS -> {
|
MusicType.ALBUMS -> {
|
||||||
_albumList.value = homeGenerator.albums()
|
|
||||||
_albumInstructions.put(instructions)
|
_albumInstructions.put(instructions)
|
||||||
|
_albumList.value = homeGenerator.albums()
|
||||||
}
|
}
|
||||||
MusicType.ARTISTS -> {
|
MusicType.ARTISTS -> {
|
||||||
_artistList.value = homeGenerator.artists()
|
|
||||||
_artistInstructions.put(instructions)
|
_artistInstructions.put(instructions)
|
||||||
|
_artistList.value = homeGenerator.artists()
|
||||||
}
|
}
|
||||||
MusicType.GENRES -> {
|
MusicType.GENRES -> {
|
||||||
_genreList.value = homeGenerator.genres()
|
|
||||||
_genreInstructions.put(instructions)
|
_genreInstructions.put(instructions)
|
||||||
|
_genreList.value = homeGenerator.genres()
|
||||||
}
|
}
|
||||||
MusicType.PLAYLISTS -> {
|
MusicType.PLAYLISTS -> {
|
||||||
_playlistList.value = homeGenerator.playlists()
|
|
||||||
_playlistInstructions.put(instructions)
|
_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.
|
// On small screens, only display an icon.
|
||||||
width < 370 -> tab.setIcon(icon).setContentDescription(homeTab.nameRes)
|
width < 370 -> tab.setIcon(icon).setContentDescription(homeTab.nameRes)
|
||||||
// On large screens, display an icon and text.
|
// 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.
|
// On medium-size screens, display text.
|
||||||
else -> tab.setIcon(icon).setText(homeTab.nameRes)
|
else -> tab.setIcon(icon).setText(homeTab.nameRes)
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,12 +46,19 @@ interface ListSettings : Settings<ListSettings.Listener> {
|
||||||
|
|
||||||
interface Listener {
|
interface Listener {
|
||||||
fun onSongSortChanged() {}
|
fun onSongSortChanged() {}
|
||||||
|
|
||||||
fun onAlbumSortChanged() {}
|
fun onAlbumSortChanged() {}
|
||||||
|
|
||||||
fun onAlbumSongSortChanged() {}
|
fun onAlbumSongSortChanged() {}
|
||||||
|
|
||||||
fun onArtistSortChanged() {}
|
fun onArtistSortChanged() {}
|
||||||
|
|
||||||
fun onArtistSongSortChanged() {}
|
fun onArtistSongSortChanged() {}
|
||||||
|
|
||||||
fun onGenreSortChanged() {}
|
fun onGenreSortChanged() {}
|
||||||
|
|
||||||
fun onGenreSongSortChanged() {}
|
fun onGenreSongSortChanged() {}
|
||||||
|
|
||||||
fun onPlaylistSortChanged() {}
|
fun onPlaylistSortChanged() {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,8 @@
|
||||||
|
|
||||||
package org.oxycblt.auxio.music.info
|
package org.oxycblt.auxio.music.info
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.list.Item
|
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)
|
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(
|
.newWakeLock(
|
||||||
PowerManager.PARTIAL_WAKE_LOCK, BuildConfig.APPLICATION_ID + ":IndexingComponent")
|
PowerManager.PARTIAL_WAKE_LOCK, BuildConfig.APPLICATION_ID + ":IndexingComponent")
|
||||||
|
|
||||||
init {
|
fun attach() {
|
||||||
musicSettings.registerListener(this)
|
musicSettings.registerListener(this)
|
||||||
musicRepository.addUpdateListener(this)
|
musicRepository.addUpdateListener(this)
|
||||||
musicRepository.addIndexingListener(this)
|
musicRepository.addIndexingListener(this)
|
||||||
|
|
|
@ -18,17 +18,12 @@
|
||||||
|
|
||||||
package org.oxycblt.auxio.music.service
|
package org.oxycblt.auxio.music.service
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.support.v4.media.MediaBrowserCompat.MediaItem
|
import android.support.v4.media.MediaBrowserCompat.MediaItem
|
||||||
import android.support.v4.media.MediaDescriptionCompat
|
import android.support.v4.media.MediaDescriptionCompat
|
||||||
import android.view.MenuInflater
|
|
||||||
import androidx.annotation.MenuRes
|
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.appcompat.view.menu.MenuBuilder
|
|
||||||
import androidx.core.view.children
|
|
||||||
import androidx.media.utils.MediaConstants
|
import androidx.media.utils.MediaConstants
|
||||||
import org.oxycblt.auxio.BuildConfig
|
import org.oxycblt.auxio.BuildConfig
|
||||||
import org.oxycblt.auxio.R
|
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
|
typealias Sugar = Bundle.(Context) -> Unit
|
||||||
|
|
||||||
fun header(@StringRes nameRes: Int): Sugar = {
|
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)
|
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 {
|
private fun makeExtras(context: Context, vararg sugars: Sugar): Bundle {
|
||||||
return Bundle().apply { sugars.forEach { this.it(context) } }
|
return Bundle().apply { sugars.forEach { this.it(context) } }
|
||||||
}
|
}
|
||||||
|
@ -181,7 +126,7 @@ fun Song.toMediaDescription(
|
||||||
} else {
|
} else {
|
||||||
MediaSessionUID.ChildItem(parent.uid, uid)
|
MediaSessionUID.ChildItem(parent.uid, uid)
|
||||||
}
|
}
|
||||||
val extras = makeExtras(context, *sugar, menu(R.menu.song))
|
val extras = makeExtras(context, *sugar)
|
||||||
return MediaDescriptionCompat.Builder()
|
return MediaDescriptionCompat.Builder()
|
||||||
.setMediaId(mediaSessionUID.toString())
|
.setMediaId(mediaSessionUID.toString())
|
||||||
.setTitle(name.resolve(context))
|
.setTitle(name.resolve(context))
|
||||||
|
@ -212,7 +157,7 @@ fun Album.toMediaItem(
|
||||||
} else {
|
} else {
|
||||||
MediaSessionUID.ChildItem(parent.uid, uid)
|
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 counts = context.getPlural(R.plurals.fmt_song_count, songs.size)
|
||||||
val description =
|
val description =
|
||||||
MediaDescriptionCompat.Builder()
|
MediaDescriptionCompat.Builder()
|
||||||
|
@ -241,7 +186,7 @@ fun Artist.toMediaItem(context: Context, vararg sugar: Sugar): MediaItem {
|
||||||
} else {
|
} else {
|
||||||
context.getString(R.string.def_song_count)
|
context.getString(R.string.def_song_count)
|
||||||
})
|
})
|
||||||
val extras = makeExtras(context, *sugar, menu(R.menu.parent))
|
val extras = makeExtras(context, *sugar)
|
||||||
val description =
|
val description =
|
||||||
MediaDescriptionCompat.Builder()
|
MediaDescriptionCompat.Builder()
|
||||||
.setMediaId(mediaSessionUID.toString())
|
.setMediaId(mediaSessionUID.toString())
|
||||||
|
@ -262,7 +207,7 @@ fun Genre.toMediaItem(context: Context, vararg sugar: Sugar): MediaItem {
|
||||||
} else {
|
} else {
|
||||||
context.getString(R.string.def_song_count)
|
context.getString(R.string.def_song_count)
|
||||||
}
|
}
|
||||||
val extras = makeExtras(context, *sugar, menu(R.menu.parent))
|
val extras = makeExtras(context, *sugar)
|
||||||
val description =
|
val description =
|
||||||
MediaDescriptionCompat.Builder()
|
MediaDescriptionCompat.Builder()
|
||||||
.setMediaId(mediaSessionUID.toString())
|
.setMediaId(mediaSessionUID.toString())
|
||||||
|
@ -282,7 +227,7 @@ fun Playlist.toMediaItem(context: Context, vararg sugar: Sugar): MediaItem {
|
||||||
} else {
|
} else {
|
||||||
context.getString(R.string.def_song_count)
|
context.getString(R.string.def_song_count)
|
||||||
}
|
}
|
||||||
val extras = makeExtras(context, *sugar, menu(R.menu.playlist))
|
val extras = makeExtras(context, *sugar)
|
||||||
val description =
|
val description =
|
||||||
MediaDescriptionCompat.Builder()
|
MediaDescriptionCompat.Builder()
|
||||||
.setMediaId(mediaSessionUID.toString())
|
.setMediaId(mediaSessionUID.toString())
|
||||||
|
|
|
@ -24,13 +24,8 @@ import javax.inject.Inject
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.detail.DetailGenerator
|
import org.oxycblt.auxio.detail.DetailGenerator
|
||||||
import org.oxycblt.auxio.detail.DetailSection
|
import org.oxycblt.auxio.detail.DetailSection
|
||||||
import org.oxycblt.auxio.detail.list.SortHeader
|
|
||||||
import org.oxycblt.auxio.home.HomeGenerator
|
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.adapter.UpdateInstructions
|
||||||
import org.oxycblt.auxio.list.sort.Sort
|
|
||||||
import org.oxycblt.auxio.music.Album
|
import org.oxycblt.auxio.music.Album
|
||||||
import org.oxycblt.auxio.music.Artist
|
import org.oxycblt.auxio.music.Artist
|
||||||
import org.oxycblt.auxio.music.Genre
|
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.MusicType
|
||||||
import org.oxycblt.auxio.music.Playlist
|
import org.oxycblt.auxio.music.Playlist
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
|
import org.oxycblt.auxio.music.info.resolveNumber
|
||||||
import org.oxycblt.auxio.search.SearchEngine
|
import org.oxycblt.auxio.search.SearchEngine
|
||||||
|
|
||||||
class MusicBrowser
|
class MusicBrowser
|
||||||
|
@ -76,6 +72,11 @@ private constructor(
|
||||||
private val homeGenerator = homeGeneratorFactory.create(this)
|
private val homeGenerator = homeGeneratorFactory.create(this)
|
||||||
private val detailGenerator = detailGeneratorFactory.create(this)
|
private val detailGenerator = detailGeneratorFactory.create(this)
|
||||||
|
|
||||||
|
fun attach() {
|
||||||
|
homeGenerator.attach()
|
||||||
|
detailGenerator.attach()
|
||||||
|
}
|
||||||
|
|
||||||
fun release() {
|
fun release() {
|
||||||
homeGenerator.release()
|
homeGenerator.release()
|
||||||
detailGenerator.release()
|
detailGenerator.release()
|
||||||
|
@ -87,24 +88,22 @@ private constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun invalidateTabs() {
|
override fun invalidateTabs() {
|
||||||
for (i in 0..10) {
|
val rootId = MediaSessionUID.Tab(TabNode.Root).toString()
|
||||||
// TODO: Temporary bodge, move the amount parameter to a bundle extra
|
val moreId = MediaSessionUID.Tab(TabNode.More).toString()
|
||||||
val rootId = MediaSessionUID.Tab(TabNode.Root(i)).toString()
|
invalidator.invalidateMusic(setOf(rootId, moreId))
|
||||||
val moreId = MediaSessionUID.Tab(TabNode.More(i)).toString()
|
|
||||||
invalidator.invalidateMusic(setOf(rootId, moreId))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun invalidate(type: MusicType, replace: Int?) {
|
override fun invalidate(type: MusicType, replace: Int?) {
|
||||||
val deviceLibrary = musicRepository.deviceLibrary ?: return
|
val deviceLibrary = musicRepository.deviceLibrary ?: return
|
||||||
val userLibrary = musicRepository.userLibrary ?: return
|
val userLibrary = musicRepository.userLibrary ?: return
|
||||||
val music = when (type) {
|
val music =
|
||||||
MusicType.ALBUMS -> deviceLibrary.albums
|
when (type) {
|
||||||
MusicType.ARTISTS -> deviceLibrary.artists
|
MusicType.ALBUMS -> deviceLibrary.albums
|
||||||
MusicType.GENRES -> deviceLibrary.genres
|
MusicType.ARTISTS -> deviceLibrary.artists
|
||||||
MusicType.PLAYLISTS -> userLibrary.playlists
|
MusicType.GENRES -> deviceLibrary.genres
|
||||||
else -> return
|
MusicType.PLAYLISTS -> userLibrary.playlists
|
||||||
}
|
else -> return
|
||||||
|
}
|
||||||
if (music.isEmpty()) {
|
if (music.isEmpty()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -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 deviceLibrary = musicRepository.deviceLibrary
|
||||||
val userLibrary = musicRepository.userLibrary
|
val userLibrary = musicRepository.userLibrary
|
||||||
if (deviceLibrary == null || userLibrary == null) {
|
if (deviceLibrary == null || userLibrary == null) {
|
||||||
return listOf()
|
return listOf()
|
||||||
}
|
}
|
||||||
|
return getMediaItemList(parentId, maxTabs)
|
||||||
return getMediaItemList(parentId)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun search(query: String): MutableList<MediaItem> {
|
suspend fun search(query: String): MutableList<MediaItem> {
|
||||||
|
@ -179,10 +177,10 @@ private constructor(
|
||||||
return music
|
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)) {
|
return when (val mediaSessionUID = MediaSessionUID.fromString(id)) {
|
||||||
is MediaSessionUID.Tab -> {
|
is MediaSessionUID.Tab -> {
|
||||||
getCategoryMediaItems(mediaSessionUID.node)
|
getCategoryMediaItems(mediaSessionUID.node, maxTabs)
|
||||||
}
|
}
|
||||||
is MediaSessionUID.SingleItem -> {
|
is MediaSessionUID.SingleItem -> {
|
||||||
getChildMediaItems(mediaSessionUID.uid)
|
getChildMediaItems(mediaSessionUID.uid)
|
||||||
|
@ -196,22 +194,22 @@ private constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getCategoryMediaItems(node: TabNode) =
|
private fun getCategoryMediaItems(node: TabNode, maxTabs: Int) =
|
||||||
when (node) {
|
when (node) {
|
||||||
is TabNode.Root -> {
|
is TabNode.Root -> {
|
||||||
val tabs = homeGenerator.tabs()
|
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) {
|
if (base.size < tabs.size) {
|
||||||
base + TabNode.More(tabs.size - base.size)
|
base + TabNode.More
|
||||||
} else {
|
} else {
|
||||||
base
|
base
|
||||||
}
|
}
|
||||||
.map { it.toMediaItem(context) }
|
.map { it.toMediaItem(context) }
|
||||||
}
|
}
|
||||||
is TabNode.More ->
|
is TabNode.More -> {
|
||||||
homeGenerator.tabs().takeLast(node.remainder).map {
|
val tabs = homeGenerator.tabs()
|
||||||
TabNode.Home(it).toMediaItem(context)
|
tabs.takeLast(tabs.size - maxTabs).map { TabNode.Home(it).toMediaItem(context) }
|
||||||
}
|
}
|
||||||
is TabNode.Home ->
|
is TabNode.Home ->
|
||||||
when (node.type) {
|
when (node.type) {
|
||||||
MusicType.SONGS -> homeGenerator.songs().map { it.toMediaItem(context, null) }
|
MusicType.SONGS -> homeGenerator.songs().map { it.toMediaItem(context, null) }
|
||||||
|
@ -226,20 +224,17 @@ private constructor(
|
||||||
val detail = detailGenerator.any(uid) ?: return null
|
val detail = detailGenerator.any(uid) ?: return null
|
||||||
return detail.sections.flatMap { section ->
|
return detail.sections.flatMap { section ->
|
||||||
when (section) {
|
when (section) {
|
||||||
is DetailSection.Songs -> section.items.map { it.toMediaItem(context, null, header(section.stringRes)) }
|
is DetailSection.Songs ->
|
||||||
is DetailSection.Albums -> section.items.map { it.toMediaItem(context, null, header(section.stringRes)) }
|
section.items.map { it.toMediaItem(context, null, header(section.stringRes)) }
|
||||||
is DetailSection.Artists -> section.items.map { it.toMediaItem(context, header(section.stringRes)) }
|
is DetailSection.Albums ->
|
||||||
is DetailSection.Discs -> section.discs.flatMap {
|
section.items.map { it.toMediaItem(context, null, header(section.stringRes)) }
|
||||||
section.discs.flatMap { entry ->
|
is DetailSection.Artists ->
|
||||||
val disc = entry.key
|
section.items.map { it.toMediaItem(context, header(section.stringRes)) }
|
||||||
val discString = if (disc != null) {
|
is DetailSection.Discs ->
|
||||||
context.getString(R.string.fmt_disc_no, disc.number)
|
section.discs.flatMap { (disc, songs) ->
|
||||||
} else {
|
val discString = disc.resolveNumber(context)
|
||||||
context.getString(R.string.def_disc)
|
songs.map { it.toMediaItem(context, null, header(discString)) }
|
||||||
}
|
|
||||||
entry.value.map { it.toMediaItem(context, null, header(discString)) }
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
else -> error("Unknown section type: $section")
|
else -> error("Unknown section type: $section")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,7 +23,6 @@ import android.os.Bundle
|
||||||
import android.support.v4.media.MediaBrowserCompat.MediaItem
|
import android.support.v4.media.MediaBrowserCompat.MediaItem
|
||||||
import androidx.media.MediaBrowserServiceCompat.BrowserRoot
|
import androidx.media.MediaBrowserServiceCompat.BrowserRoot
|
||||||
import androidx.media.MediaBrowserServiceCompat.Result
|
import androidx.media.MediaBrowserServiceCompat.Result
|
||||||
import androidx.media.utils.MediaConstants
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
@ -75,6 +74,11 @@ constructor(
|
||||||
fun invalidateMusic(mediaId: String)
|
fun invalidateMusic(mediaId: String)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun attach() {
|
||||||
|
indexer.attach()
|
||||||
|
musicBrowser.attach()
|
||||||
|
}
|
||||||
|
|
||||||
fun release() {
|
fun release() {
|
||||||
dispatchJob.cancel()
|
dispatchJob.cancel()
|
||||||
musicBrowser.release()
|
musicBrowser.release()
|
||||||
|
@ -95,30 +99,17 @@ constructor(
|
||||||
indexer.createNotification(post)
|
indexer.createNotification(post)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getRoot(maxItems: Int) =
|
fun getRoot() = BrowserRoot(MediaSessionUID.Tab(TabNode.Root).toString(), Bundle())
|
||||||
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 getItem(mediaId: String, result: Result<MediaItem>) =
|
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>>) =
|
fun getChildren(mediaId: String, maxTabs: Int, result: Result<MutableList<MediaItem>>) =
|
||||||
result.dispatch { musicBrowser.getChildren(mediaId)?.toMutableList() }
|
result.dispatch { musicBrowser.getChildren(mediaId, maxTabs)?.toMutableList() }
|
||||||
|
|
||||||
fun search(query: String, result: Result<MutableList<MediaItem>>) =
|
fun search(query: String, result: Result<MutableList<MediaItem>>) =
|
||||||
result.dispatchAsync { musicBrowser.search(query) }
|
result.dispatchAsync { musicBrowser.search(query) }
|
||||||
|
|
|
@ -23,37 +23,27 @@ import org.oxycblt.auxio.music.MusicType
|
||||||
|
|
||||||
sealed class TabNode {
|
sealed class TabNode {
|
||||||
abstract val id: String
|
abstract val id: String
|
||||||
abstract val data: Int
|
|
||||||
abstract val nameRes: Int
|
abstract val nameRes: Int
|
||||||
abstract val bitmapRes: Int?
|
abstract val bitmapRes: Int?
|
||||||
|
|
||||||
override fun toString() = "${id}/${data}"
|
override fun toString() = id
|
||||||
|
|
||||||
data class Root(val amount: Int) : TabNode() {
|
data object Root : TabNode() {
|
||||||
override val id = ID
|
override val id = "root"
|
||||||
override val data = amount
|
|
||||||
override val nameRes = R.string.info_app_name
|
override val nameRes = R.string.info_app_name
|
||||||
override val bitmapRes = null
|
override val bitmapRes = null
|
||||||
|
|
||||||
companion object {
|
override fun toString() = id
|
||||||
const val ID = "root"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data class More(val remainder: Int) : TabNode() {
|
data object More : TabNode() {
|
||||||
override val id = ID
|
override val id = "more"
|
||||||
override val data = remainder
|
|
||||||
override val nameRes = R.string.lbl_more
|
override val nameRes = R.string.lbl_more
|
||||||
override val bitmapRes = null
|
override val bitmapRes = R.drawable.ic_more_bitmap_24
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val ID = "more"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data class Home(val type: MusicType) : TabNode() {
|
data class Home(val type: MusicType) : TabNode() {
|
||||||
override val id = ID
|
override val id = "$ID/${type.intCode}"
|
||||||
override val data = type.intCode
|
|
||||||
override val bitmapRes: Int
|
override val bitmapRes: Int
|
||||||
get() =
|
get() =
|
||||||
when (type) {
|
when (type) {
|
||||||
|
@ -73,15 +63,15 @@ sealed class TabNode {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromString(str: String): TabNode? {
|
fun fromString(str: String): TabNode? {
|
||||||
val split = str.split("/", limit = 2)
|
return when {
|
||||||
if (split.size != 2) {
|
str == Root.id -> Root
|
||||||
return null
|
str == More.id -> More
|
||||||
}
|
str.startsWith(Home.ID) -> {
|
||||||
val data = split[1].toIntOrNull() ?: return null
|
val split = str.split("/")
|
||||||
return when (split[0]) {
|
if (split.size != 2) return null
|
||||||
Root.ID -> Root(data)
|
val intCode = split[1].toIntOrNull() ?: return null
|
||||||
More.ID -> More(data)
|
Home(MusicType.fromIntCode(intCode) ?: return null)
|
||||||
Home.ID -> Home(MusicType.fromIntCode(data) ?: return null)
|
}
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* 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.common.C
|
||||||
import androidx.media3.exoplayer.source.ShuffleOrder
|
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
|
* 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
|
* 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
|
* 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/>.
|
* 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.Context
|
||||||
import androidx.media3.datasource.ContentDataSource
|
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.ogg.OggExtractor
|
||||||
import androidx.media3.extractor.ts.AdtsExtractor
|
import androidx.media3.extractor.ts.AdtsExtractor
|
||||||
import androidx.media3.extractor.wav.WavExtractor
|
import androidx.media3.extractor.wav.WavExtractor
|
||||||
|
import dagger.Binds
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
|
@ -40,7 +41,13 @@ import dagger.hilt.components.SingletonComponent
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
@InstallIn(SingletonComponent::class)
|
@InstallIn(SingletonComponent::class)
|
||||||
class SystemModule {
|
interface PlayerModule {
|
||||||
|
@Binds fun playerKernelFactory(factory: PlayerKernelFactoryImpl): PlayerKernel.Factory
|
||||||
|
}
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@InstallIn(SingletonComponent::class)
|
||||||
|
class ExoPlayerModule {
|
||||||
@Provides
|
@Provides
|
||||||
fun mediaSourceFactory(
|
fun mediaSourceFactory(
|
||||||
dataSourceFactory: DataSource.Factory,
|
dataSourceFactory: DataSource.Factory,
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2024 Auxio Project
|
* 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
|
* 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
|
* 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/>.
|
* 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.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.media.audiofx.AudioEffect
|
import android.media.audiofx.AudioEffect
|
||||||
import androidx.media3.common.AudioAttributes
|
|
||||||
import androidx.media3.common.C
|
|
||||||
import androidx.media3.common.MediaItem
|
import androidx.media3.common.MediaItem
|
||||||
import androidx.media3.common.PlaybackException
|
import androidx.media3.common.PlaybackException
|
||||||
import androidx.media3.common.Player
|
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 javax.inject.Inject
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
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.logD
|
||||||
import org.oxycblt.auxio.util.logE
|
import org.oxycblt.auxio.util.logE
|
||||||
|
|
||||||
class ExoPlaybackStateHolder(
|
class PlayerStateHolder(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val player: ExoPlayer,
|
playerKernelFactory: PlayerKernel.Factory,
|
||||||
|
gaplessQueuerFactory: GaplessQueuer.Factory,
|
||||||
private val playbackManager: PlaybackStateManager,
|
private val playbackManager: PlaybackStateManager,
|
||||||
private val persistenceRepository: PersistenceRepository,
|
private val persistenceRepository: PersistenceRepository,
|
||||||
private val playbackSettings: PlaybackSettings,
|
private val playbackSettings: PlaybackSettings,
|
||||||
|
@ -73,30 +64,58 @@ class ExoPlaybackStateHolder(
|
||||||
private val imageSettings: ImageSettings
|
private val imageSettings: ImageSettings
|
||||||
) :
|
) :
|
||||||
PlaybackStateHolder,
|
PlaybackStateHolder,
|
||||||
Player.Listener,
|
PlayerKernel.Listener,
|
||||||
|
Queuer.Listener,
|
||||||
MusicRepository.UpdateListener,
|
MusicRepository.UpdateListener,
|
||||||
PlaybackSettings.Listener,
|
|
||||||
ImageSettings.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 saveJob = Job()
|
||||||
private val saveScope = CoroutineScope(Dispatchers.IO + saveJob)
|
private val saveScope = CoroutineScope(Dispatchers.IO + saveJob)
|
||||||
private val restoreScope = CoroutineScope(Dispatchers.IO + saveJob)
|
private val restoreScope = CoroutineScope(Dispatchers.IO + saveJob)
|
||||||
private var currentSaveJob: Job? = null
|
private var currentSaveJob: Job? = null
|
||||||
private var openAudioEffectSession = false
|
private var openAudioEffectSession = false
|
||||||
|
private val player = playerKernelFactory.create(context, this, gaplessQueuerFactory, this)
|
||||||
|
|
||||||
var sessionOngoing = false
|
var sessionOngoing = false
|
||||||
private set
|
private set
|
||||||
|
|
||||||
init {
|
fun attach() {
|
||||||
|
player.attach()
|
||||||
imageSettings.registerListener(this)
|
imageSettings.registerListener(this)
|
||||||
player.addListener(this)
|
|
||||||
playbackManager.registerStateHolder(this)
|
playbackManager.registerStateHolder(this)
|
||||||
playbackSettings.registerListener(this)
|
|
||||||
musicRepository.addUpdateListener(this)
|
musicRepository.addUpdateListener(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun release() {
|
fun release() {
|
||||||
saveJob.cancel()
|
saveJob.cancel()
|
||||||
player.removeListener(this)
|
player.release()
|
||||||
playbackManager.unregisterStateHolder(this)
|
playbackManager.unregisterStateHolder(this)
|
||||||
musicRepository.removeUpdateListener(this)
|
musicRepository.removeUpdateListener(this)
|
||||||
replayGainProcessor.release()
|
replayGainProcessor.release()
|
||||||
|
@ -109,7 +128,7 @@ class ExoPlaybackStateHolder(
|
||||||
|
|
||||||
override val progression: Progression
|
override val progression: Progression
|
||||||
get() {
|
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 duration = mediaItem.mediaMetadata.extras?.getLong("durationMs") ?: Long.MAX_VALUE
|
||||||
val clampedPosition = player.currentPosition.coerceAtLeast(0).coerceAtMost(duration)
|
val clampedPosition = player.currentPosition.coerceAtLeast(0).coerceAtMost(duration)
|
||||||
return Progression.from(player.playWhenReady, player.isPlaying, clampedPosition)
|
return Progression.from(player.playWhenReady, player.isPlaying, clampedPosition)
|
||||||
|
@ -117,7 +136,7 @@ class ExoPlaybackStateHolder(
|
||||||
|
|
||||||
override val repeatMode
|
override val repeatMode
|
||||||
get() =
|
get() =
|
||||||
when (val repeatMode = player.repeatMode) {
|
when (val repeatMode = player.queuer.repeatMode) {
|
||||||
Player.REPEAT_MODE_OFF -> RepeatMode.NONE
|
Player.REPEAT_MODE_OFF -> RepeatMode.NONE
|
||||||
Player.REPEAT_MODE_ONE -> RepeatMode.TRACK
|
Player.REPEAT_MODE_ONE -> RepeatMode.TRACK
|
||||||
Player.REPEAT_MODE_ALL -> RepeatMode.ALL
|
Player.REPEAT_MODE_ALL -> RepeatMode.ALL
|
||||||
|
@ -128,18 +147,11 @@ class ExoPlaybackStateHolder(
|
||||||
get() = player.audioSessionId
|
get() = player.audioSessionId
|
||||||
|
|
||||||
override fun resolveQueue(): RawQueue {
|
override fun resolveQueue(): RawQueue {
|
||||||
val deviceLibrary =
|
val heap = player.queuer.computeHeap()
|
||||||
musicRepository.deviceLibrary
|
|
||||||
// No library, cannot do anything.
|
|
||||||
?: return RawQueue(emptyList(), emptyList(), 0)
|
|
||||||
val heap = (0 until player.mediaItemCount).map { player.getMediaItemAt(it) }
|
|
||||||
val shuffledMapping =
|
val shuffledMapping =
|
||||||
if (player.shuffleModeEnabled) {
|
if (player.queuer.shuffleModeEnabled) player.queuer.computeMapping() else emptyList()
|
||||||
player.unscrambleQueueIndices()
|
return RawQueue(
|
||||||
} else {
|
heap.mapNotNull { it.song }, shuffledMapping, player.queuer.currentMediaItemIndex)
|
||||||
emptyList()
|
|
||||||
}
|
|
||||||
return RawQueue(heap.mapNotNull { it.song }, shuffledMapping, player.currentMediaItemIndex)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun handleDeferred(action: DeferredPlayback): Boolean {
|
override fun handleDeferred(action: DeferredPlayback): Boolean {
|
||||||
|
@ -202,43 +214,31 @@ class ExoPlaybackStateHolder(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun repeatMode(repeatMode: RepeatMode) {
|
override fun repeatMode(repeatMode: RepeatMode) {
|
||||||
player.repeatMode =
|
player.queuer.repeatMode =
|
||||||
when (repeatMode) {
|
when (repeatMode) {
|
||||||
RepeatMode.NONE -> Player.REPEAT_MODE_OFF
|
RepeatMode.NONE -> Player.REPEAT_MODE_OFF
|
||||||
RepeatMode.ALL -> Player.REPEAT_MODE_ALL
|
RepeatMode.ALL -> Player.REPEAT_MODE_ALL
|
||||||
RepeatMode.TRACK -> Player.REPEAT_MODE_ONE
|
RepeatMode.TRACK -> Player.REPEAT_MODE_ONE
|
||||||
}
|
}
|
||||||
updatePauseOnRepeat()
|
|
||||||
playbackManager.ack(this, StateAck.RepeatModeChanged)
|
playbackManager.ack(this, StateAck.RepeatModeChanged)
|
||||||
deferSave()
|
deferSave()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun newPlayback(command: PlaybackCommand) {
|
override fun newPlayback(command: PlaybackCommand) {
|
||||||
parent = command.parent
|
parent = command.parent
|
||||||
player.shuffleModeEnabled = command.shuffled
|
val mediaItems = command.queue.map { it.buildMediaItem() }
|
||||||
player.setMediaItems(command.queue.map { it.buildMediaItem() })
|
|
||||||
val startIndex =
|
val startIndex =
|
||||||
command.song
|
command.song
|
||||||
?.let { command.queue.indexOf(it) }
|
?.let { command.queue.indexOf(it) }
|
||||||
.also { check(it != -1) { "Start song not in queue" } }
|
.also { check(it != -1) { "Start song not in queue" } }
|
||||||
if (command.shuffled) {
|
player.queuer.prepareNew(mediaItems, startIndex, 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.play()
|
player.play()
|
||||||
playbackManager.ack(this, StateAck.NewPlayback)
|
playbackManager.ack(this, StateAck.NewPlayback)
|
||||||
deferSave()
|
deferSave()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun shuffled(shuffled: Boolean) {
|
override fun shuffled(shuffled: Boolean) {
|
||||||
player.setShuffleModeEnabled(shuffled)
|
player.queuer.shuffled(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))
|
|
||||||
}
|
|
||||||
playbackManager.ack(this, StateAck.QueueReordered)
|
playbackManager.ack(this, StateAck.QueueReordered)
|
||||||
deferSave()
|
deferSave()
|
||||||
}
|
}
|
||||||
|
@ -247,14 +247,14 @@ class ExoPlaybackStateHolder(
|
||||||
// Replicate the old pseudo-circular queue behavior when no repeat option is implemented.
|
// 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
|
// 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.
|
// wrap around the queue, albeit playback will be paused.
|
||||||
if (player.repeatMode == Player.REPEAT_MODE_ALL || player.hasNextMediaItem()) {
|
if (player.queuer.repeatMode == Player.REPEAT_MODE_ALL ||
|
||||||
player.seekToNext()
|
player.queuer.hasNextMediaItem()) {
|
||||||
|
player.queuer.seekToNext()
|
||||||
if (!playbackSettings.rememberPause) {
|
if (!playbackSettings.rememberPause) {
|
||||||
player.play()
|
player.play()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
player.seekTo(
|
player.queuer.goto(player.queuer.computeFirstMediaItemIndex())
|
||||||
player.currentTimeline.getFirstWindowIndex(player.shuffleModeEnabled), C.TIME_UNSET)
|
|
||||||
// TODO: Dislike the UX implications of this, I feel should I bite the bullet
|
// TODO: Dislike the UX implications of this, I feel should I bite the bullet
|
||||||
// and switch to dynamic skip enable/disable?
|
// and switch to dynamic skip enable/disable?
|
||||||
if (!playbackSettings.rememberPause) {
|
if (!playbackSettings.rememberPause) {
|
||||||
|
@ -267,9 +267,9 @@ class ExoPlaybackStateHolder(
|
||||||
|
|
||||||
override fun prev() {
|
override fun prev() {
|
||||||
if (playbackSettings.rewindWithPrev) {
|
if (playbackSettings.rewindWithPrev) {
|
||||||
player.seekToPrevious()
|
player.queuer.seekToPrevious()
|
||||||
} else if (player.hasPreviousMediaItem()) {
|
} else if (player.queuer.hasPreviousMediaItem()) {
|
||||||
player.seekToPreviousMediaItem()
|
player.queuer.seekToPreviousMediaItem()
|
||||||
} else {
|
} else {
|
||||||
player.seekTo(0)
|
player.seekTo(0)
|
||||||
}
|
}
|
||||||
|
@ -281,13 +281,12 @@ class ExoPlaybackStateHolder(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun goto(index: Int) {
|
override fun goto(index: Int) {
|
||||||
val indices = player.unscrambleQueueIndices()
|
val indices = player.queuer.computeMapping()
|
||||||
if (indices.isEmpty()) {
|
if (indices.isEmpty()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val trueIndex = indices[index]
|
val trueIndex = indices[index]
|
||||||
player.seekTo(trueIndex, C.TIME_UNSET) // Handles remaining custom logic
|
player.queuer.goto(trueIndex)
|
||||||
if (!playbackSettings.rememberPause) {
|
if (!playbackSettings.rememberPause) {
|
||||||
player.play()
|
player.play()
|
||||||
}
|
}
|
||||||
|
@ -296,63 +295,40 @@ class ExoPlaybackStateHolder(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun playNext(songs: List<Song>, ack: StateAck.PlayNext) {
|
override fun playNext(songs: List<Song>, ack: StateAck.PlayNext) {
|
||||||
val currTimeline = player.currentTimeline
|
player.queuer.addBottomMediaItems(songs.map { it.buildMediaItem() })
|
||||||
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() })
|
|
||||||
}
|
|
||||||
playbackManager.ack(this, ack)
|
playbackManager.ack(this, ack)
|
||||||
deferSave()
|
deferSave()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun addToQueue(songs: List<Song>, ack: StateAck.AddToQueue) {
|
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)
|
playbackManager.ack(this, ack)
|
||||||
deferSave()
|
deferSave()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun move(from: Int, to: Int, ack: StateAck.Move) {
|
override fun move(from: Int, to: Int, ack: StateAck.Move) {
|
||||||
val indices = player.unscrambleQueueIndices()
|
val indices = player.queuer.computeMapping()
|
||||||
if (indices.isEmpty()) {
|
if (indices.isEmpty()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val trueFrom = indices[from]
|
val trueFrom = indices[from]
|
||||||
val trueTo = indices[to]
|
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.
|
player.queuer.moveMediaItem(trueFrom, trueTo)
|
||||||
when {
|
|
||||||
trueFrom > trueTo -> {
|
|
||||||
player.moveMediaItem(trueFrom, trueTo)
|
|
||||||
player.moveMediaItem(trueTo + 1, trueFrom)
|
|
||||||
}
|
|
||||||
trueTo > trueFrom -> {
|
|
||||||
player.moveMediaItem(trueFrom, trueTo)
|
|
||||||
player.moveMediaItem(trueTo - 1, trueFrom)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
playbackManager.ack(this, ack)
|
playbackManager.ack(this, ack)
|
||||||
deferSave()
|
deferSave()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun remove(at: Int, ack: StateAck.Remove) {
|
override fun remove(at: Int, ack: StateAck.Remove) {
|
||||||
val indices = player.unscrambleQueueIndices()
|
val indices = player.queuer.computeMapping()
|
||||||
if (indices.isEmpty()) {
|
if (indices.isEmpty()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val trueIndex = indices[at]
|
val trueIndex = indices[at]
|
||||||
val songWillChange = player.currentMediaItemIndex == trueIndex
|
val songWillChange = player.queuer.currentMediaItemIndex == trueIndex
|
||||||
player.removeMediaItem(trueIndex)
|
player.queuer.removeMediaItem(trueIndex)
|
||||||
if (songWillChange && !playbackSettings.rememberPause) {
|
if (songWillChange && !playbackSettings.rememberPause) {
|
||||||
player.play()
|
player.play()
|
||||||
}
|
}
|
||||||
|
@ -372,15 +348,11 @@ class ExoPlaybackStateHolder(
|
||||||
sendEvent = true
|
sendEvent = true
|
||||||
}
|
}
|
||||||
if (rawQueue != resolveQueue()) {
|
if (rawQueue != resolveQueue()) {
|
||||||
player.setMediaItems(rawQueue.heap.map { it.buildMediaItem() })
|
player.queuer.prepareSaved(
|
||||||
if (rawQueue.isShuffled) {
|
rawQueue.heap.map { it.buildMediaItem() },
|
||||||
player.shuffleModeEnabled = true
|
rawQueue.shuffledMapping,
|
||||||
player.setShuffleOrder(BetterShuffleOrder(rawQueue.shuffledMapping.toIntArray()))
|
rawQueue.heapIndex,
|
||||||
} else {
|
rawQueue.isShuffled)
|
||||||
player.shuffleModeEnabled = false
|
|
||||||
}
|
|
||||||
player.seekTo(rawQueue.heapIndex, C.TIME_UNSET)
|
|
||||||
player.prepare()
|
|
||||||
player.pause()
|
player.pause()
|
||||||
sendEvent = true
|
sendEvent = true
|
||||||
}
|
}
|
||||||
|
@ -404,16 +376,14 @@ class ExoPlaybackStateHolder(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun reset(ack: StateAck.NewPlayback) {
|
override fun reset(ack: StateAck.NewPlayback) {
|
||||||
player.setMediaItems(listOf())
|
player.queuer.discard()
|
||||||
playbackManager.ack(this, ack)
|
playbackManager.ack(this, ack)
|
||||||
deferSave()
|
deferSave()
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- PLAYER OVERRIDES ---
|
// --- PLAYER OVERRIDES ---
|
||||||
|
|
||||||
override fun onPlayWhenReadyChanged(playWhenReady: Boolean, reason: Int) {
|
override fun onPlayWhenReadyChanged() {
|
||||||
super.onPlayWhenReadyChanged(playWhenReady, reason)
|
|
||||||
|
|
||||||
if (player.playWhenReady) {
|
if (player.playWhenReady) {
|
||||||
// Mark that we have started playing so that the notification can now be posted.
|
// Mark that we have started playing so that the notification can now be posted.
|
||||||
logD("Player has started playing")
|
logD("Player has started playing")
|
||||||
|
@ -431,40 +401,27 @@ class ExoPlaybackStateHolder(
|
||||||
broadcastAudioEffectAction(AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION)
|
broadcastAudioEffectAction(AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION)
|
||||||
openAudioEffectSession = false
|
openAudioEffectSession = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
playbackManager.ack(this, StateAck.ProgressionChanged)
|
||||||
|
deferSave()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPlaybackStateChanged(playbackState: Int) {
|
override fun onIsPlayingChanged() {
|
||||||
super.onPlaybackStateChanged(playbackState)
|
playbackManager.ack(this, StateAck.ProgressionChanged)
|
||||||
|
deferSave()
|
||||||
if (playbackState == Player.STATE_ENDED && player.repeatMode == Player.REPEAT_MODE_OFF) {
|
|
||||||
goto(0)
|
|
||||||
player.pause()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
|
override fun onPositionDiscontinuity() {
|
||||||
super.onMediaItemTransition(mediaItem, reason)
|
playbackManager.ack(this, StateAck.ProgressionChanged)
|
||||||
|
deferSave()
|
||||||
if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_AUTO) {
|
|
||||||
playbackManager.ack(this, StateAck.IndexMoved)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onEvents(player: Player, events: Player.Events) {
|
override fun onAutoTransition() {
|
||||||
super.onEvents(player, events)
|
playbackManager.ack(this, StateAck.IndexMoved)
|
||||||
|
deferSave()
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPlayerError(error: PlaybackException) {
|
override fun onError(error: PlaybackException) {
|
||||||
// TODO: Replace with no skipping and a notification instead
|
// TODO: Replace with no skipping and a notification instead
|
||||||
// If there's any issue, just go to the next song.
|
// If there's any issue, just go to the next song.
|
||||||
logE("Player error occurred")
|
logE("Player error occurred")
|
||||||
|
@ -491,17 +448,7 @@ class ExoPlaybackStateHolder(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- PLAYBACKSETTINGS OVERRIDES ---
|
// --- OVERRIDES ---
|
||||||
|
|
||||||
override fun onPauseOnRepeatChanged() {
|
|
||||||
super.onPauseOnRepeatChanged()
|
|
||||||
updatePauseOnRepeat()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updatePauseOnRepeat() {
|
|
||||||
player.pauseAtEndOfMediaItems =
|
|
||||||
player.repeatMode == Player.REPEAT_MODE_ONE && playbackSettings.pauseOnRepeat
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun save(cb: () -> Unit) {
|
private fun save(cb: () -> Unit) {
|
||||||
saveJob {
|
saveJob {
|
||||||
|
@ -533,101 +480,6 @@ class ExoPlaybackStateHolder(
|
||||||
private val MediaItem.song: Song?
|
private val MediaItem.song: Song?
|
||||||
get() = this.localConfiguration?.tag as? 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 {
|
private companion object {
|
||||||
const val SAVE_BUFFER = 5000L
|
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)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
class ReplayGainAudioProcessor
|
class ReplayGainAudioProcessor
|
||||||
private constructor(
|
@Inject
|
||||||
|
constructor(
|
||||||
private val playbackManager: PlaybackStateManager,
|
private val playbackManager: PlaybackStateManager,
|
||||||
private val playbackSettings: PlaybackSettings
|
private val playbackSettings: PlaybackSettings
|
||||||
) : BaseAudioProcessor(), PlaybackStateManager.Listener, PlaybackSettings.Listener {
|
) : 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
|
private var volume = 1f
|
||||||
set(value) {
|
set(value) {
|
||||||
field = value
|
field = value
|
||||||
|
@ -65,7 +57,7 @@ private constructor(
|
||||||
flush()
|
flush()
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
fun attach() {
|
||||||
playbackManager.addListener(this)
|
playbackManager.addListener(this)
|
||||||
playbackSettings.registerListener(this)
|
playbackSettings.registerListener(this)
|
||||||
}
|
}
|
||||||
|
|
|
@ -96,7 +96,7 @@ private constructor(
|
||||||
val notification: ForegroundServiceNotification
|
val notification: ForegroundServiceNotification
|
||||||
get() = _notification
|
get() = _notification
|
||||||
|
|
||||||
init {
|
fun attach() {
|
||||||
playbackManager.addListener(this)
|
playbackManager.addListener(this)
|
||||||
playbackSettings.registerListener(this)
|
playbackSettings.registerListener(this)
|
||||||
imageSettings.registerListener(this)
|
imageSettings.registerListener(this)
|
||||||
|
|
|
@ -25,6 +25,7 @@ import kotlinx.coroutines.Job
|
||||||
import org.oxycblt.auxio.ForegroundListener
|
import org.oxycblt.auxio.ForegroundListener
|
||||||
import org.oxycblt.auxio.ForegroundServiceNotification
|
import org.oxycblt.auxio.ForegroundServiceNotification
|
||||||
import org.oxycblt.auxio.IntegerTable
|
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.DeferredPlayback
|
||||||
import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
|
@ -35,7 +36,7 @@ private constructor(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val foregroundListener: ForegroundListener,
|
private val foregroundListener: ForegroundListener,
|
||||||
private val playbackManager: PlaybackStateManager,
|
private val playbackManager: PlaybackStateManager,
|
||||||
exoHolderFactory: ExoPlaybackStateHolder.Factory,
|
playerHolderFactory: PlayerStateHolder.Factory,
|
||||||
sessionHolderFactory: MediaSessionHolder.Factory,
|
sessionHolderFactory: MediaSessionHolder.Factory,
|
||||||
widgetComponentFactory: WidgetComponent.Factory,
|
widgetComponentFactory: WidgetComponent.Factory,
|
||||||
systemReceiverFactory: SystemPlaybackReceiver.Factory,
|
systemReceiverFactory: SystemPlaybackReceiver.Factory,
|
||||||
|
@ -44,7 +45,7 @@ private constructor(
|
||||||
@Inject
|
@Inject
|
||||||
constructor(
|
constructor(
|
||||||
private val playbackManager: PlaybackStateManager,
|
private val playbackManager: PlaybackStateManager,
|
||||||
private val exoHolderFactory: ExoPlaybackStateHolder.Factory,
|
private val exoHolderFactory: PlayerStateHolder.Factory,
|
||||||
private val sessionHolderFactory: MediaSessionHolder.Factory,
|
private val sessionHolderFactory: MediaSessionHolder.Factory,
|
||||||
private val widgetComponentFactory: WidgetComponent.Factory,
|
private val widgetComponentFactory: WidgetComponent.Factory,
|
||||||
private val systemReceiverFactory: SystemPlaybackReceiver.Factory,
|
private val systemReceiverFactory: SystemPlaybackReceiver.Factory,
|
||||||
|
@ -61,18 +62,20 @@ private constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private val waitJob = Job()
|
private val waitJob = Job()
|
||||||
private val exoHolder = exoHolderFactory.create()
|
private val exoHolder = playerHolderFactory.create(context)
|
||||||
private val sessionHolder = sessionHolderFactory.create(context, foregroundListener)
|
private val sessionHolder = sessionHolderFactory.create(context, foregroundListener)
|
||||||
private val widgetComponent = widgetComponentFactory.create(context)
|
private val widgetComponent = widgetComponentFactory.create(context)
|
||||||
private val systemReceiver = systemReceiverFactory.create(context)
|
private val systemReceiver = systemReceiverFactory.create(context, widgetComponent)
|
||||||
|
|
||||||
val token: MediaSessionCompat.Token
|
|
||||||
get() = sessionHolder.token
|
|
||||||
|
|
||||||
// --- MEDIASESSION CALLBACKS ---
|
// --- MEDIASESSION CALLBACKS ---
|
||||||
|
|
||||||
init {
|
fun attach(): MediaSessionCompat.Token {
|
||||||
|
exoHolder.attach()
|
||||||
|
sessionHolder.attach()
|
||||||
|
widgetComponent.attach()
|
||||||
|
systemReceiver.attach()
|
||||||
playbackManager.addListener(this)
|
playbackManager.addListener(this)
|
||||||
|
return sessionHolder.token
|
||||||
}
|
}
|
||||||
|
|
||||||
fun handleTaskRemoved() {
|
fun handleTaskRemoved() {
|
||||||
|
|
|
@ -37,6 +37,7 @@ import org.oxycblt.auxio.widgets.WidgetProvider
|
||||||
*/
|
*/
|
||||||
class SystemPlaybackReceiver
|
class SystemPlaybackReceiver
|
||||||
private constructor(
|
private constructor(
|
||||||
|
private val context: Context,
|
||||||
private val playbackManager: PlaybackStateManager,
|
private val playbackManager: PlaybackStateManager,
|
||||||
private val playbackSettings: PlaybackSettings,
|
private val playbackSettings: PlaybackSettings,
|
||||||
private val widgetComponent: WidgetComponent
|
private val widgetComponent: WidgetComponent
|
||||||
|
@ -47,16 +48,19 @@ private constructor(
|
||||||
@Inject
|
@Inject
|
||||||
constructor(
|
constructor(
|
||||||
private val playbackManager: PlaybackStateManager,
|
private val playbackManager: PlaybackStateManager,
|
||||||
private val playbackSettings: PlaybackSettings,
|
private val playbackSettings: PlaybackSettings
|
||||||
private val widgetComponent: WidgetComponent
|
|
||||||
) {
|
) {
|
||||||
fun create(context: Context): SystemPlaybackReceiver {
|
fun create(context: Context, widgetComponent: WidgetComponent) =
|
||||||
val receiver =
|
SystemPlaybackReceiver(context, playbackManager, playbackSettings, widgetComponent)
|
||||||
SystemPlaybackReceiver(playbackManager, playbackSettings, widgetComponent)
|
}
|
||||||
ContextCompat.registerReceiver(
|
|
||||||
context, receiver, INTENT_FILTER, ContextCompat.RECEIVER_EXPORTED)
|
fun attach() {
|
||||||
return receiver
|
ContextCompat.registerReceiver(
|
||||||
}
|
context, this, INTENT_FILTER, ContextCompat.RECEIVER_EXPORTED)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun release() {
|
||||||
|
context.unregisterReceiver(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
|
|
@ -67,7 +67,7 @@ private constructor(
|
||||||
|
|
||||||
private val widgetProvider = WidgetProvider()
|
private val widgetProvider = WidgetProvider()
|
||||||
|
|
||||||
init {
|
fun attach() {
|
||||||
playbackManager.addListener(this)
|
playbackManager.addListener(this)
|
||||||
uiSettings.registerListener(this)
|
uiSettings.registerListener(this)
|
||||||
imageSettings.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