Compare commits

...

24 commits

Author SHA1 Message Date
Alexander Capehart
d394b76908 playback: fix more build errors 2024-09-24 18:55:24 -06:00
Alexander Capehart
9ef3c41bf8 playback: fix di error 2024-09-24 18:48:16 -06:00
Alexander Capehart
a7aae6a11e playback: connect listener to gapless queuer 2024-09-24 18:46:28 -06:00
Alexander Capehart
4d28fe51b5 playback: save on playing change again 2024-09-24 18:45:23 -06:00
Alexander Capehart
2c87aa5830 playback: reformat 2024-09-24 18:42:39 -06:00
Alexander Capehart
f245e33887 playback: restructure repeat mode/listeners 2024-09-24 18:40:18 -06:00
Alexander Capehart
b784250fed playback: move player dep module 2024-09-23 15:18:53 -06:00
Alexander Capehart
5d1111b12a playback: move player into module 2024-09-23 15:15:06 -06:00
Alexander Capehart
e32c687c61 playback: extract gapless playback impl
I need to make a setting to switch between gapless and single-item playback
to accomodate extremely large queues, so extract the crazy hacky queue
stuff into a new PlayerKernel construct.

Single-item will be added at a later point.
2024-09-23 11:46:54 -06:00
Alexander Capehart
34f7bc4886 all: reformat 2024-09-19 17:49:41 -06:00
Alexander Capehart
acd81d1c57 music: introduce icon for backport more tab 2024-09-19 17:49:07 -06:00
Alexander Capehart
1f5b202c5a service: avoid crash on death 2024-09-19 17:22:48 -06:00
Alexander Capehart
0ef2dafc29 service: decouple maxtab handling and ids
Simpler and more versatile.
2024-09-19 17:22:34 -06:00
Alexander Capehart
66fad791d5 music: simplify disc number resolution
Introduce a resolveDisc extension function to share disc name
resolution between detail/browser
2024-09-18 16:00:36 -06:00
Alexander Capehart
01bebfe63d home: dont show tab icons in phone mode 2024-09-18 15:56:40 -06:00
Alexander Capehart
c108ec7e12 home: fix broken item refresh 2024-09-18 15:56:22 -06:00
Alexander Capehart
e2b4f215cb detail: fix broken item refresh 2024-09-18 15:56:11 -06:00
Alexander Capehart
c7e18cdc6a detail: generate sort header w/discs 2024-09-18 15:32:27 -06:00
Alexander Capehart
8e6b49c8ec ui: attach to generators 2024-09-18 15:31:39 -06:00
Alexander Capehart
4accfaafaf detail: fix incorrect disc section generation 2024-09-18 15:31:17 -06:00
Alexander Capehart
4917330633 service: re-add attach pattern
Turns out I can't actually couple creation/attach without creating a
huge amount of variable issues.
2024-09-18 14:50:56 -06:00
Alexander Capehart
09588b3f38 all: reformat 2024-09-18 14:50:56 -06:00
Alexander Capehart
af812bc840 music: tear down menus
Only works on automotive OS, which I am not targeting right now.
2024-09-18 14:50:56 -06:00
Alexander Capehart
556ac243f0 all: reformat/fixes 2024-09-18 14:50:56 -06:00
31 changed files with 844 additions and 530 deletions

View file

