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_SUBTITLE, artist)
.putText(MediaMetadataCompat.METADATA_KEY_DISPLAY_DESCRIPTION, album) .putText(MediaMetadataCompat.METADATA_KEY_DISPLAY_DESCRIPTION, album)
.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, song.durationMs) .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)) 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(
.putText(MetadataExtras.KEY_DESCRIPTION_LINK_MEDIA_ID, MediaSessionUID.SingleItem(song.album.uid).toString()) 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. // These fields are nullable and so we must check first before adding them to the fields.
song.track?.let { song.track?.let {
logD("Adding track information") logD("Adding track information")
@ -309,7 +314,8 @@ private constructor(
// MediaStore URI instead of loading a bitmap. // MediaStore URI instead of loading a bitmap.
.setIconUri(song.album.cover.single.mediaStoreCoverUri) .setIconUri(song.album.cover.single.mediaStoreCoverUri)
.setMediaUri(song.uri) .setMediaUri(song.uri)
.setExtras(Bundle().apply { putInt(MediaSessionInterface.KEY_QUEUE_POS, i) }) .setExtras(
Bundle().apply { putInt(MediaSessionInterface.KEY_QUEUE_POS, i) })
.build() .build()
// Store the item index so we can then use the analogous index in the // Store the item index so we can then use the analogous index in the
// playback state. // playback state.

View file

@ -22,12 +22,14 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.provider.MediaStore
import android.support.v4.media.MediaDescriptionCompat import android.support.v4.media.MediaDescriptionCompat
import android.support.v4.media.session.MediaSessionCompat import android.support.v4.media.session.MediaSessionCompat
import android.support.v4.media.session.PlaybackStateCompat import android.support.v4.media.session.PlaybackStateCompat
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import org.oxycblt.auxio.BuildConfig
import javax.inject.Inject 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.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
@ -36,7 +38,9 @@ import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.MusicRepository import org.oxycblt.auxio.music.MusicRepository
import org.oxycblt.auxio.music.Playlist import org.oxycblt.auxio.music.Playlist
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.device.DeviceLibrary
import org.oxycblt.auxio.music.service.MediaSessionUID 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.PlaybackCommand
import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.playback.state.PlaybackStateManager
import org.oxycblt.auxio.playback.state.RepeatMode import org.oxycblt.auxio.playback.state.RepeatMode
@ -50,39 +54,65 @@ constructor(
private val commandFactory: PlaybackCommand.Factory, private val commandFactory: PlaybackCommand.Factory,
private val musicRepository: MusicRepository, private val musicRepository: MusicRepository,
) : MediaSessionCompat.Callback() { ) : MediaSessionCompat.Callback() {
private val jaroWinkler = JaroWinklerSimilarity()
override fun onPrepare() { override fun onPrepare() {
super.onPrepare() super.onPrepare()
// STUB, we already automatically prepare playback.
} }
override fun onPrepareFromMediaId(mediaId: String?, extras: Bundle?) { override fun onPrepareFromMediaId(mediaId: String?, extras: Bundle?) {
super.onPrepareFromMediaId(mediaId, extras) 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?) { override fun onPlayFromMediaId(mediaId: String?, extras: Bundle?) {
super.onPlayFromMediaId(mediaId, extras) super.onPlayFromMediaId(mediaId, extras)
val uid = MediaSessionUID.fromString(mediaId ?: return) ?: return val uid = MediaSessionUID.fromString(mediaId ?: return) ?: return
val command = expandIntoCommand(uid) val command = expandUidIntoCommand(uid)
requireNotNull(command) { "Invalid playback configuration" } playbackManager.play(requireNotNull(command) { "Invalid playback configuration" })
playbackManager.play(command)
} }
override fun onPrepareFromSearch(query: String?, extras: Bundle?) { override fun onPrepareFromSearch(query: String?, extras: Bundle?) {
super.onPrepareFromSearch(query, extras) 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) 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?) { data class QueryBundle(
super.onPrepareFromUri(uri, extras) val title: String?,
} val album: String?,
val artist: String?,
val genre: String?,
val playlist: String?
)
override fun onPlayFromUri(uri: Uri?, extras: Bundle?) { private fun <T : Music> Collection<T>.fuzzyBest(query: String): T =
super.onPlayFromUri(uri, extras) maxByOrNull { jaroWinkler.apply(it.name.resolve(context), query) } ?: first()
// STUB
}
override fun onAddQueueItem(description: MediaDescriptionCompat) { override fun onAddQueueItem(description: MediaDescriptionCompat) {
super.onAddQueueItem(description) super.onAddQueueItem(description)
@ -120,6 +150,10 @@ constructor(
playbackManager.prev() playbackManager.prev()
} }
override fun onSkipToQueueItem(id: Long) {
playbackManager.goto(id.toInt())
}
override fun onSeekTo(position: Long) { override fun onSeekTo(position: Long) {
playbackManager.seekTo(position) playbackManager.seekTo(position)
} }
@ -149,8 +183,9 @@ constructor(
shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_GROUP) shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_GROUP)
} }
override fun onSkipToQueueItem(id: Long) { override fun onStop() {
playbackManager.goto(id.toInt()) // Get the service to shut down with the ACTION_EXIT intent
context.sendBroadcast(Intent(PlaybackActions.ACTION_EXIT))
} }
override fun onCustomAction(action: String, extras: Bundle?) { override fun onCustomAction(action: String, extras: Bundle?) {
@ -160,12 +195,7 @@ constructor(
context.sendBroadcast(Intent(action)) context.sendBroadcast(Intent(action))
} }
override fun onStop() { private fun expandUidIntoCommand(uid: MediaSessionUID): PlaybackCommand? {
// Get the service to shut down with the ACTION_EXIT intent
context.sendBroadcast(Intent(PlaybackActions.ACTION_EXIT))
}
private fun expandIntoCommand(uid: MediaSessionUID): PlaybackCommand? {
val music: Music val music: Music
var parent: MusicParent? = null var parent: MusicParent? = null
when (uid) { 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?) = private fun inferSongFromParent(music: Song, parent: MusicParent?) =
when (parent) { when (parent) {
is Album -> commandFactory.songFromAlbum(music, ShuffleMode.IMPLICIT) 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 KEY_QUEUE_POS = BuildConfig.APPLICATION_ID + ".metadata.QUEUE_POS"
const val ACTIONS = const val ACTIONS =
PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID or PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID or
PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH or
PlaybackStateCompat.ACTION_PLAY or PlaybackStateCompat.ACTION_PLAY or
PlaybackStateCompat.ACTION_PAUSE or PlaybackStateCompat.ACTION_PAUSE or
PlaybackStateCompat.ACTION_PLAY_PAUSE or PlaybackStateCompat.ACTION_PLAY_PAUSE or