From b2e7c1eb50f7cef89b380b43ed156dc0be3fe47f Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Wed, 28 Aug 2024 15:52:42 -0600 Subject: [PATCH] playback: basic play from search functionality --- .../playback/service/MediaSessionHolder.kt | 14 +- .../playback/service/MediaSessionInterface.kt | 122 +++++++++++++++--- 2 files changed, 111 insertions(+), 25 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/playback/service/MediaSessionHolder.kt b/app/src/main/java/org/oxycblt/auxio/playback/service/MediaSessionHolder.kt index acc940110..e133388b4 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/service/MediaSessionHolder.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/service/MediaSessionHolder.kt @@ -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. diff --git a/app/src/main/java/org/oxycblt/auxio/playback/service/MediaSessionInterface.kt b/app/src/main/java/org/oxycblt/auxio/playback/service/MediaSessionInterface.kt index 25c3078f5..66805f801 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/service/MediaSessionInterface.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/service/MediaSessionInterface.kt @@ -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 Collection.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