playback: basic play from search functionality
This commit is contained in:
parent
fda4548515
commit
b2e7c1eb50
2 changed files with 111 additions and 25 deletions
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue