playback: basic play from search functionality

This commit is contained in:
Alexander Capehart 2024-08-28 15:52:42 -06:00
parent fda4548515
commit b2e7c1eb50
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
2 changed files with 111 additions and 25 deletions

View file

@ -251,10 +251,15 @@ private constructor(
.putText(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, artist)
.putText(MediaMetadataCompat.METADATA_KEY_DISPLAY_DESCRIPTION, album)
.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, song.durationMs)
.putText(PlaybackNotification.KEY_PARENT,
.putText(
PlaybackNotification.KEY_PARENT,
parent?.name?.resolve(context) ?: context.getString(R.string.lbl_all_songs))
.putText(MetadataExtras.KEY_SUBTITLE_LINK_MEDIA_ID, MediaSessionUID.SingleItem(song.artists[0].uid).toString())
.putText(MetadataExtras.KEY_DESCRIPTION_LINK_MEDIA_ID, MediaSessionUID.SingleItem(song.album.uid).toString())
.putText(
MetadataExtras.KEY_SUBTITLE_LINK_MEDIA_ID,
MediaSessionUID.SingleItem(song.artists[0].uid).toString())
.putText(
MetadataExtras.KEY_DESCRIPTION_LINK_MEDIA_ID,
MediaSessionUID.SingleItem(song.album.uid).toString())
// These fields are nullable and so we must check first before adding them to the fields.
song.track?.let {
logD("Adding track information")
@ -309,7 +314,8 @@ private constructor(
// MediaStore URI instead of loading a bitmap.
.setIconUri(song.album.cover.single.mediaStoreCoverUri)
.setMediaUri(song.uri)
.setExtras(Bundle().apply { putInt(MediaSessionInterface.KEY_QUEUE_POS, i) })
.setExtras(
Bundle().apply { putInt(MediaSessionInterface.KEY_QUEUE_POS, i) })
.build()
// Store the item index so we can then use the analogous index in the
// playback state.

View file

@ -22,12 +22,14 @@ import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.provider.MediaStore
import android.support.v4.media.MediaDescriptionCompat
import android.support.v4.media.session.MediaSessionCompat
import android.support.v4.media.session.PlaybackStateCompat
import dagger.hilt.android.qualifiers.ApplicationContext
import org.oxycblt.auxio.BuildConfig
import javax.inject.Inject
import org.apache.commons.text.similarity.JaroWinklerSimilarity
import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre
@ -36,7 +38,9 @@ import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.MusicRepository
import org.oxycblt.auxio.music.Playlist
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.device.DeviceLibrary
import org.oxycblt.auxio.music.service.MediaSessionUID
import org.oxycblt.auxio.music.user.UserLibrary
import org.oxycblt.auxio.playback.state.PlaybackCommand
import org.oxycblt.auxio.playback.state.PlaybackStateManager
import org.oxycblt.auxio.playback.state.RepeatMode
@ -50,39 +54,65 @@ constructor(
private val commandFactory: PlaybackCommand.Factory,
private val musicRepository: MusicRepository,
) : MediaSessionCompat.Callback() {
private val jaroWinkler = JaroWinklerSimilarity()
override fun onPrepare() {
super.onPrepare()
// STUB, we already automatically prepare playback.
}
override fun onPrepareFromMediaId(mediaId: String?, extras: Bundle?) {
super.onPrepareFromMediaId(mediaId, extras)
// STUB, can't tell when this is called
}
override fun onPrepareFromUri(uri: Uri?, extras: Bundle?) {
super.onPrepareFromUri(uri, extras)
// STUB, can't tell when this is called
}
override fun onPlayFromUri(uri: Uri?, extras: Bundle?) {
super.onPlayFromUri(uri, extras)
// STUB, can't tell when this is called
}
override fun onPlayFromMediaId(mediaId: String?, extras: Bundle?) {
super.onPlayFromMediaId(mediaId, extras)
val uid = MediaSessionUID.fromString(mediaId ?: return) ?: return
val command = expandIntoCommand(uid)
requireNotNull(command) { "Invalid playback configuration" }
playbackManager.play(command)
val command = expandUidIntoCommand(uid)
playbackManager.play(requireNotNull(command) { "Invalid playback configuration" })
}
override fun onPrepareFromSearch(query: String?, extras: Bundle?) {
super.onPrepareFromSearch(query, extras)
// STUB, can't tell when this is called
}
override fun onPlayFromSearch(query: String?, extras: Bundle?) {
override fun onPlayFromSearch(query: String, extras: Bundle) {
super.onPlayFromSearch(query, extras)
// STUB: Unimplemented, no search engine
val deviceLibrary = musicRepository.deviceLibrary ?: return
val userLibrary = musicRepository.userLibrary ?: return
val queryBundle =
QueryBundle(
(extras.getString(MediaStore.EXTRA_MEDIA_TITLE) ?: query).ifBlank { null },
extras.getString(MediaStore.EXTRA_MEDIA_ALBUM)?.ifBlank { null },
extras.getString(MediaStore.EXTRA_MEDIA_ARTIST)?.ifBlank { null },
extras.getString(MediaStore.EXTRA_MEDIA_GENRE)?.ifBlank { null },
extras.getString(@Suppress("DEPRECATION") MediaStore.EXTRA_MEDIA_PLAYLIST))
val command = expandSearchInfoCommand(queryBundle, deviceLibrary, userLibrary)
playbackManager.play(requireNotNull(command) { "Invalid playback configuration" })
}
override fun onPrepareFromUri(uri: Uri?, extras: Bundle?) {
super.onPrepareFromUri(uri, extras)
}
data class QueryBundle(
val title: String?,
val album: String?,
val artist: String?,
val genre: String?,
val playlist: String?
)
override fun onPlayFromUri(uri: Uri?, extras: Bundle?) {
super.onPlayFromUri(uri, extras)
// STUB
}
private fun <T : Music> Collection<T>.fuzzyBest(query: String): T =
maxByOrNull { jaroWinkler.apply(it.name.resolve(context), query) } ?: first()
override fun onAddQueueItem(description: MediaDescriptionCompat) {
super.onAddQueueItem(description)
@ -120,6 +150,10 @@ constructor(
playbackManager.prev()
}
override fun onSkipToQueueItem(id: Long) {
playbackManager.goto(id.toInt())
}
override fun onSeekTo(position: Long) {
playbackManager.seekTo(position)
}
@ -149,8 +183,9 @@ constructor(
shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_GROUP)
}
override fun onSkipToQueueItem(id: Long) {
playbackManager.goto(id.toInt())
override fun onStop() {
// Get the service to shut down with the ACTION_EXIT intent
context.sendBroadcast(Intent(PlaybackActions.ACTION_EXIT))
}
override fun onCustomAction(action: String, extras: Bundle?) {
@ -160,12 +195,7 @@ constructor(
context.sendBroadcast(Intent(action))
}
override fun onStop() {
// Get the service to shut down with the ACTION_EXIT intent
context.sendBroadcast(Intent(PlaybackActions.ACTION_EXIT))
}
private fun expandIntoCommand(uid: MediaSessionUID): PlaybackCommand? {
private fun expandUidIntoCommand(uid: MediaSessionUID): PlaybackCommand? {
val music: Music
var parent: MusicParent? = null
when (uid) {
@ -188,6 +218,55 @@ constructor(
}
}
private fun expandSearchInfoCommand(
query: QueryBundle,
deviceLibrary: DeviceLibrary,
userLibrary: UserLibrary
): PlaybackCommand? {
if (query.album != null) {
val album = deviceLibrary.albums.fuzzyBest(query.album)
if (query.title == null) {
return commandFactory.album(album, ShuffleMode.OFF)
}
val song = album.songs.fuzzyBest(query.title)
return commandFactory.songFromAlbum(song, ShuffleMode.OFF)
}
if (query.artist != null) {
val artist = deviceLibrary.artists.fuzzyBest(query.artist)
if (query.title == null) {
return commandFactory.artist(artist, ShuffleMode.OFF)
}
val song = artist.songs.fuzzyBest(query.title)
return commandFactory.songFromArtist(song, artist, ShuffleMode.OFF)
}
if (query.genre != null) {
val genre = deviceLibrary.genres.fuzzyBest(query.genre)
if (query.title == null) {
return commandFactory.genre(genre, ShuffleMode.OFF)
}
val song = genre.songs.fuzzyBest(query.title)
return commandFactory.songFromGenre(song, genre, ShuffleMode.OFF)
}
if (query.playlist != null) {
val playlist = userLibrary.playlists.fuzzyBest(query.playlist)
if (query.title == null) {
return commandFactory.playlist(playlist, ShuffleMode.OFF)
}
val song = playlist.songs.fuzzyBest(query.title)
return commandFactory.songFromPlaylist(song, playlist, ShuffleMode.OFF)
}
if (query.title != null) {
val song = deviceLibrary.songs.fuzzyBest(query.title)
return commandFactory.songFromAll(song, ShuffleMode.OFF)
}
return commandFactory.all(ShuffleMode.ON)
}
private fun inferSongFromParent(music: Song, parent: MusicParent?) =
when (parent) {
is Album -> commandFactory.songFromAlbum(music, ShuffleMode.IMPLICIT)
@ -203,6 +282,7 @@ constructor(
const val KEY_QUEUE_POS = BuildConfig.APPLICATION_ID + ".metadata.QUEUE_POS"
const val ACTIONS =
PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID or
PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH or
PlaybackStateCompat.ACTION_PLAY or
PlaybackStateCompat.ACTION_PAUSE or
PlaybackStateCompat.ACTION_PLAY_PAUSE or