playback: fix notif issues on older devices

- Slight coroutine delay in cover fetch causes the notif to flicker
- Default play/pause actions look absolutely hideous
This commit is contained in:
Alexander Capehart 2024-04-19 18:48:54 -06:00
parent b99cd96726
commit 8b7b916489
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
8 changed files with 116 additions and 24 deletions

View file

@ -18,22 +18,31 @@
package org.oxycblt.auxio.image.service package org.oxycblt.auxio.image.service
import android.content.Context
import android.graphics.Bitmap import android.graphics.Bitmap
import android.net.Uri import android.net.Uri
import androidx.media3.common.MediaMetadata import androidx.media3.common.MediaMetadata
import androidx.media3.common.util.BitmapLoader import androidx.media3.common.util.BitmapLoader
import coil.ImageLoader
import coil.memory.MemoryCache
import coil.request.Options
import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.ListenableFuture
import com.google.common.util.concurrent.SettableFuture import com.google.common.util.concurrent.SettableFuture
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject import javax.inject.Inject
import org.oxycblt.auxio.image.BitmapProvider import org.oxycblt.auxio.image.BitmapProvider
import org.oxycblt.auxio.image.extractor.SongKeyer
import org.oxycblt.auxio.music.MusicRepository import org.oxycblt.auxio.music.MusicRepository
import org.oxycblt.auxio.music.service.MediaSessionUID import org.oxycblt.auxio.music.service.MediaSessionUID
class MediaSessionBitmapLoader class MediaSessionBitmapLoader
@Inject @Inject
constructor( constructor(
@ApplicationContext private val context: Context,
private val musicRepository: MusicRepository, private val musicRepository: MusicRepository,
private val bitmapProvider: BitmapProvider private val bitmapProvider: BitmapProvider,
private val songKeyer: SongKeyer,
private val imageLoader: ImageLoader,
) : BitmapLoader { ) : BitmapLoader {
override fun decodeBitmap(data: ByteArray): ListenableFuture<Bitmap> { override fun decodeBitmap(data: ByteArray): ListenableFuture<Bitmap> {
throw NotImplementedError() throw NotImplementedError()
@ -58,6 +67,13 @@ constructor(
else -> return null else -> return null
} }
?: return null ?: return null
// Even launching a coroutine to obtained cached covers is enough to make the notification
// go without covers.
val key = songKeyer.key(listOf(song), Options(context))
if (imageLoader.memoryCache?.get(MemoryCache.Key(key)) != null) {
future.set(imageLoader.memoryCache?.get(MemoryCache.Key(key))?.bitmap)
return future
}
bitmapProvider.load( bitmapProvider.load(
song, song,
object : BitmapProvider.Target { object : BitmapProvider.Target {

View file

@ -15,7 +15,7 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* 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.music.service package org.oxycblt.auxio.music.service
import android.content.Context import android.content.Context
@ -141,10 +141,8 @@ constructor(
is MediaSessionUID.Category -> return uid.toMediaItem(context) is MediaSessionUID.Category -> return uid.toMediaItem(context)
is MediaSessionUID.Single -> is MediaSessionUID.Single ->
musicRepository.find(uid.uid)?.let { musicRepository.find(it.uid) } musicRepository.find(uid.uid)?.let { musicRepository.find(it.uid) }
is MediaSessionUID.Joined -> is MediaSessionUID.Joined ->
musicRepository.find(uid.childUid)?.let { musicRepository.find(it.uid) } musicRepository.find(uid.childUid)?.let { musicRepository.find(it.uid) }
null -> null null -> null
} }
?: return null ?: return null
@ -179,40 +177,32 @@ constructor(
when (mediaSessionUID) { when (mediaSessionUID) {
MediaSessionUID.Category.ROOT -> MediaSessionUID.Category.ROOT ->
MediaSessionUID.Category.IMPORTANT.map { it.toMediaItem(context) } MediaSessionUID.Category.IMPORTANT.map { it.toMediaItem(context) }
MediaSessionUID.Category.SONGS -> MediaSessionUID.Category.SONGS ->
listSettings.songSort.songs(deviceLibrary.songs).map { listSettings.songSort.songs(deviceLibrary.songs).map {
it.toMediaItem(context, null) it.toMediaItem(context, null)
} }
MediaSessionUID.Category.ALBUMS -> MediaSessionUID.Category.ALBUMS ->
listSettings.albumSort.albums(deviceLibrary.albums).map { listSettings.albumSort.albums(deviceLibrary.albums).map {
it.toMediaItem(context) it.toMediaItem(context)
} }
MediaSessionUID.Category.ARTISTS -> MediaSessionUID.Category.ARTISTS ->
listSettings.artistSort.artists(deviceLibrary.artists).map { listSettings.artistSort.artists(deviceLibrary.artists).map {
it.toMediaItem(context) it.toMediaItem(context)
} }
MediaSessionUID.Category.GENRES -> MediaSessionUID.Category.GENRES ->
listSettings.genreSort.genres(deviceLibrary.genres).map { listSettings.genreSort.genres(deviceLibrary.genres).map {
it.toMediaItem(context) it.toMediaItem(context)
} }
MediaSessionUID.Category.PLAYLISTS -> MediaSessionUID.Category.PLAYLISTS ->
userLibrary.playlists.map { it.toMediaItem(context) } userLibrary.playlists.map { it.toMediaItem(context) }
} }
} }
is MediaSessionUID.Single -> { is MediaSessionUID.Single -> {
getChildMediaItems(mediaSessionUID.uid) getChildMediaItems(mediaSessionUID.uid)
} }
is MediaSessionUID.Joined -> { is MediaSessionUID.Joined -> {
getChildMediaItems(mediaSessionUID.childUid) getChildMediaItems(mediaSessionUID.childUid)
} }
null -> { null -> {
return null return null
} }
@ -225,24 +215,20 @@ constructor(
val songs = listSettings.albumSongSort.songs(item.songs) val songs = listSettings.albumSongSort.songs(item.songs)
songs.map { it.toMediaItem(context, item) } songs.map { it.toMediaItem(context, item) }
} }
is Artist -> { is Artist -> {
val albums = ARTIST_ALBUMS_SORT.albums(item.explicitAlbums + item.implicitAlbums) val albums = ARTIST_ALBUMS_SORT.albums(item.explicitAlbums + item.implicitAlbums)
val songs = listSettings.artistSongSort.songs(item.songs) val songs = listSettings.artistSongSort.songs(item.songs)
albums.map { it.toMediaItem(context) } + songs.map { it.toMediaItem(context, item) } albums.map { it.toMediaItem(context) } + songs.map { it.toMediaItem(context, item) }
} }
is Genre -> { is Genre -> {
val artists = GENRE_ARTISTS_SORT.artists(item.artists) val artists = GENRE_ARTISTS_SORT.artists(item.artists)
val songs = listSettings.genreSongSort.songs(item.songs) val songs = listSettings.genreSongSort.songs(item.songs)
artists.map { it.toMediaItem(context) } + artists.map { it.toMediaItem(context) } +
songs.map { it.toMediaItem(context, null) } songs.map { it.toMediaItem(context, null) }
} }
is Playlist -> { is Playlist -> {
item.songs.map { it.toMediaItem(context, item) } item.songs.map { it.toMediaItem(context, item) }
} }
is Song, is Song,
null -> return null null -> return null
} }
@ -339,8 +325,7 @@ constructor(
deviceLibrary.albums, deviceLibrary.albums,
deviceLibrary.artists, deviceLibrary.artists,
deviceLibrary.genres, deviceLibrary.genres,
userLibrary.playlists userLibrary.playlists)
)
val results = searchEngine.search(items, query) val results = searchEngine.search(items, query)
for (entry in searchSubscribers.entries) { for (entry in searchSubscribers.entries) {
if (entry.value == query) { if (entry.value == query) {

View file

@ -106,7 +106,8 @@ class ExoPlaybackStateHolder(
private set private set
val mediaSessionPlayer: Player val mediaSessionPlayer: Player
get() = MediaSessionPlayer(player, playbackManager, commandFactory, musicRepository) get() =
MediaSessionPlayer(context, player, playbackManager, commandFactory, musicRepository)
override val progression: Progression override val progression: Progression
get() { get() {

View file

@ -18,6 +18,8 @@
package org.oxycblt.auxio.playback.service package org.oxycblt.auxio.playback.service
import android.content.Context
import android.os.Bundle
import android.view.Surface import android.view.Surface
import android.view.SurfaceHolder import android.view.SurfaceHolder
import android.view.SurfaceView import android.view.SurfaceView
@ -31,6 +33,7 @@ import androidx.media3.common.PlaybackParameters
import androidx.media3.common.Player import androidx.media3.common.Player
import androidx.media3.common.TrackSelectionParameters import androidx.media3.common.TrackSelectionParameters
import java.lang.Exception import java.lang.Exception
import org.oxycblt.auxio.R
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
@ -60,6 +63,7 @@ import org.oxycblt.auxio.util.logE
* @author Alexander Capehart * @author Alexander Capehart
*/ */
class MediaSessionPlayer( class MediaSessionPlayer(
private val context: Context,
player: Player, player: Player,
private val playbackManager: PlaybackStateManager, private val playbackManager: PlaybackStateManager,
private val commandFactory: PlaybackCommand.Factory, private val commandFactory: PlaybackCommand.Factory,
@ -86,6 +90,20 @@ class MediaSessionPlayer(
setMediaItems(mediaItems, C.INDEX_UNSET, C.TIME_UNSET) setMediaItems(mediaItems, C.INDEX_UNSET, C.TIME_UNSET)
} }
override fun getMediaMetadata() =
super.getMediaMetadata().run {
val existingExtras = extras
val newExtras = existingExtras?.let { Bundle(it) } ?: Bundle()
newExtras.apply {
putString(
"parent",
playbackManager.parent?.name?.resolve(context)
?: context.getString(R.string.lbl_all_songs))
}
buildUpon().setExtras(newExtras).build()
}
override fun setMediaItems( override fun setMediaItems(
mediaItems: MutableList<MediaItem>, mediaItems: MutableList<MediaItem>,
startIndex: Int, startIndex: Int,

View file

@ -255,7 +255,7 @@ constructor(
mediaSession.setCustomLayout(layout) mediaSession.setCustomLayout(layout)
} }
override fun invalidate(ids: Map<String, Int>){ override fun invalidate(ids: Map<String, Int>) {
for (id in ids) { for (id in ids) {
mediaSession.notifyChildrenChanged(id.key, id.value, null) mediaSession.notifyChildrenChanged(id.key, id.value, null)
} }

View file

@ -1,6 +1,6 @@
/* /*
* Copyright (c) 2024 Auxio Project * Copyright (c) 2024 Auxio Project
* SystemPlaybackReciever.kt is part of Auxio. * PlaybackActionHandler.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
@ -25,7 +25,9 @@ import android.content.IntentFilter
import android.media.AudioManager import android.media.AudioManager
import android.os.Bundle import android.os.Bundle
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.media3.common.Player
import androidx.media3.session.CommandButton import androidx.media3.session.CommandButton
import androidx.media3.session.DefaultMediaNotificationProvider
import androidx.media3.session.SessionCommand import androidx.media3.session.SessionCommand
import androidx.media3.session.SessionCommands import androidx.media3.session.SessionCommands
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
@ -36,6 +38,7 @@ import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.ActionMode import org.oxycblt.auxio.playback.ActionMode
import org.oxycblt.auxio.playback.PlaybackSettings import org.oxycblt.auxio.playback.PlaybackSettings
import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.playback.state.PlaybackStateManager
import org.oxycblt.auxio.playback.state.Progression
import org.oxycblt.auxio.playback.state.RepeatMode import org.oxycblt.auxio.playback.state.RepeatMode
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.widgets.WidgetComponent import org.oxycblt.auxio.widgets.WidgetComponent
@ -102,6 +105,13 @@ constructor(
.setDisplayName(context.getString(R.string.desc_change_repeat)) .setDisplayName(context.getString(R.string.desc_change_repeat))
.setSessionCommand( .setSessionCommand(
SessionCommand(PlaybackActions.ACTION_INC_REPEAT_MODE, Bundle())) SessionCommand(PlaybackActions.ACTION_INC_REPEAT_MODE, Bundle()))
.setEnabled(true)
.setExtras(
Bundle().apply {
putInt(
DefaultMediaNotificationProvider.COMMAND_KEY_COMPACT_VIEW_INDEX,
0)
})
.build()) .build())
} }
ActionMode.SHUFFLE -> { ActionMode.SHUFFLE -> {
@ -113,16 +123,56 @@ constructor(
.setDisplayName(context.getString(R.string.lbl_shuffle)) .setDisplayName(context.getString(R.string.lbl_shuffle))
.setSessionCommand( .setSessionCommand(
SessionCommand(PlaybackActions.ACTION_INVERT_SHUFFLE, Bundle())) SessionCommand(PlaybackActions.ACTION_INVERT_SHUFFLE, Bundle()))
.setEnabled(true)
.build()) .build())
} }
else -> {} else -> {}
} }
actions.add(
CommandButton.Builder()
.setIconResId(R.drawable.ic_skip_prev_24)
.setDisplayName(context.getString(R.string.desc_skip_prev))
.setPlayerCommand(Player.COMMAND_SEEK_TO_PREVIOUS)
.setEnabled(true)
.setExtras(
Bundle().apply {
putInt(DefaultMediaNotificationProvider.COMMAND_KEY_COMPACT_VIEW_INDEX, 1)
})
.build())
actions.add(
CommandButton.Builder()
.setIconResId(
if (playbackManager.progression.isPlaying) R.drawable.ic_pause_24
else R.drawable.ic_play_24)
.setDisplayName(context.getString(R.string.desc_play_pause))
.setPlayerCommand(Player.COMMAND_PLAY_PAUSE)
.setEnabled(true)
.setExtras(
Bundle().apply {
putInt(DefaultMediaNotificationProvider.COMMAND_KEY_COMPACT_VIEW_INDEX, 2)
})
.build())
actions.add(
CommandButton.Builder()
.setIconResId(R.drawable.ic_skip_next_24)
.setDisplayName(context.getString(R.string.desc_skip_next))
.setPlayerCommand(Player.COMMAND_SEEK_TO_NEXT)
.setEnabled(true)
.setExtras(
Bundle().apply {
putInt(DefaultMediaNotificationProvider.COMMAND_KEY_COMPACT_VIEW_INDEX, 3)
})
.build())
actions.add( actions.add(
CommandButton.Builder() CommandButton.Builder()
.setIconResId(R.drawable.ic_close_24) .setIconResId(R.drawable.ic_close_24)
.setDisplayName(context.getString(R.string.desc_exit)) .setDisplayName(context.getString(R.string.desc_exit))
.setSessionCommand(SessionCommand(PlaybackActions.ACTION_EXIT, Bundle())) .setSessionCommand(SessionCommand(PlaybackActions.ACTION_EXIT, Bundle()))
.setEnabled(true)
.build()) .build())
return actions return actions
@ -133,6 +183,11 @@ constructor(
callback?.onCustomLayoutChanged(createCustomLayout()) callback?.onCustomLayoutChanged(createCustomLayout())
} }
override fun onProgressionChanged(progression: Progression) {
super.onProgressionChanged(progression)
callback?.onCustomLayoutChanged(createCustomLayout())
}
override fun onRepeatModeChanged(repeatMode: RepeatMode) { override fun onRepeatModeChanged(repeatMode: RepeatMode) {
super.onRepeatModeChanged(repeatMode) super.onRepeatModeChanged(repeatMode)
callback?.onCustomLayoutChanged(createCustomLayout()) callback?.onCustomLayoutChanged(createCustomLayout())

View file

@ -1,2 +1,19 @@
/*
* Copyright (c) 2024 Auxio Project
* Tasker.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.tasker package org.oxycblt.auxio.tasker

2
media

@ -1 +1 @@
Subproject commit bfa4c10f773bb9336d9c7dade490463318b12ab6 Subproject commit 6c77cfa13c83bf2ae5188603d2c9a51ec4cb3ac3