@ -49,8 +49,9 @@ class AuxioService :
override fun onCreate() {
super.onCreate()
playbackFragment = playbackFragmentFactory.create(this, this)
sessionToken = playbackFragment.token
sessionToken = playbackFragment.attach()
musicFragment = musicFragmentFactory.create(this, this, this)
musicFragment.attach()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
@ -80,7 +81,6 @@ class AuxioService :
super.onDestroy()
musicFragment.release()
playbackFragment.release()
sessionToken = null
}
override fun onGetRoot(
@ -88,9 +88,7 @@ class AuxioService :
clientUid: Int,
rootHints: Bundle?
): BrowserRoot {
val maximumRootChildLimit =
rootHints?.getInt(MediaConstants.BROWSER_ROOT_HINTS_KEY_ROOT_CHILDREN_LIMIT, 4) ?: 4
return musicFragment.getRoot(maximumRootChildLimit)
return musicFragment.getRoot()
}
override fun onLoadItem(itemId: String, result: Result<MediaItem>) {
@ -98,7 +96,10 @@ class AuxioService :
}
override fun onLoadChildren(parentId: String, result: Result<MutableList<MediaItem>>) {
musicFragment.getChildren(parentId, result)
val maximumRootChildLimit =
browserRootHints?.getInt(MediaConstants.BROWSER_ROOT_HINTS_KEY_ROOT_CHILDREN_LIMIT, 4)
?: 4
musicFragment.getChildren(parentId, maximumRootChildLimit, result)
}
override fun onSearch(query: String, extras: Bundle?, result: Result<MutableList<MediaItem>>) {

View file

@ -1,9 +1,26 @@
/*
* Copyright (c) 2024 Auxio Project
* DetailGenerator.kt is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.detail
import android.content.Context
import androidx.annotation.StringRes
import javax.inject.Inject
import org.oxycblt.auxio.R
import org.oxycblt.auxio.detail.list.DiscHeader
import org.oxycblt.auxio.list.ListSettings
import org.oxycblt.auxio.list.sort.Sort
import org.oxycblt.auxio.music.Album
@ -18,15 +35,20 @@ import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.info.Disc
import org.oxycblt.auxio.music.info.ReleaseType
import org.oxycblt.auxio.util.logD
import java.util.SortedMap
import javax.inject.Inject
interface DetailGenerator {
fun any(uid: Music.UID): Detail<out MusicParent>?
fun album(uid: Music.UID): Detail<Album>?
fun artist(uid: Music.UID): Detail<Artist>?
fun genre(uid: Music.UID): Detail<Genre>?
fun playlist(uid: Music.UID): Detail<Playlist>?
fun attach()
fun release()
interface Factory {
@ -38,10 +60,10 @@ interface DetailGenerator {
}
}
class DetailGeneratorFactoryImpl @Inject constructor(
private val listSettings: ListSettings,
private val musicRepository: MusicRepository
) : DetailGenerator.Factory {
class DetailGeneratorFactoryImpl
@Inject
constructor(private val listSettings: ListSettings, private val musicRepository: MusicRepository) :
DetailGenerator.Factory {
override fun create(invalidator: DetailGenerator.Invalidator): DetailGenerator =
DetailGeneratorImpl(invalidator, listSettings, musicRepository)
}
@ -51,7 +73,7 @@ private class DetailGeneratorImpl(
private val listSettings: ListSettings,
private val musicRepository: MusicRepository
) : DetailGenerator, MusicRepository.UpdateListener, ListSettings.Listener {
init {
override fun attach() {
listSettings.registerListener(this)
musicRepository.addUpdateListener(this)
}
@ -102,11 +124,12 @@ private class DetailGeneratorImpl(
val album = musicRepository.deviceLibrary?.findAlbum(uid) ?: return null
val songs = listSettings.albumSongSort.songs(album.songs)
val discs = songs.groupBy { it.disc }
val section = if (discs.size > 1 || discs.keys.first() != null) {
DetailSection.Discs(discs)
} else {
DetailSection.Songs(songs)
}
val section =
if (discs.size > 1) {
DetailSection.Discs(discs)
} else {
DetailSection.Songs(songs)
}
return Detail(album, listOf(section))
}
@ -138,13 +161,14 @@ private class DetailGeneratorImpl(
// implicit album list into the mapping.
logD("Implicit albums present, adding to list")
@Suppress("UNCHECKED_CAST")
(grouping as MutableMap<DetailSection.Albums.Category, Collection<Album>>)[DetailSection.Albums.Category.APPEARANCES] =
artist.implicitAlbums
(grouping as MutableMap<DetailSection.Albums.Category, Collection<Album>>)[
DetailSection.Albums.Category.APPEARANCES] = artist.implicitAlbums
}
val sections = grouping.mapTo(mutableListOf<DetailSection>()) { (category, albums) ->
DetailSection.Albums(category, ARTIST_ALBUM_SORT.albums(albums))
}
val sections =
grouping.mapTo(mutableListOf<DetailSection>()) { (category, albums) ->
DetailSection.Albums(category, ARTIST_ALBUM_SORT.albums(albums))
}
val songs = DetailSection.Songs(listSettings.artistSongSort.songs(artist.songs))
sections.add(songs)
return Detail(artist, sections)
@ -184,7 +208,8 @@ sealed interface DetailSection {
override val stringRes = R.string.lbl_songs
}
data class Albums(val category: Category, override val items: List<Album>) : PlainSection<Album>() {
data class Albums(val category: Category, override val items: List<Album>) :
PlainSection<Album>() {
override val order = 1 + category.ordinal
override val stringRes = category.stringRes
@ -203,7 +228,6 @@ sealed interface DetailSection {
}
}
data class Songs(override val items: List<Song>) : PlainSection<Song>() {
override val order = 12
override val stringRes = R.string.lbl_songs

View file

@ -1,6 +1,6 @@
/*
* Copyright (c) 2024 Auxio Project
* HomeModule.kt is part of Auxio.
* DetailModule.kt is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by

View file

@ -72,9 +72,6 @@ constructor(
private val playbackSettings: PlaybackSettings,
detailGeneratorFactory: DetailGenerator.Factory
) : ViewModel(), DetailGenerator.Invalidator {
private val detailGenerator = detailGeneratorFactory.create(this)
private val _toShow = MutableEvent<Show>()
/**
* A [Show] command that is awaiting a view capable of responding to it. Null if none currently.
@ -198,6 +195,12 @@ constructor(
playbackSettings.inParentPlaybackMode
?: PlaySong.FromPlaylist(unlikelyToBeNull(currentPlaylist.value))
private val detailGenerator = detailGeneratorFactory.create(this)
init {
detailGenerator.attach()
}
override fun onCleared() {
detailGenerator.release()
}
@ -208,21 +211,18 @@ constructor(
val album = detailGenerator.album(currentAlbum.value?.uid ?: return)
refreshDetail(album, _currentAlbum, _albumSongList, _albumSongInstructions, replace)
}
MusicType.ARTISTS -> {
val artist = detailGenerator.artist(currentArtist.value?.uid ?: return)
refreshDetail(artist, _currentArtist, _artistSongList, _artistSongInstructions, replace)
refreshDetail(
artist, _currentArtist, _artistSongList, _artistSongInstructions, replace)
}
MusicType.GENRES -> {
val genre = detailGenerator.genre(currentGenre.value?.uid ?: return)
refreshDetail(genre, _currentGenre, _genreSongList, _genreSongInstructions, replace)
}
MusicType.PLAYLISTS -> {
refreshPlaylist(currentPlaylist.value?.uid ?: return)
}
else -> error("Unexpected music type $type")
}
}
@ -522,7 +522,6 @@ constructor(
}
}
private fun <T : MusicParent> refreshDetail(
detail: Detail<T>?,
parent: MutableStateFlow<T?>,
@ -537,24 +536,23 @@ constructor(
val newList = mutableListOf<Item>()
var newInstructions: UpdateInstructions = UpdateInstructions.Diff
for ((i, section) in detail.sections.withIndex()) {
val items = when (section) {
is DetailSection.PlainSection<*> -> {
val header = if (section is DetailSection.Songs)
SortHeader(section.stringRes) else BasicHeader(section.stringRes)
newList.add(Divider(header))
newList.add(header)
section.items
}
is DetailSection.Discs -> {
val header = BasicHeader(section.stringRes)
newList.add(Divider(header))
newList.add(header)
section.discs.flatMap {
listOf(DiscHeader(it.key)) + it.value
val items =
when (section) {
is DetailSection.PlainSection<*> -> {
val header =
if (section is DetailSection.Songs) SortHeader(section.stringRes)
else BasicHeader(section.stringRes)
newList.add(Divider(header))
newList.add(header)
section.items
}
is DetailSection.Discs -> {
val header = SortHeader(section.stringRes)
newList.add(Divider(header))
newList.add(header)
section.discs.flatMap { listOf(DiscHeader(it.key)) + it.value }
}
}
}
// Currently only the final section (songs, which can be sorted) are invalidatable
// and thus need to be replaced.
if (replace == -1 && i == detail.sections.lastIndex) {
@ -564,16 +562,20 @@ constructor(
newList.addAll(items)
}
parent.value = detail.parent
list.value = newList
instructions.put(newInstructions)
list.value = newList
}
private fun refreshPlaylist(uid: Music.UID, instructions: UpdateInstructions = UpdateInstructions.Diff) {
private fun refreshPlaylist(
uid: Music.UID,
instructions: UpdateInstructions = UpdateInstructions.Diff
) {
logD("Refreshing playlist list")
val edited = editedPlaylist.value
if (edited == null) {
val playlist = detailGenerator.playlist(uid)
refreshDetail(playlist, _currentPlaylist, _playlistSongList, _playlistSongInstructions, null)
refreshDetail(
playlist, _currentPlaylist, _playlistSongList, _playlistSongInstructions, null)
return
}
val list = mutableListOf<Item>()
@ -583,8 +585,8 @@ constructor(
list.add(header)
list.addAll(edited)
}
_playlistSongList.value = list
_playlistSongInstructions.put(instructions)
_playlistSongList.value = list
}
}

View file

@ -35,10 +35,10 @@ import org.oxycblt.auxio.list.adapter.SimpleDiffCallback
import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.info.Disc
import org.oxycblt.auxio.music.info.resolveNumber
import org.oxycblt.auxio.playback.formatDurationMs
import org.oxycblt.auxio.util.context
import org.oxycblt.auxio.util.inflater
import org.oxycblt.auxio.util.logD
/**
* An [DetailListAdapter] implementing the header and sub-items for the [Album] detail view.
@ -111,16 +111,10 @@ private class DiscHeaderViewHolder(private val binding: ItemDiscHeaderBinding) :
*/
fun bind(discHeader: DiscHeader) {
val disc = discHeader.inner
if (disc != null) {
binding.discNumber.text = binding.context.getString(R.string.fmt_disc_no, disc.number)
binding.discName.apply {
text = disc.name
isGone = disc.name == null
}
} else {
logD("Disc is null, defaulting to no disc")
binding.discNumber.text = binding.context.getString(R.string.def_disc)
binding.discName.isGone = true
binding.discNumber.text = disc.resolveNumber(binding.context)
binding.discName.apply {
text = disc?.name
isGone = disc?.name == null
}
}

View file

@ -32,6 +32,10 @@ import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.util.logD
interface HomeGenerator {
fun attach()
fun release()
fun songs(): List<Song>
fun albums(): List<Album>
@ -44,8 +48,6 @@ interface HomeGenerator {
fun tabs(): List<MusicType>
fun release()
interface Invalidator {
fun invalidateMusic(type: MusicType, instructions: UpdateInstructions)
@ -74,41 +76,14 @@ private class HomeGeneratorImpl(
private val listSettings: ListSettings,
private val musicRepository: MusicRepository,
) : HomeGenerator, HomeSettings.Listener, ListSettings.Listener, MusicRepository.UpdateListener {
override fun songs() =
musicRepository.deviceLibrary?.let { listSettings.songSort.songs(it.songs) } ?: emptyList()
override fun albums() =
musicRepository.deviceLibrary?.let { listSettings.albumSort.albums(it.albums) }
?: emptyList()
override fun artists() =
musicRepository.deviceLibrary?.let { listSettings.artistSort.artists(it.artists) }
?: emptyList()
override fun genres() =
musicRepository.deviceLibrary?.let { listSettings.genreSort.genres(it.genres) }
?: emptyList()
override fun playlists() =
musicRepository.userLibrary?.let { listSettings.playlistSort.playlists(it.playlists) }
?: emptyList()
override fun tabs() = homeSettings.homeTabs.filterIsInstance<Tab.Visible>().map { it.type }
override fun onTabsChanged() {
invalidator.invalidateTabs()
}
init {
override fun attach() {
homeSettings.registerListener(this)
listSettings.registerListener(this)
musicRepository.addUpdateListener(this)
}
override fun release() {
musicRepository.removeUpdateListener(this)
listSettings.unregisterListener(this)
homeSettings.unregisterListener(this)
override fun onTabsChanged() {
invalidator.invalidateTabs()
}
override fun onHideCollaboratorsChanged() {
@ -161,4 +136,31 @@ private class HomeGeneratorImpl(
invalidator.invalidateMusic(MusicType.PLAYLISTS, UpdateInstructions.Diff)
}
}
override fun release() {
musicRepository.removeUpdateListener(this)
listSettings.unregisterListener(this)
homeSettings.unregisterListener(this)
}
override fun songs() =
musicRepository.deviceLibrary?.let { listSettings.songSort.songs(it.songs) } ?: emptyList()
override fun albums() =
musicRepository.deviceLibrary?.let { listSettings.albumSort.albums(it.albums) }
?: emptyList()
override fun artists() =
musicRepository.deviceLibrary?.let { listSettings.artistSort.artists(it.artists) }
?: emptyList()
override fun genres() =
musicRepository.deviceLibrary?.let { listSettings.genreSort.genres(it.genres) }
?: emptyList()
override fun playlists() =
musicRepository.userLibrary?.let { listSettings.playlistSort.playlists(it.playlists) }
?: emptyList()
override fun tabs() = homeSettings.homeTabs.filterIsInstance<Tab.Visible>().map { it.type }
}

View file

@ -52,8 +52,6 @@ constructor(
private val playbackSettings: PlaybackSettings,
homeGeneratorFactory: HomeGenerator.Factory
) : ViewModel(), HomeGenerator.Invalidator {
private val homeGenerator = homeGeneratorFactory.create(this)
private val _songList = MutableStateFlow(listOf<Song>())
/** A list of [Song]s, sorted by the preferred [Sort], to be shown in the home view. */
val songList: StateFlow<List<Song>>
@ -131,6 +129,8 @@ constructor(
val playlistSort: Sort
get() = listSettings.playlistSort
private val homeGenerator = homeGeneratorFactory.create(this)
/**
* A list of [MusicType] corresponding to the current [Tab] configuration, excluding invisible
* [Tab]s.
@ -163,6 +163,10 @@ constructor(
val showOuter: Event<Outer>
get() = _showOuter
init {
homeGenerator.attach()
}
override fun onCleared() {
super.onCleared()
homeGenerator.release()
@ -171,24 +175,24 @@ constructor(
override fun invalidateMusic(type: MusicType, instructions: UpdateInstructions) {
when (type) {
MusicType.SONGS -> {
_songList.value = homeGenerator.songs()
_songInstructions.put(instructions)
_songList.value = homeGenerator.songs()
}
MusicType.ALBUMS -> {
_albumList.value = homeGenerator.albums()
_albumInstructions.put(instructions)
_albumList.value = homeGenerator.albums()
}
MusicType.ARTISTS -> {
_artistList.value = homeGenerator.artists()
_artistInstructions.put(instructions)
_artistList.value = homeGenerator.artists()
}
MusicType.GENRES -> {
_genreList.value = homeGenerator.genres()
_genreInstructions.put(instructions)
_genreList.value = homeGenerator.genres()
}
MusicType.PLAYLISTS -> {
_playlistList.value = homeGenerator.playlists()
_playlistInstructions.put(instructions)
_playlistList.value = homeGenerator.playlists()
}
}
}

View file

@ -52,7 +52,7 @@ class AdaptiveTabStrategy(context: Context, private val tabs: List<MusicType>) :
// On small screens, only display an icon.
width < 370 -> tab.setIcon(icon).setContentDescription(homeTab.nameRes)
// On large screens, display an icon and text.
width < 600 -> tab.setText(homeTab.nameRes).setIcon(icon)
width < 600 -> tab.setText(homeTab.nameRes)
// On medium-size screens, display text.
else -> tab.setIcon(icon).setText(homeTab.nameRes)
}

View file

@ -46,12 +46,19 @@ interface ListSettings : Settings<ListSettings.Listener> {
interface Listener {
fun onSongSortChanged() {}
fun onAlbumSortChanged() {}
fun onAlbumSongSortChanged() {}
fun onArtistSortChanged() {}
fun onArtistSongSortChanged() {}
fun onGenreSortChanged() {}
fun onGenreSongSortChanged() {}
fun onPlaylistSortChanged() {}
}
}

View file

@ -18,6 +18,8 @@
package org.oxycblt.auxio.music.info
import android.content.Context
import org.oxycblt.auxio.R
import org.oxycblt.auxio.list.Item
/**
@ -34,3 +36,7 @@ class Disc(val number: Int, val name: String?) : Item, Comparable<Disc> {
override fun compareTo(other: Disc) = number.compareTo(other.number)
}
fun Disc?.resolveNumber(context: Context) =
this?.run { context.getString(R.string.fmt_disc_no, number) }
?: context.getString(R.string.def_disc)

View file

@ -80,7 +80,7 @@ private constructor(
.newWakeLock(
PowerManager.PARTIAL_WAKE_LOCK, BuildConfig.APPLICATION_ID + ":IndexingComponent")
init {
fun attach() {
musicSettings.registerListener(this)
musicRepository.addUpdateListener(this)
musicRepository.addIndexingListener(this)

View file

@ -18,17 +18,12 @@
package org.oxycblt.auxio.music.service
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.BitmapFactory
import android.os.Bundle
import android.support.v4.media.MediaBrowserCompat.MediaItem
import android.support.v4.media.MediaDescriptionCompat
import android.view.MenuInflater
import androidx.annotation.MenuRes
import androidx.annotation.StringRes
import androidx.appcompat.view.menu.MenuBuilder
import androidx.core.view.children
import androidx.media.utils.MediaConstants
import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.R
@ -83,46 +78,6 @@ sealed interface MediaSessionUID {
}
}
enum class BrowserOption(val actionId: String, val labelRes: Int, val iconRes: Int) {
PLAY(BuildConfig.APPLICATION_ID + ".menu.PLAY", R.string.lbl_play, R.drawable.ic_play_24),
SHUFFLE(
BuildConfig.APPLICATION_ID + ".menu.SHUFFLE",
R.string.lbl_shuffle,
R.drawable.ic_shuffle_off_24),
PLAY_NEXT(
BuildConfig.APPLICATION_ID + ".menu.PLAY_NEXT",
R.string.lbl_play_next,
R.drawable.ic_play_next_24),
ADD_TO_QUEUE(
BuildConfig.APPLICATION_ID + ".menu.ADD_TO_QUEUE",
R.string.lbl_queue_add,
R.drawable.ic_queue_add_24),
DETAILS(
BuildConfig.APPLICATION_ID + ".menu.DETAILS",
R.string.lbl_parent_detail,
R.drawable.ic_details_24),
ALBUM_DETAILS(
BuildConfig.APPLICATION_ID + ".menu.ALBUM_DETAILS",
R.string.lbl_album_details,
R.drawable.ic_album_24),
ARTIST_DETAILS(
BuildConfig.APPLICATION_ID + ".menu.ARTIST_DETAILS",
R.string.lbl_artist_details,
R.drawable.ic_artist_24);
companion object {
val ITEM_ID_MAP =
mapOf(
R.id.action_play to PLAY,
R.id.action_shuffle to SHUFFLE,
R.id.action_play_next to PLAY_NEXT,
R.id.action_queue_add to ADD_TO_QUEUE,
R.id.action_detail to DETAILS,
R.id.action_album_details to ALBUM_DETAILS,
R.id.action_artist_details to ARTIST_DETAILS)
}
}
typealias Sugar = Bundle.(Context) -> Unit
fun header(@StringRes nameRes: Int): Sugar = {
@ -138,16 +93,6 @@ private fun style(style: Int): Sugar = {
putInt(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_SINGLE_ITEM, style)
}
private fun menu(@MenuRes res: Int): Sugar = { context ->
@SuppressLint("RestrictedApi") val builder = MenuBuilder(context)
MenuInflater(context).inflate(res, builder)
val menuIds =
builder.children.mapNotNullTo(ArrayList()) {
BrowserOption.ITEM_ID_MAP[it.itemId]?.actionId
}
putStringArrayList(MediaConstants.DESCRIPTION_EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ID_LIST, menuIds)
}
private fun makeExtras(context: Context, vararg sugars: Sugar): Bundle {
return Bundle().apply { sugars.forEach { this.it(context) } }
}
@ -181,7 +126,7 @@ fun Song.toMediaDescription(
} else {
MediaSessionUID.ChildItem(parent.uid, uid)
}
val extras = makeExtras(context, *sugar, menu(R.menu.song))
val extras = makeExtras(context, *sugar)
return MediaDescriptionCompat.Builder()
.setMediaId(mediaSessionUID.toString())
.setTitle(name.resolve(context))
@ -212,7 +157,7 @@ fun Album.toMediaItem(
} else {
MediaSessionUID.ChildItem(parent.uid, uid)
}
val extras = makeExtras(context, *sugar, menu(R.menu.album))
val extras = makeExtras(context, *sugar)
val counts = context.getPlural(R.plurals.fmt_song_count, songs.size)
val description =
MediaDescriptionCompat.Builder()
@ -241,7 +186,7 @@ fun Artist.toMediaItem(context: Context, vararg sugar: Sugar): MediaItem {
} else {
context.getString(R.string.def_song_count)
})
val extras = makeExtras(context, *sugar, menu(R.menu.parent))
val extras = makeExtras(context, *sugar)
val description =
MediaDescriptionCompat.Builder()
.setMediaId(mediaSessionUID.toString())
@ -262,7 +207,7 @@ fun Genre.toMediaItem(context: Context, vararg sugar: Sugar): MediaItem {
} else {
context.getString(R.string.def_song_count)
}
val extras = makeExtras(context, *sugar, menu(R.menu.parent))
val extras = makeExtras(context, *sugar)
val description =
MediaDescriptionCompat.Builder()
.setMediaId(mediaSessionUID.toString())
@ -282,7 +227,7 @@ fun Playlist.toMediaItem(context: Context, vararg sugar: Sugar): MediaItem {
} else {
context.getString(R.string.def_song_count)
}
val extras = makeExtras(context, *sugar, menu(R.menu.playlist))
val extras = makeExtras(context, *sugar)
val description =
MediaDescriptionCompat.Builder()
.setMediaId(mediaSessionUID.toString())

View file

@ -24,13 +24,8 @@ import javax.inject.Inject
import org.oxycblt.auxio.R
import org.oxycblt.auxio.detail.DetailGenerator
import org.oxycblt.auxio.detail.DetailSection
import org.oxycblt.auxio.detail.list.SortHeader
import org.oxycblt.auxio.home.HomeGenerator
import org.oxycblt.auxio.list.BasicHeader
import org.oxycblt.auxio.list.Divider
import org.oxycblt.auxio.list.ListSettings
import org.oxycblt.auxio.list.adapter.UpdateInstructions
import org.oxycblt.auxio.list.sort.Sort
import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre
@ -39,6 +34,7 @@ import org.oxycblt.auxio.music.MusicRepository
import org.oxycblt.auxio.music.MusicType
import org.oxycblt.auxio.music.Playlist
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.info.resolveNumber
import org.oxycblt.auxio.search.SearchEngine
class MusicBrowser
@ -76,6 +72,11 @@ private constructor(
private val homeGenerator = homeGeneratorFactory.create(this)
private val detailGenerator = detailGeneratorFactory.create(this)
fun attach() {
homeGenerator.attach()
detailGenerator.attach()
}
fun release() {
homeGenerator.release()
detailGenerator.release()
@ -87,24 +88,22 @@ private constructor(
}
override fun invalidateTabs() {
for (i in 0..10) {
// TODO: Temporary bodge, move the amount parameter to a bundle extra
val rootId = MediaSessionUID.Tab(TabNode.Root(i)).toString()
val moreId = MediaSessionUID.Tab(TabNode.More(i)).toString()
invalidator.invalidateMusic(setOf(rootId, moreId))
}
val rootId = MediaSessionUID.Tab(TabNode.Root).toString()
val moreId = MediaSessionUID.Tab(TabNode.More).toString()
invalidator.invalidateMusic(setOf(rootId, moreId))
}
override fun invalidate(type: MusicType, replace: Int?) {
val deviceLibrary = musicRepository.deviceLibrary ?: return
val userLibrary = musicRepository.userLibrary ?: return
val music = when (type) {
MusicType.ALBUMS -> deviceLibrary.albums
MusicType.ARTISTS -> deviceLibrary.artists
MusicType.GENRES -> deviceLibrary.genres
MusicType.PLAYLISTS -> userLibrary.playlists
else -> return
}
val music =
when (type) {
MusicType.ALBUMS -> deviceLibrary.albums
MusicType.ARTISTS -> deviceLibrary.artists
MusicType.GENRES -> deviceLibrary.genres
MusicType.PLAYLISTS -> userLibrary.playlists
else -> return
}
if (music.isEmpty()) {
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 userLibrary = musicRepository.userLibrary
if (deviceLibrary == null || userLibrary == null) {
return listOf()
}
return getMediaItemList(parentId)
return getMediaItemList(parentId, maxTabs)
}
suspend fun search(query: String): MutableList<MediaItem> {
@ -179,10 +177,10 @@ private constructor(
return music
}
private fun getMediaItemList(id: String): List<MediaItem>? {
private fun getMediaItemList(id: String, maxTabs: Int): List<MediaItem>? {
return when (val mediaSessionUID = MediaSessionUID.fromString(id)) {
is MediaSessionUID.Tab -> {
getCategoryMediaItems(mediaSessionUID.node)
getCategoryMediaItems(mediaSessionUID.node, maxTabs)
}
is MediaSessionUID.SingleItem -> {
getChildMediaItems(mediaSessionUID.uid)
@ -196,22 +194,22 @@ private constructor(
}
}
private fun getCategoryMediaItems(node: TabNode) =
private fun getCategoryMediaItems(node: TabNode, maxTabs: Int) =
when (node) {
is TabNode.Root -> {
val tabs = homeGenerator.tabs()
val base = tabs.take(node.amount - 1).map { TabNode.Home(it) }
val base = tabs.take(maxTabs - 1).map { TabNode.Home(it) }
if (base.size < tabs.size) {
base + TabNode.More(tabs.size - base.size)
base + TabNode.More
} else {
base
}
.map { it.toMediaItem(context) }
}
is TabNode.More ->
homeGenerator.tabs().takeLast(node.remainder).map {
TabNode.Home(it).toMediaItem(context)
}
is TabNode.More -> {
val tabs = homeGenerator.tabs()
tabs.takeLast(tabs.size - maxTabs).map { TabNode.Home(it).toMediaItem(context) }
}
is TabNode.Home ->
when (node.type) {
MusicType.SONGS -> homeGenerator.songs().map { it.toMediaItem(context, null) }
@ -226,20 +224,17 @@ private constructor(
val detail = detailGenerator.any(uid) ?: return null
return detail.sections.flatMap { section ->
when (section) {
is DetailSection.Songs -> section.items.map { it.toMediaItem(context, null, header(section.stringRes)) }
is DetailSection.Albums -> section.items.map { it.toMediaItem(context, null, header(section.stringRes)) }
is DetailSection.Artists -> section.items.map { it.toMediaItem(context, header(section.stringRes)) }
is DetailSection.Discs -> section.discs.flatMap {
section.discs.flatMap { entry ->
val disc = entry.key
val discString = if (disc != null) {
context.getString(R.string.fmt_disc_no, disc.number)
} else {
context.getString(R.string.def_disc)
}
entry.value.map { it.toMediaItem(context, null, header(discString)) }
is DetailSection.Songs ->
section.items.map { it.toMediaItem(context, null, header(section.stringRes)) }
is DetailSection.Albums ->
section.items.map { it.toMediaItem(context, null, header(section.stringRes)) }
is DetailSection.Artists ->
section.items.map { it.toMediaItem(context, header(section.stringRes)) }
is DetailSection.Discs ->
section.discs.flatMap { (disc, songs) ->
val discString = disc.resolveNumber(context)
songs.map { it.toMediaItem(context, null, header(discString)) }
}
}
else -> error("Unknown section type: $section")
}
}

View file

@ -23,7 +23,6 @@ import android.os.Bundle
import android.support.v4.media.MediaBrowserCompat.MediaItem
import androidx.media.MediaBrowserServiceCompat.BrowserRoot
import androidx.media.MediaBrowserServiceCompat.Result
import androidx.media.utils.MediaConstants
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@ -75,6 +74,11 @@ constructor(
fun invalidateMusic(mediaId: String)
}
fun attach() {
indexer.attach()
musicBrowser.attach()
}
fun release() {
dispatchJob.cancel()
musicBrowser.release()
@ -95,30 +99,17 @@ constructor(
indexer.createNotification(post)
}
fun getRoot(maxItems: Int) =
BrowserRoot(
MediaSessionUID.Tab(TabNode.Root(maxItems)).toString(),
Bundle().apply {
val actions =
BrowserOption.entries.mapTo(ArrayList()) {
Bundle().apply {
putString(
MediaConstants.EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ID, it.actionId)
putString(
MediaConstants.EXTRAS_KEY_CUSTOM_BROWSER_ACTION_LABEL,
context.getString(it.labelRes))
}
}
putParcelableArrayList(
MediaConstants.BROWSER_SERVICE_EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ROOT_LIST,
actions)
})
fun getRoot() = BrowserRoot(MediaSessionUID.Tab(TabNode.Root).toString(), Bundle())
fun getItem(mediaId: String, result: Result<MediaItem>) =
result.dispatch { musicBrowser.getItem(mediaId) }
result.dispatch {
musicBrowser.getItem(
mediaId,
)
}
fun getChildren(mediaId: String, result: Result<MutableList<MediaItem>>) =
result.dispatch { musicBrowser.getChildren(mediaId)?.toMutableList() }
fun getChildren(mediaId: String, maxTabs: Int, result: Result<MutableList<MediaItem>>) =
result.dispatch { musicBrowser.getChildren(mediaId, maxTabs)?.toMutableList() }
fun search(query: String, result: Result<MutableList<MediaItem>>) =
result.dispatchAsync { musicBrowser.search(query) }

View file

@ -23,37 +23,27 @@ import org.oxycblt.auxio.music.MusicType
sealed class TabNode {
abstract val id: String
abstract val data: Int
abstract val nameRes: Int
abstract val bitmapRes: Int?
override fun toString() = "${id}/${data}"
override fun toString() = id
data class Root(val amount: Int) : TabNode() {
override val id = ID
override val data = amount
data object Root : TabNode() {
override val id = "root"
override val nameRes = R.string.info_app_name
override val bitmapRes = null
companion object {
const val ID = "root"
}
override fun toString() = id
}
data class More(val remainder: Int) : TabNode() {
override val id = ID
override val data = remainder
data object More : TabNode() {
override val id = "more"
override val nameRes = R.string.lbl_more
override val bitmapRes = null
companion object {
const val ID = "more"
}
override val bitmapRes = R.drawable.ic_more_bitmap_24
}
data class Home(val type: MusicType) : TabNode() {
override val id = ID
override val data = type.intCode
override val id = "$ID/${type.intCode}"
override val bitmapRes: Int
get() =
when (type) {
@ -73,15 +63,15 @@ sealed class TabNode {
companion object {
fun fromString(str: String): TabNode? {
val split = str.split("/", limit = 2)
if (split.size != 2) {
return null
}
val data = split[1].toIntOrNull() ?: return null
return when (split[0]) {
Root.ID -> Root(data)
More.ID -> More(data)
Home.ID -> Home(MusicType.fromIntCode(data) ?: return null)
return when {
str == Root.id -> Root
str == More.id -> More
str.startsWith(Home.ID) -> {
val split = str.split("/")
if (split.size != 2) return null
val intCode = split[1].toIntOrNull() ?: return null
Home(MusicType.fromIntCode(intCode) ?: return null)
}
else -> null
}
}

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.playback.service
package org.oxycblt.auxio.playback.player
import androidx.media3.common.C
import androidx.media3.exoplayer.source.ShuffleOrder

View file

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

View file

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

View file

@ -1,6 +1,6 @@
/*
* Copyright (c) 2023 Auxio Project
* SystemModule.kt is part of Auxio.
* PlayerModule.kt is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.playback.service
package org.oxycblt.auxio.playback.player
import android.content.Context
import androidx.media3.datasource.ContentDataSource
@ -32,6 +32,7 @@ import androidx.media3.extractor.mp4.Mp4Extractor
import androidx.media3.extractor.ogg.OggExtractor
import androidx.media3.extractor.ts.AdtsExtractor
import androidx.media3.extractor.wav.WavExtractor
import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -40,7 +41,13 @@ import dagger.hilt.components.SingletonComponent
@Module
@InstallIn(SingletonComponent::class)
class SystemModule {
interface PlayerModule {
@Binds fun playerKernelFactory(factory: PlayerKernelFactoryImpl): PlayerKernel.Factory
}
@Module
@InstallIn(SingletonComponent::class)
class ExoPlayerModule {
@Provides
fun mediaSourceFactory(
dataSourceFactory: DataSource.Factory,

View file

@ -1,6 +1,6 @@
/*
* Copyright (c) 2024 Auxio Project
* ExoPlaybackStateHolder.kt is part of Auxio.
* PlayerStateHolder.kt is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -16,24 +16,14 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.playback.service
package org.oxycblt.auxio.playback.player
import android.content.Context
import android.content.Intent
import android.media.audiofx.AudioEffect
import androidx.media3.common.AudioAttributes
import androidx.media3.common.C
import androidx.media3.common.MediaItem
import androidx.media3.common.PlaybackException
import androidx.media3.common.Player
import androidx.media3.decoder.ffmpeg.FfmpegAudioRenderer
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.RenderersFactory
import androidx.media3.exoplayer.audio.AudioCapabilities
import androidx.media3.exoplayer.audio.MediaCodecAudioRenderer
import androidx.media3.exoplayer.mediacodec.MediaCodecSelector
import androidx.media3.exoplayer.source.MediaSource
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@ -61,9 +51,10 @@ import org.oxycblt.auxio.playback.state.StateAck
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logE
class ExoPlaybackStateHolder(
class PlayerStateHolder(
private val context: Context,
private val player: ExoPlayer,
playerKernelFactory: PlayerKernel.Factory,
gaplessQueuerFactory: GaplessQueuer.Factory,
private val playbackManager: PlaybackStateManager,
private val persistenceRepository: PersistenceRepository,
private val playbackSettings: PlaybackSettings,
@ -73,30 +64,58 @@ class ExoPlaybackStateHolder(
private val imageSettings: ImageSettings
) :
PlaybackStateHolder,
Player.Listener,
PlayerKernel.Listener,
Queuer.Listener,
MusicRepository.UpdateListener,
PlaybackSettings.Listener,
ImageSettings.Listener {
class Factory
@Inject
constructor(
private val playbackManager: PlaybackStateManager,
private val persistenceRepository: PersistenceRepository,
private val playbackSettings: PlaybackSettings,
private val playerFactory: PlayerKernel.Factory,
private val gaplessQueuerFactory: GaplessQueuer.Factory,
private val commandFactory: PlaybackCommand.Factory,
private val replayGainProcessor: ReplayGainAudioProcessor,
private val musicRepository: MusicRepository,
private val imageSettings: ImageSettings,
) {
fun create(context: Context): PlayerStateHolder {
return PlayerStateHolder(
context,
playerFactory,
gaplessQueuerFactory,
playbackManager,
persistenceRepository,
playbackSettings,
commandFactory,
replayGainProcessor,
musicRepository,
imageSettings)
}
}
private val saveJob = Job()
private val saveScope = CoroutineScope(Dispatchers.IO + saveJob)
private val restoreScope = CoroutineScope(Dispatchers.IO + saveJob)
private var currentSaveJob: Job? = null
private var openAudioEffectSession = false
private val player = playerKernelFactory.create(context, this, gaplessQueuerFactory, this)
var sessionOngoing = false
private set
init {
fun attach() {
player.attach()
imageSettings.registerListener(this)
player.addListener(this)
playbackManager.registerStateHolder(this)
playbackSettings.registerListener(this)
musicRepository.addUpdateListener(this)
}
fun release() {
saveJob.cancel()
player.removeListener(this)
player.release()
playbackManager.unregisterStateHolder(this)
musicRepository.removeUpdateListener(this)
replayGainProcessor.release()
@ -109,7 +128,7 @@ class ExoPlaybackStateHolder(
override val progression: Progression
get() {
val mediaItem = player.currentMediaItem ?: return Progression.nil()
val mediaItem = player.queuer.currentMediaItem ?: return Progression.nil()
val duration = mediaItem.mediaMetadata.extras?.getLong("durationMs") ?: Long.MAX_VALUE
val clampedPosition = player.currentPosition.coerceAtLeast(0).coerceAtMost(duration)
return Progression.from(player.playWhenReady, player.isPlaying, clampedPosition)
@ -117,7 +136,7 @@ class ExoPlaybackStateHolder(
override val repeatMode
get() =
when (val repeatMode = player.repeatMode) {
when (val repeatMode = player.queuer.repeatMode) {
Player.REPEAT_MODE_OFF -> RepeatMode.NONE
Player.REPEAT_MODE_ONE -> RepeatMode.TRACK
Player.REPEAT_MODE_ALL -> RepeatMode.ALL
@ -128,18 +147,11 @@ class ExoPlaybackStateHolder(
get() = player.audioSessionId
override fun resolveQueue(): RawQueue {
val deviceLibrary =
musicRepository.deviceLibrary
// No library, cannot do anything.
?: return RawQueue(emptyList(), emptyList(), 0)
val heap = (0 until player.mediaItemCount).map { player.getMediaItemAt(it) }
val heap = player.queuer.computeHeap()
val shuffledMapping =
if (player.shuffleModeEnabled) {
player.unscrambleQueueIndices()
} else {
emptyList()
}
return RawQueue(heap.mapNotNull { it.song }, shuffledMapping, player.currentMediaItemIndex)
if (player.queuer.shuffleModeEnabled) player.queuer.computeMapping() else emptyList()
return RawQueue(
heap.mapNotNull { it.song }, shuffledMapping, player.queuer.currentMediaItemIndex)
}
override fun handleDeferred(action: DeferredPlayback): Boolean {
@ -202,43 +214,31 @@ class ExoPlaybackStateHolder(
}
override fun repeatMode(repeatMode: RepeatMode) {
player.repeatMode =
player.queuer.repeatMode =
when (repeatMode) {
RepeatMode.NONE -> Player.REPEAT_MODE_OFF
RepeatMode.ALL -> Player.REPEAT_MODE_ALL
RepeatMode.TRACK -> Player.REPEAT_MODE_ONE
}
updatePauseOnRepeat()
playbackManager.ack(this, StateAck.RepeatModeChanged)
deferSave()
}
override fun newPlayback(command: PlaybackCommand) {
parent = command.parent
player.shuffleModeEnabled = command.shuffled
player.setMediaItems(command.queue.map { it.buildMediaItem() })
val mediaItems = command.queue.map { it.buildMediaItem() }
val startIndex =
command.song
?.let { command.queue.indexOf(it) }
.also { check(it != -1) { "Start song not in queue" } }
if (command.shuffled) {
player.setShuffleOrder(BetterShuffleOrder(command.queue.size, startIndex ?: -1))
}
val target = startIndex ?: player.currentTimeline.getFirstWindowIndex(command.shuffled)
player.seekTo(target, C.TIME_UNSET)
player.prepare()
player.queuer.prepareNew(mediaItems, startIndex, command.shuffled)
player.play()
playbackManager.ack(this, StateAck.NewPlayback)
deferSave()
}
override fun shuffled(shuffled: Boolean) {
player.setShuffleModeEnabled(shuffled)
if (player.shuffleModeEnabled) {
// Have to manually refresh the shuffle seed and anchor it to the new current songs
player.setShuffleOrder(
BetterShuffleOrder(player.mediaItemCount, player.currentMediaItemIndex))
}
player.queuer.shuffled(shuffled)
playbackManager.ack(this, StateAck.QueueReordered)
deferSave()
}
@ -247,14 +247,14 @@ class ExoPlaybackStateHolder(
// Replicate the old pseudo-circular queue behavior when no repeat option is implemented.
// Basically, you can't skip back and wrap around the queue, but you can skip forward and
// wrap around the queue, albeit playback will be paused.
if (player.repeatMode == Player.REPEAT_MODE_ALL || player.hasNextMediaItem()) {
player.seekToNext()
if (player.queuer.repeatMode == Player.REPEAT_MODE_ALL ||
player.queuer.hasNextMediaItem()) {
player.queuer.seekToNext()
if (!playbackSettings.rememberPause) {
player.play()
}
} else {
player.seekTo(
player.currentTimeline.getFirstWindowIndex(player.shuffleModeEnabled), C.TIME_UNSET)
player.queuer.goto(player.queuer.computeFirstMediaItemIndex())
// TODO: Dislike the UX implications of this, I feel should I bite the bullet
// and switch to dynamic skip enable/disable?
if (!playbackSettings.rememberPause) {
@ -267,9 +267,9 @@ class ExoPlaybackStateHolder(
override fun prev() {
if (playbackSettings.rewindWithPrev) {
player.seekToPrevious()
} else if (player.hasPreviousMediaItem()) {
player.seekToPreviousMediaItem()
player.queuer.seekToPrevious()
} else if (player.queuer.hasPreviousMediaItem()) {
player.queuer.seekToPreviousMediaItem()
} else {
player.seekTo(0)
}
@ -281,13 +281,12 @@ class ExoPlaybackStateHolder(
}
override fun goto(index: Int) {
val indices = player.unscrambleQueueIndices()
val indices = player.queuer.computeMapping()
if (indices.isEmpty()) {
return
}
val trueIndex = indices[index]
player.seekTo(trueIndex, C.TIME_UNSET) // Handles remaining custom logic
player.queuer.goto(trueIndex)
if (!playbackSettings.rememberPause) {
player.play()
}
@ -296,63 +295,40 @@ class ExoPlaybackStateHolder(
}
override fun playNext(songs: List<Song>, ack: StateAck.PlayNext) {
val currTimeline = player.currentTimeline
val nextIndex =
if (currTimeline.isEmpty) {
C.INDEX_UNSET
} else {
currTimeline.getNextWindowIndex(
player.currentMediaItemIndex, Player.REPEAT_MODE_OFF, player.shuffleModeEnabled)
}
if (nextIndex == C.INDEX_UNSET) {
player.addMediaItems(songs.map { it.buildMediaItem() })
} else {
player.addMediaItems(nextIndex, songs.map { it.buildMediaItem() })
}
player.queuer.addBottomMediaItems(songs.map { it.buildMediaItem() })
playbackManager.ack(this, ack)
deferSave()
}
override fun addToQueue(songs: List<Song>, ack: StateAck.AddToQueue) {
player.addMediaItems(songs.map { it.buildMediaItem() })
player.queuer.addTopMediaItems(songs.map { it.buildMediaItem() })
playbackManager.ack(this, ack)
deferSave()
}
override fun move(from: Int, to: Int, ack: StateAck.Move) {
val indices = player.unscrambleQueueIndices()
val indices = player.queuer.computeMapping()
if (indices.isEmpty()) {
return
}
val trueFrom = indices[from]
val trueTo = indices[to]
// ExoPlayer does not actually update it's ShuffleOrder when moving items. Retain a
// semblance of "normalcy" by doing a weird no-op swap that actually moves the item.
when {
trueFrom > trueTo -> {
player.moveMediaItem(trueFrom, trueTo)
player.moveMediaItem(trueTo + 1, trueFrom)
}
trueTo > trueFrom -> {
player.moveMediaItem(trueFrom, trueTo)
player.moveMediaItem(trueTo - 1, trueFrom)
}
}
player.queuer.moveMediaItem(trueFrom, trueTo)
playbackManager.ack(this, ack)
deferSave()
}
override fun remove(at: Int, ack: StateAck.Remove) {
val indices = player.unscrambleQueueIndices()
val indices = player.queuer.computeMapping()
if (indices.isEmpty()) {
return
}
val trueIndex = indices[at]
val songWillChange = player.currentMediaItemIndex == trueIndex
player.removeMediaItem(trueIndex)
val songWillChange = player.queuer.currentMediaItemIndex == trueIndex
player.queuer.removeMediaItem(trueIndex)
if (songWillChange && !playbackSettings.rememberPause) {
player.play()
}
@ -372,15 +348,11 @@ class ExoPlaybackStateHolder(
sendEvent = true
}
if (rawQueue != resolveQueue()) {
player.setMediaItems(rawQueue.heap.map { it.buildMediaItem() })
if (rawQueue.isShuffled) {
player.shuffleModeEnabled = true
player.setShuffleOrder(BetterShuffleOrder(rawQueue.shuffledMapping.toIntArray()))
} else {
player.shuffleModeEnabled = false
}
player.seekTo(rawQueue.heapIndex, C.TIME_UNSET)
player.prepare()
player.queuer.prepareSaved(
rawQueue.heap.map { it.buildMediaItem() },
rawQueue.shuffledMapping,
rawQueue.heapIndex,
rawQueue.isShuffled)
player.pause()
sendEvent = true
}
@ -404,16 +376,14 @@ class ExoPlaybackStateHolder(
}
override fun reset(ack: StateAck.NewPlayback) {
player.setMediaItems(listOf())
player.queuer.discard()
playbackManager.ack(this, ack)
deferSave()
}
// --- PLAYER OVERRIDES ---
override fun onPlayWhenReadyChanged(playWhenReady: Boolean, reason: Int) {
super.onPlayWhenReadyChanged(playWhenReady, reason)
override fun onPlayWhenReadyChanged() {
if (player.playWhenReady) {
// Mark that we have started playing so that the notification can now be posted.
logD("Player has started playing")
@ -431,40 +401,27 @@ class ExoPlaybackStateHolder(
broadcastAudioEffectAction(AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION)
openAudioEffectSession = false
}
playbackManager.ack(this, StateAck.ProgressionChanged)
deferSave()
}
override fun onPlaybackStateChanged(playbackState: Int) {
super.onPlaybackStateChanged(playbackState)
if (playbackState == Player.STATE_ENDED && player.repeatMode == Player.REPEAT_MODE_OFF) {
goto(0)
player.pause()
}
override fun onIsPlayingChanged() {
playbackManager.ack(this, StateAck.ProgressionChanged)
deferSave()
}
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
super.onMediaItemTransition(mediaItem, reason)
if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_AUTO) {
playbackManager.ack(this, StateAck.IndexMoved)
}
override fun onPositionDiscontinuity() {
playbackManager.ack(this, StateAck.ProgressionChanged)
deferSave()
}
override fun onEvents(player: Player, events: Player.Events) {
super.onEvents(player, events)
// So many actions trigger progression changes that it becomes easier just to handle it
// in an ExoPlayer callback anyway. This doesn't really cause issues anywhere.
if (events.containsAny(
Player.EVENT_PLAY_WHEN_READY_CHANGED,
Player.EVENT_IS_PLAYING_CHANGED,
Player.EVENT_POSITION_DISCONTINUITY)) {
logD("Player state changed, must synchronize state")
playbackManager.ack(this, StateAck.ProgressionChanged)
}
override fun onAutoTransition() {
playbackManager.ack(this, StateAck.IndexMoved)
deferSave()
}
override fun onPlayerError(error: PlaybackException) {
override fun onError(error: PlaybackException) {
// TODO: Replace with no skipping and a notification instead
// If there's any issue, just go to the next song.
logE("Player error occurred")
@ -491,17 +448,7 @@ class ExoPlaybackStateHolder(
}
}
// --- PLAYBACKSETTINGS OVERRIDES ---
override fun onPauseOnRepeatChanged() {
super.onPauseOnRepeatChanged()
updatePauseOnRepeat()
}
private fun updatePauseOnRepeat() {
player.pauseAtEndOfMediaItems =
player.repeatMode == Player.REPEAT_MODE_ONE && playbackSettings.pauseOnRepeat
}
// --- OVERRIDES ---
private fun save(cb: () -> Unit) {
saveJob {
@ -533,101 +480,6 @@ class ExoPlaybackStateHolder(
private val MediaItem.song: Song?
get() = this.localConfiguration?.tag as? Song?
private fun Player.unscrambleQueueIndices(): List<Int> {
val timeline = currentTimeline
if (timeline.isEmpty) {
return emptyList()
}
val queue = mutableListOf<Int>()
// Add the active queue item.
val currentMediaItemIndex = currentMediaItemIndex
queue.add(currentMediaItemIndex)
// Fill queue alternating with next and/or previous queue items.
var firstMediaItemIndex = currentMediaItemIndex
var lastMediaItemIndex = currentMediaItemIndex
val shuffleModeEnabled = shuffleModeEnabled
while ((firstMediaItemIndex != C.INDEX_UNSET || lastMediaItemIndex != C.INDEX_UNSET)) {
// Begin with next to have a longer tail than head if an even sized queue needs to be
// trimmed.
if (lastMediaItemIndex != C.INDEX_UNSET) {
lastMediaItemIndex =
timeline.getNextWindowIndex(
lastMediaItemIndex, Player.REPEAT_MODE_OFF, shuffleModeEnabled)
if (lastMediaItemIndex != C.INDEX_UNSET) {
queue.add(lastMediaItemIndex)
}
}
if (firstMediaItemIndex != C.INDEX_UNSET) {
firstMediaItemIndex =
timeline.getPreviousWindowIndex(
firstMediaItemIndex, Player.REPEAT_MODE_OFF, shuffleModeEnabled)
if (firstMediaItemIndex != C.INDEX_UNSET) {
queue.add(0, firstMediaItemIndex)
}
}
}
return queue
}
class Factory
@Inject
constructor(
@ApplicationContext private val context: Context,
private val playbackManager: PlaybackStateManager,
private val persistenceRepository: PersistenceRepository,
private val playbackSettings: PlaybackSettings,
private val commandFactory: PlaybackCommand.Factory,
private val mediaSourceFactory: MediaSource.Factory,
private val replayGainProcessorFactory: ReplayGainAudioProcessor.Factory,
private val musicRepository: MusicRepository,
private val imageSettings: ImageSettings,
) {
fun create(): ExoPlaybackStateHolder {
// Since Auxio is a music player, only specify an audio renderer to save
// battery/apk size/cache size
val replayGainProcessor = replayGainProcessorFactory.create()
val audioRenderer = RenderersFactory { handler, _, audioListener, _, _ ->
arrayOf(
FfmpegAudioRenderer(handler, audioListener, replayGainProcessor),
MediaCodecAudioRenderer(
context,
MediaCodecSelector.DEFAULT,
handler,
audioListener,
AudioCapabilities.DEFAULT_AUDIO_CAPABILITIES,
replayGainProcessor))
}
val exoPlayer =
ExoPlayer.Builder(context, audioRenderer)
.setMediaSourceFactory(mediaSourceFactory)
// Enable automatic WakeLock support
.setWakeMode(C.WAKE_MODE_LOCAL)
.setAudioAttributes(
// Signal that we are a music player.
AudioAttributes.Builder()
.setUsage(C.USAGE_MEDIA)
.setContentType(C.AUDIO_CONTENT_TYPE_MUSIC)
.build(),
true)
.build()
return ExoPlaybackStateHolder(
context,
exoPlayer,
playbackManager,
persistenceRepository,
playbackSettings,
commandFactory,
replayGainProcessor,
musicRepository,
imageSettings)
}
}
private companion object {
const val SAVE_BUFFER = 5000L
}

View file

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

View file

@ -45,19 +45,11 @@ import org.oxycblt.auxio.util.logD
* @author Alexander Capehart (OxygenCobalt)
*/
class ReplayGainAudioProcessor
private constructor(
@Inject
constructor(
private val playbackManager: PlaybackStateManager,
private val playbackSettings: PlaybackSettings
) : BaseAudioProcessor(), PlaybackStateManager.Listener, PlaybackSettings.Listener {
class Factory
@Inject
constructor(
private val playbackManager: PlaybackStateManager,
private val playbackSettings: PlaybackSettings
) {
fun create() = ReplayGainAudioProcessor(playbackManager, playbackSettings)
}
private var volume = 1f
set(value) {
field = value
@ -65,7 +57,7 @@ private constructor(
flush()
}
init {
fun attach() {
playbackManager.addListener(this)
playbackSettings.registerListener(this)
}

View file

@ -96,7 +96,7 @@ private constructor(
val notification: ForegroundServiceNotification
get() = _notification
init {
fun attach() {
playbackManager.addListener(this)
playbackSettings.registerListener(this)
imageSettings.registerListener(this)

View file

@ -25,6 +25,7 @@ import kotlinx.coroutines.Job
import org.oxycblt.auxio.ForegroundListener
import org.oxycblt.auxio.ForegroundServiceNotification
import org.oxycblt.auxio.IntegerTable
import org.oxycblt.auxio.playback.player.PlayerStateHolder
import org.oxycblt.auxio.playback.state.DeferredPlayback
import org.oxycblt.auxio.playback.state.PlaybackStateManager
import org.oxycblt.auxio.util.logD
@ -35,7 +36,7 @@ private constructor(
private val context: Context,
private val foregroundListener: ForegroundListener,
private val playbackManager: PlaybackStateManager,
exoHolderFactory: ExoPlaybackStateHolder.Factory,
playerHolderFactory: PlayerStateHolder.Factory,
sessionHolderFactory: MediaSessionHolder.Factory,
widgetComponentFactory: WidgetComponent.Factory,
systemReceiverFactory: SystemPlaybackReceiver.Factory,
@ -44,7 +45,7 @@ private constructor(
@Inject
constructor(
private val playbackManager: PlaybackStateManager,
private val exoHolderFactory: ExoPlaybackStateHolder.Factory,
private val exoHolderFactory: PlayerStateHolder.Factory,
private val sessionHolderFactory: MediaSessionHolder.Factory,
private val widgetComponentFactory: WidgetComponent.Factory,
private val systemReceiverFactory: SystemPlaybackReceiver.Factory,
@ -61,18 +62,20 @@ private constructor(
}
private val waitJob = Job()
private val exoHolder = exoHolderFactory.create()
private val exoHolder = playerHolderFactory.create(context)
private val sessionHolder = sessionHolderFactory.create(context, foregroundListener)
private val widgetComponent = widgetComponentFactory.create(context)
private val systemReceiver = systemReceiverFactory.create(context)
val token: MediaSessionCompat.Token
get() = sessionHolder.token
private val systemReceiver = systemReceiverFactory.create(context, widgetComponent)
// --- MEDIASESSION CALLBACKS ---
init {
fun attach(): MediaSessionCompat.Token {
exoHolder.attach()
sessionHolder.attach()
widgetComponent.attach()
systemReceiver.attach()
playbackManager.addListener(this)
return sessionHolder.token
}
fun handleTaskRemoved() {

View file

@ -37,6 +37,7 @@ import org.oxycblt.auxio.widgets.WidgetProvider
*/
class SystemPlaybackReceiver
private constructor(
private val context: Context,
private val playbackManager: PlaybackStateManager,
private val playbackSettings: PlaybackSettings,
private val widgetComponent: WidgetComponent
@ -47,16 +48,19 @@ private constructor(
@Inject
constructor(
private val playbackManager: PlaybackStateManager,
private val playbackSettings: PlaybackSettings,
private val widgetComponent: WidgetComponent
private val playbackSettings: PlaybackSettings
) {
fun create(context: Context): SystemPlaybackReceiver {
val receiver =
SystemPlaybackReceiver(playbackManager, playbackSettings, widgetComponent)
ContextCompat.registerReceiver(
context, receiver, INTENT_FILTER, ContextCompat.RECEIVER_EXPORTED)
return receiver
}
fun create(context: Context, widgetComponent: WidgetComponent) =
SystemPlaybackReceiver(context, playbackManager, playbackSettings, widgetComponent)
}
fun attach() {
ContextCompat.registerReceiver(
context, this, INTENT_FILTER, ContextCompat.RECEIVER_EXPORTED)
}
fun release() {
context.unregisterReceiver(this)
}
override fun onReceive(context: Context, intent: Intent) {

View file

@ -67,7 +67,7 @@ private constructor(
private val widgetProvider = WidgetProvider()
init {
fun attach() {
playbackManager.addListener(this)
uiSettings.registerListener(this)
imageSettings.registerListener(this)

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 217 B