music: rework id system
Completely rework the ID system to pave the way to MusicBrainz ID support and greatly increase ID integrity in general. This changeset removes the old ID field, an emulation of a polynomial hash that was used in all items, and replaces it with a new type called UID that is specific to Music. Other types just use plain equals now, and most instances of "id" to check for equality in the app have either been inlined into an equals override or removed outright. The new UID format is as follows: datatype/format:uuid Datatype is a tag that is just the lowercase tag name. For example, "song". Format is the program that created the UID. auxio will be an md5 hash, and musicbrainz will the a musicbrainz ID extracted from a file. UUID is the uuid itself. This is much more reliable and extendable than the old ID format. This will also be the last time I break compat with old ID formats. From now on, a legacy UID field will not be included to enable backwards compat, when the time comes for a breaking change.
This commit is contained in:
parent
457013d047
commit
48ad45e4c3
31 changed files with 443 additions and 360 deletions
|
@ -2,6 +2,9 @@
|
||||||
|
|
||||||
## dev
|
## dev
|
||||||
|
|
||||||
|
#### What's New
|
||||||
|
- Reworked music hashing to be even more reliable (Will wipe playback state)
|
||||||
|
|
||||||
#### What's Fixed
|
#### What's Fixed
|
||||||
- Fixed issue where the scroll popup would not display correctly in landscape mode [#230]
|
- Fixed issue where the scroll popup would not display correctly in landscape mode [#230]
|
||||||
- Fixed issue where the playback progress would continue in the notification even if
|
- Fixed issue where the playback progress would continue in the notification even if
|
||||||
|
|
|
@ -3,6 +3,7 @@ plugins {
|
||||||
id "kotlin-android"
|
id "kotlin-android"
|
||||||
id "androidx.navigation.safeargs.kotlin"
|
id "androidx.navigation.safeargs.kotlin"
|
||||||
id "com.diffplug.spotless"
|
id "com.diffplug.spotless"
|
||||||
|
id "kotlin-parcelize"
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
|
|
@ -225,7 +225,7 @@ class MainFragment :
|
||||||
findNavController().navigate(MainFragmentDirections.actionShowAbout())
|
findNavController().navigate(MainFragmentDirections.actionShowAbout())
|
||||||
is MainNavigationAction.SongDetails ->
|
is MainNavigationAction.SongDetails ->
|
||||||
findNavController()
|
findNavController()
|
||||||
.navigate(MainFragmentDirections.actionShowDetails(action.song.id))
|
.navigate(MainFragmentDirections.actionShowDetails(action.song.uid))
|
||||||
}
|
}
|
||||||
|
|
||||||
navModel.finishMainNavigation()
|
navModel.finishMainNavigation()
|
||||||
|
|
|
@ -74,7 +74,7 @@ class AlbumDetailFragment :
|
||||||
override fun onCreateBinding(inflater: LayoutInflater) = FragmentDetailBinding.inflate(inflater)
|
override fun onCreateBinding(inflater: LayoutInflater) = FragmentDetailBinding.inflate(inflater)
|
||||||
|
|
||||||
override fun onBindingCreated(binding: FragmentDetailBinding, savedInstanceState: Bundle?) {
|
override fun onBindingCreated(binding: FragmentDetailBinding, savedInstanceState: Bundle?) {
|
||||||
detailModel.setAlbumId(args.albumId)
|
detailModel.setAlbumUid(args.albumUid)
|
||||||
|
|
||||||
binding.detailToolbar.apply {
|
binding.detailToolbar.apply {
|
||||||
inflateMenu(R.menu.menu_album_detail)
|
inflateMenu(R.menu.menu_album_detail)
|
||||||
|
@ -169,7 +169,7 @@ class AlbumDetailFragment :
|
||||||
findNavController()
|
findNavController()
|
||||||
.navigate(
|
.navigate(
|
||||||
AlbumDetailFragmentDirections.actionShowArtist(
|
AlbumDetailFragmentDirections.actionShowArtist(
|
||||||
unlikelyToBeNull(detailModel.currentAlbum.value).artist.id))
|
unlikelyToBeNull(detailModel.currentAlbum.value).artist.uid))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleItemChange(album: Album?) {
|
private fun handleItemChange(album: Album?) {
|
||||||
|
@ -187,28 +187,28 @@ class AlbumDetailFragment :
|
||||||
// Songs should be scrolled to if the album matches, or a new detail
|
// Songs should be scrolled to if the album matches, or a new detail
|
||||||
// fragment should be launched otherwise.
|
// fragment should be launched otherwise.
|
||||||
is Song -> {
|
is Song -> {
|
||||||
if (unlikelyToBeNull(detailModel.currentAlbum.value).id == item.album.id) {
|
if (unlikelyToBeNull(detailModel.currentAlbum.value) == item.album) {
|
||||||
logD("Navigating to a song in this album")
|
logD("Navigating to a song in this album")
|
||||||
scrollToItem(item.id)
|
scrollToItem(item)
|
||||||
navModel.finishExploreNavigation()
|
navModel.finishExploreNavigation()
|
||||||
} else {
|
} else {
|
||||||
logD("Navigating to another album")
|
logD("Navigating to another album")
|
||||||
findNavController()
|
findNavController()
|
||||||
.navigate(AlbumDetailFragmentDirections.actionShowAlbum(item.album.id))
|
.navigate(AlbumDetailFragmentDirections.actionShowAlbum(item.album.uid))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the album matches, no need to do anything. Otherwise launch a new
|
// If the album matches, no need to do anything. Otherwise launch a new
|
||||||
// detail fragment.
|
// detail fragment.
|
||||||
is Album -> {
|
is Album -> {
|
||||||
if (unlikelyToBeNull(detailModel.currentAlbum.value).id == item.id) {
|
if (unlikelyToBeNull(detailModel.currentAlbum.value) == item) {
|
||||||
logD("Navigating to the top of this album")
|
logD("Navigating to the top of this album")
|
||||||
binding.detailRecycler.scrollToPosition(0)
|
binding.detailRecycler.scrollToPosition(0)
|
||||||
navModel.finishExploreNavigation()
|
navModel.finishExploreNavigation()
|
||||||
} else {
|
} else {
|
||||||
logD("Navigating to another album")
|
logD("Navigating to another album")
|
||||||
findNavController()
|
findNavController()
|
||||||
.navigate(AlbumDetailFragmentDirections.actionShowAlbum(item.id))
|
.navigate(AlbumDetailFragmentDirections.actionShowAlbum(item.uid))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -216,17 +216,17 @@ class AlbumDetailFragment :
|
||||||
is Artist -> {
|
is Artist -> {
|
||||||
logD("Navigating to another artist")
|
logD("Navigating to another artist")
|
||||||
findNavController()
|
findNavController()
|
||||||
.navigate(AlbumDetailFragmentDirections.actionShowArtist(item.id))
|
.navigate(AlbumDetailFragmentDirections.actionShowArtist(item.uid))
|
||||||
}
|
}
|
||||||
null -> {}
|
null -> {}
|
||||||
else -> error("Unexpected navigation item ${item::class.java}")
|
else -> error("Unexpected navigation item ${item::class.java}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Scroll to an song using its [id]. */
|
/** Scroll to an [song]. */
|
||||||
private fun scrollToItem(id: Long) {
|
private fun scrollToItem(song: Song) {
|
||||||
// Calculate where the item for the currently played song is
|
// Calculate where the item for the currently played song is
|
||||||
val pos = detailModel.albumData.value.indexOfFirst { it.id == id && it is Song }
|
val pos = detailModel.albumData.value.indexOf(song)
|
||||||
|
|
||||||
if (pos != -1) {
|
if (pos != -1) {
|
||||||
val binding = requireBinding()
|
val binding = requireBinding()
|
||||||
|
@ -255,7 +255,7 @@ class AlbumDetailFragment :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parent is Album && parent.id == unlikelyToBeNull(detailModel.currentAlbum.value).id) {
|
if (parent is Album && parent == unlikelyToBeNull(detailModel.currentAlbum.value)) {
|
||||||
detailAdapter.updateIndicator(song, isPlaying)
|
detailAdapter.updateIndicator(song, isPlaying)
|
||||||
} else {
|
} else {
|
||||||
// Clear the ViewHolders if the mode isn't ALL_SONGS
|
// Clear the ViewHolders if the mode isn't ALL_SONGS
|
||||||
|
@ -279,8 +279,6 @@ class AlbumDetailFragment :
|
||||||
boxStart: Int,
|
boxStart: Int,
|
||||||
boxEnd: Int,
|
boxEnd: Int,
|
||||||
snapPreference: Int
|
snapPreference: Int
|
||||||
): Int {
|
): Int = (boxStart + (boxEnd - boxStart) / 2) - (viewStart + (viewEnd - viewStart) / 2)
|
||||||
return (boxStart + (boxEnd - boxStart) / 2) - (viewStart + (viewEnd - viewStart) / 2)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -69,7 +69,7 @@ class ArtistDetailFragment :
|
||||||
override fun onCreateBinding(inflater: LayoutInflater) = FragmentDetailBinding.inflate(inflater)
|
override fun onCreateBinding(inflater: LayoutInflater) = FragmentDetailBinding.inflate(inflater)
|
||||||
|
|
||||||
override fun onBindingCreated(binding: FragmentDetailBinding, savedInstanceState: Bundle?) {
|
override fun onBindingCreated(binding: FragmentDetailBinding, savedInstanceState: Bundle?) {
|
||||||
detailModel.setArtistId(args.artistId)
|
detailModel.setArtistUid(args.artistUid)
|
||||||
|
|
||||||
binding.detailToolbar.apply {
|
binding.detailToolbar.apply {
|
||||||
inflateMenu(R.menu.menu_genre_artist_detail)
|
inflateMenu(R.menu.menu_genre_artist_detail)
|
||||||
|
@ -177,22 +177,22 @@ class ArtistDetailFragment :
|
||||||
is Song -> {
|
is Song -> {
|
||||||
logD("Navigating to another album")
|
logD("Navigating to another album")
|
||||||
findNavController()
|
findNavController()
|
||||||
.navigate(ArtistDetailFragmentDirections.actionShowAlbum(item.album.id))
|
.navigate(ArtistDetailFragmentDirections.actionShowAlbum(item.album.uid))
|
||||||
}
|
}
|
||||||
is Album -> {
|
is Album -> {
|
||||||
logD("Navigating to another album")
|
logD("Navigating to another album")
|
||||||
findNavController()
|
findNavController()
|
||||||
.navigate(ArtistDetailFragmentDirections.actionShowAlbum(item.id))
|
.navigate(ArtistDetailFragmentDirections.actionShowAlbum(item.uid))
|
||||||
}
|
}
|
||||||
is Artist -> {
|
is Artist -> {
|
||||||
if (item.id == detailModel.currentArtist.value?.id) {
|
if (item.uid == detailModel.currentArtist.value?.uid) {
|
||||||
logD("Navigating to the top of this artist")
|
logD("Navigating to the top of this artist")
|
||||||
binding.detailRecycler.scrollToPosition(0)
|
binding.detailRecycler.scrollToPosition(0)
|
||||||
navModel.finishExploreNavigation()
|
navModel.finishExploreNavigation()
|
||||||
} else {
|
} else {
|
||||||
logD("Navigating to another artist")
|
logD("Navigating to another artist")
|
||||||
findNavController()
|
findNavController()
|
||||||
.navigate(ArtistDetailFragmentDirections.actionShowArtist(item.id))
|
.navigate(ArtistDetailFragmentDirections.actionShowArtist(item.uid))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
null -> {}
|
null -> {}
|
||||||
|
@ -207,7 +207,7 @@ class ArtistDetailFragment :
|
||||||
item = parent
|
item = parent
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parent is Artist && parent.id == unlikelyToBeNull(detailModel.currentArtist.value).id) {
|
if (parent is Artist && parent == unlikelyToBeNull(detailModel.currentArtist.value)) {
|
||||||
item = song
|
item = song
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -109,10 +109,10 @@ class DetailViewModel(application: Application) :
|
||||||
|
|
||||||
private val songGuard = TaskGuard()
|
private val songGuard = TaskGuard()
|
||||||
|
|
||||||
fun setSongId(id: Long) {
|
fun setSongUid(uid: Music.UID) {
|
||||||
if (_currentSong.value?.run { song.id } == id) return
|
if (_currentSong.value?.run { song.uid } == uid) return
|
||||||
val library = unlikelyToBeNull(musicStore.library)
|
val library = unlikelyToBeNull(musicStore.library)
|
||||||
val song = requireNotNull(library.findSongById(id)) { "Invalid song id provided" }
|
val song = requireNotNull(library.find<Song>(uid)) { "Invalid song id provided" }
|
||||||
generateDetailSong(song)
|
generateDetailSong(song)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -121,27 +121,28 @@ class DetailViewModel(application: Application) :
|
||||||
_currentSong.value = null
|
_currentSong.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setAlbumId(id: Long) {
|
fun setAlbumUid(uid: Music.UID) {
|
||||||
if (_currentAlbum.value?.id == id) return
|
if (_currentAlbum.value?.uid == uid) return
|
||||||
val library = unlikelyToBeNull(musicStore.library)
|
val library = unlikelyToBeNull(musicStore.library)
|
||||||
val album = requireNotNull(library.findAlbumById(id)) { "Invalid album id provided " }
|
val album = requireNotNull(library.find<Album>(uid)) { "Invalid album id provided " }
|
||||||
|
|
||||||
_currentAlbum.value = album
|
_currentAlbum.value = album
|
||||||
refreshAlbumData(album)
|
refreshAlbumData(album)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setArtistId(id: Long) {
|
fun setArtistUid(uid: Music.UID) {
|
||||||
if (_currentArtist.value?.id == id) return
|
logD(uid)
|
||||||
|
if (_currentArtist.value?.uid == uid) return
|
||||||
val library = unlikelyToBeNull(musicStore.library)
|
val library = unlikelyToBeNull(musicStore.library)
|
||||||
val artist = requireNotNull(library.findArtistById(id)) { "Invalid artist id provided" }
|
val artist = requireNotNull(library.find<Artist>(uid)) { "Invalid artist id provided" }
|
||||||
_currentArtist.value = artist
|
_currentArtist.value = artist
|
||||||
refreshArtistData(artist)
|
refreshArtistData(artist)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setGenreId(id: Long) {
|
fun setGenreUid(uid: Music.UID) {
|
||||||
if (_currentGenre.value?.id == id) return
|
if (_currentGenre.value?.uid == uid) return
|
||||||
val library = unlikelyToBeNull(musicStore.library)
|
val library = unlikelyToBeNull(musicStore.library)
|
||||||
val genre = requireNotNull(library.findGenreById(id)) { "Invalid genre id provided" }
|
val genre = requireNotNull(library.find<Genre>(uid)) { "Invalid genre id provided" }
|
||||||
_currentGenre.value = genre
|
_currentGenre.value = genre
|
||||||
refreshGenreData(genre)
|
refreshGenreData(genre)
|
||||||
}
|
}
|
||||||
|
@ -318,12 +319,6 @@ class DetailViewModel(application: Application) :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class SortHeader(@StringRes val string: Int) : Item() {
|
data class SortHeader(@StringRes val string: Int) : Item
|
||||||
override val id: Long
|
|
||||||
get() = string.toLong()
|
|
||||||
}
|
|
||||||
|
|
||||||
data class DiscHeader(val disc: Int) : Item() {
|
data class DiscHeader(val disc: Int) : Item
|
||||||
override val id: Long
|
|
||||||
get() = disc.toLong()
|
|
||||||
}
|
|
||||||
|
|
|
@ -70,7 +70,7 @@ class GenreDetailFragment :
|
||||||
override fun onCreateBinding(inflater: LayoutInflater) = FragmentDetailBinding.inflate(inflater)
|
override fun onCreateBinding(inflater: LayoutInflater) = FragmentDetailBinding.inflate(inflater)
|
||||||
|
|
||||||
override fun onBindingCreated(binding: FragmentDetailBinding, savedInstanceState: Bundle?) {
|
override fun onBindingCreated(binding: FragmentDetailBinding, savedInstanceState: Bundle?) {
|
||||||
detailModel.setGenreId(args.genreId)
|
detailModel.setGenreUid(args.genreUid)
|
||||||
|
|
||||||
binding.detailToolbar.apply {
|
binding.detailToolbar.apply {
|
||||||
inflateMenu(R.menu.menu_genre_artist_detail)
|
inflateMenu(R.menu.menu_genre_artist_detail)
|
||||||
|
@ -125,6 +125,7 @@ class GenreDetailFragment :
|
||||||
override fun onOpenMenu(item: Item, anchor: View) {
|
override fun onOpenMenu(item: Item, anchor: View) {
|
||||||
if (item is Song) {
|
if (item is Song) {
|
||||||
musicMenu(anchor, R.menu.menu_song_actions, item)
|
musicMenu(anchor, R.menu.menu_song_actions, item)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
error("Unexpected datatype when opening menu: ${item::class.java}")
|
error("Unexpected datatype when opening menu: ${item::class.java}")
|
||||||
|
@ -170,16 +171,17 @@ class GenreDetailFragment :
|
||||||
is Song -> {
|
is Song -> {
|
||||||
logD("Navigating to another song")
|
logD("Navigating to another song")
|
||||||
findNavController()
|
findNavController()
|
||||||
.navigate(GenreDetailFragmentDirections.actionShowAlbum(item.album.id))
|
.navigate(GenreDetailFragmentDirections.actionShowAlbum(item.album.uid))
|
||||||
}
|
}
|
||||||
is Album -> {
|
is Album -> {
|
||||||
logD("Navigating to another album")
|
logD("Navigating to another album")
|
||||||
findNavController().navigate(GenreDetailFragmentDirections.actionShowAlbum(item.id))
|
findNavController()
|
||||||
|
.navigate(GenreDetailFragmentDirections.actionShowAlbum(item.uid))
|
||||||
}
|
}
|
||||||
is Artist -> {
|
is Artist -> {
|
||||||
logD("Navigating to another artist")
|
logD("Navigating to another artist")
|
||||||
findNavController()
|
findNavController()
|
||||||
.navigate(GenreDetailFragmentDirections.actionShowArtist(item.id))
|
.navigate(GenreDetailFragmentDirections.actionShowArtist(item.uid))
|
||||||
}
|
}
|
||||||
is Genre -> {
|
is Genre -> {
|
||||||
navModel.finishExploreNavigation()
|
navModel.finishExploreNavigation()
|
||||||
|
@ -189,7 +191,7 @@ class GenreDetailFragment :
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
|
private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
|
||||||
if (parent is Genre && parent.id == unlikelyToBeNull(detailModel.currentGenre.value).id) {
|
if (parent is Genre && parent == unlikelyToBeNull(detailModel.currentGenre.value)) {
|
||||||
detailAdapter.updateIndicator(song, isPlaying)
|
detailAdapter.updateIndicator(song, isPlaying)
|
||||||
} else {
|
} else {
|
||||||
// Ignore song playback not from the genre
|
// Ignore song playback not from the genre
|
||||||
|
|
|
@ -50,7 +50,7 @@ class SongDetailDialog : ViewBindingDialogFragment<DialogSongDetailBinding>() {
|
||||||
|
|
||||||
override fun onBindingCreated(binding: DialogSongDetailBinding, savedInstanceState: Bundle?) {
|
override fun onBindingCreated(binding: DialogSongDetailBinding, savedInstanceState: Bundle?) {
|
||||||
super.onBindingCreated(binding, savedInstanceState)
|
super.onBindingCreated(binding, savedInstanceState)
|
||||||
detailModel.setSongId(args.songId)
|
detailModel.setSongUid(args.songUid)
|
||||||
collectImmediately(detailModel.currentSong, ::updateSong)
|
collectImmediately(detailModel.currentSong, ::updateSong)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -85,15 +85,15 @@ class AlbumDetailAdapter(private val listener: Listener) :
|
||||||
companion object {
|
companion object {
|
||||||
private val DIFFER =
|
private val DIFFER =
|
||||||
object : SimpleItemCallback<Item>() {
|
object : SimpleItemCallback<Item>() {
|
||||||
override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean {
|
override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean {
|
||||||
return when {
|
return when {
|
||||||
oldItem is Album && newItem is Album ->
|
oldItem is Album && newItem is Album ->
|
||||||
AlbumDetailViewHolder.DIFFER.areItemsTheSame(oldItem, newItem)
|
AlbumDetailViewHolder.DIFFER.areContentsTheSame(oldItem, newItem)
|
||||||
oldItem is DiscHeader && newItem is DiscHeader ->
|
oldItem is DiscHeader && newItem is DiscHeader ->
|
||||||
DiscHeaderViewHolder.DIFFER.areItemsTheSame(oldItem, newItem)
|
DiscHeaderViewHolder.DIFFER.areContentsTheSame(oldItem, newItem)
|
||||||
oldItem is Song && newItem is Song ->
|
oldItem is Song && newItem is Song ->
|
||||||
AlbumSongViewHolder.DIFFER.areItemsTheSame(oldItem, newItem)
|
AlbumSongViewHolder.DIFFER.areContentsTheSame(oldItem, newItem)
|
||||||
else -> DetailAdapter.DIFFER.areItemsTheSame(oldItem, newItem)
|
else -> DetailAdapter.DIFFER.areContentsTheSame(oldItem, newItem)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -142,7 +142,7 @@ private class AlbumDetailViewHolder private constructor(private val binding: Ite
|
||||||
|
|
||||||
val DIFFER =
|
val DIFFER =
|
||||||
object : SimpleItemCallback<Album>() {
|
object : SimpleItemCallback<Album>() {
|
||||||
override fun areItemsTheSame(oldItem: Album, newItem: Album) =
|
override fun areContentsTheSame(oldItem: Album, newItem: Album) =
|
||||||
oldItem.rawName == newItem.rawName &&
|
oldItem.rawName == newItem.rawName &&
|
||||||
oldItem.artist.rawName == newItem.artist.rawName &&
|
oldItem.artist.rawName == newItem.artist.rawName &&
|
||||||
oldItem.date == newItem.date &&
|
oldItem.date == newItem.date &&
|
||||||
|
@ -168,7 +168,7 @@ class DiscHeaderViewHolder(private val binding: ItemDiscHeaderBinding) :
|
||||||
|
|
||||||
val DIFFER =
|
val DIFFER =
|
||||||
object : SimpleItemCallback<DiscHeader>() {
|
object : SimpleItemCallback<DiscHeader>() {
|
||||||
override fun areItemsTheSame(oldItem: DiscHeader, newItem: DiscHeader) =
|
override fun areContentsTheSame(oldItem: DiscHeader, newItem: DiscHeader) =
|
||||||
oldItem.disc == newItem.disc
|
oldItem.disc == newItem.disc
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -216,7 +216,7 @@ private class AlbumSongViewHolder private constructor(private val binding: ItemA
|
||||||
|
|
||||||
val DIFFER =
|
val DIFFER =
|
||||||
object : SimpleItemCallback<Song>() {
|
object : SimpleItemCallback<Song>() {
|
||||||
override fun areItemsTheSame(oldItem: Song, newItem: Song) =
|
override fun areContentsTheSame(oldItem: Song, newItem: Song) =
|
||||||
oldItem.rawName == newItem.rawName && oldItem.durationMs == newItem.durationMs
|
oldItem.rawName == newItem.rawName && oldItem.durationMs == newItem.durationMs
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -87,15 +87,15 @@ class ArtistDetailAdapter(private val listener: Listener) :
|
||||||
companion object {
|
companion object {
|
||||||
private val DIFFER =
|
private val DIFFER =
|
||||||
object : SimpleItemCallback<Item>() {
|
object : SimpleItemCallback<Item>() {
|
||||||
override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean {
|
override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean {
|
||||||
return when {
|
return when {
|
||||||
oldItem is Artist && newItem is Artist ->
|
oldItem is Artist && newItem is Artist ->
|
||||||
ArtistDetailViewHolder.DIFFER.areItemsTheSame(oldItem, newItem)
|
ArtistDetailViewHolder.DIFFER.areContentsTheSame(oldItem, newItem)
|
||||||
oldItem is Album && newItem is Album ->
|
oldItem is Album && newItem is Album ->
|
||||||
ArtistAlbumViewHolder.DIFFER.areItemsTheSame(oldItem, newItem)
|
ArtistAlbumViewHolder.DIFFER.areContentsTheSame(oldItem, newItem)
|
||||||
oldItem is Song && newItem is Song ->
|
oldItem is Song && newItem is Song ->
|
||||||
ArtistSongViewHolder.DIFFER.areItemsTheSame(oldItem, newItem)
|
ArtistSongViewHolder.DIFFER.areContentsTheSame(oldItem, newItem)
|
||||||
else -> DetailAdapter.DIFFER.areItemsTheSame(oldItem, newItem)
|
else -> DetailAdapter.DIFFER.areContentsTheSame(oldItem, newItem)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -112,7 +112,7 @@ private class ArtistDetailViewHolder private constructor(private val binding: It
|
||||||
|
|
||||||
// Get the genre that corresponds to the most songs in this artist, which would be
|
// Get the genre that corresponds to the most songs in this artist, which would be
|
||||||
// the most "Prominent" genre.
|
// the most "Prominent" genre.
|
||||||
var genresByAmount = mutableMapOf<Genre, Int>()
|
val genresByAmount = mutableMapOf<Genre, Int>()
|
||||||
for (song in item.songs) {
|
for (song in item.songs) {
|
||||||
for (genre in song.genres) {
|
for (genre in song.genres) {
|
||||||
genresByAmount[genre] = genresByAmount[genre]?.inc() ?: 1
|
genresByAmount[genre] = genresByAmount[genre]?.inc() ?: 1
|
||||||
|
@ -172,7 +172,7 @@ private constructor(
|
||||||
|
|
||||||
val DIFFER =
|
val DIFFER =
|
||||||
object : SimpleItemCallback<Album>() {
|
object : SimpleItemCallback<Album>() {
|
||||||
override fun areItemsTheSame(oldItem: Album, newItem: Album) =
|
override fun areContentsTheSame(oldItem: Album, newItem: Album) =
|
||||||
oldItem.rawName == newItem.rawName && oldItem.date == newItem.date
|
oldItem.rawName == newItem.rawName && oldItem.date == newItem.date
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -207,7 +207,7 @@ private constructor(
|
||||||
|
|
||||||
val DIFFER =
|
val DIFFER =
|
||||||
object : SimpleItemCallback<Song>() {
|
object : SimpleItemCallback<Song>() {
|
||||||
override fun areItemsTheSame(oldItem: Song, newItem: Song) =
|
override fun areContentsTheSame(oldItem: Song, newItem: Song) =
|
||||||
oldItem.rawName == newItem.rawName &&
|
oldItem.rawName == newItem.rawName &&
|
||||||
oldItem.album.rawName == newItem.album.rawName
|
oldItem.album.rawName == newItem.album.rawName
|
||||||
}
|
}
|
||||||
|
|
|
@ -95,12 +95,12 @@ abstract class DetailAdapter<L : DetailAdapter.Listener>(
|
||||||
companion object {
|
companion object {
|
||||||
val DIFFER =
|
val DIFFER =
|
||||||
object : SimpleItemCallback<Item>() {
|
object : SimpleItemCallback<Item>() {
|
||||||
override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean {
|
override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean {
|
||||||
return when {
|
return when {
|
||||||
oldItem is Header && newItem is Header ->
|
oldItem is Header && newItem is Header ->
|
||||||
HeaderViewHolder.DIFFER.areItemsTheSame(oldItem, newItem)
|
HeaderViewHolder.DIFFER.areContentsTheSame(oldItem, newItem)
|
||||||
oldItem is SortHeader && newItem is SortHeader ->
|
oldItem is SortHeader && newItem is SortHeader ->
|
||||||
SortHeaderViewHolder.DIFFER.areItemsTheSame(oldItem, newItem)
|
SortHeaderViewHolder.DIFFER.areContentsTheSame(oldItem, newItem)
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -132,7 +132,7 @@ class SortHeaderViewHolder(private val binding: ItemSortHeaderBinding) :
|
||||||
|
|
||||||
val DIFFER =
|
val DIFFER =
|
||||||
object : SimpleItemCallback<SortHeader>() {
|
object : SimpleItemCallback<SortHeader>() {
|
||||||
override fun areItemsTheSame(oldItem: SortHeader, newItem: SortHeader) =
|
override fun areContentsTheSame(oldItem: SortHeader, newItem: SortHeader) =
|
||||||
oldItem.string == newItem.string
|
oldItem.string == newItem.string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -79,12 +79,12 @@ class GenreDetailAdapter(private val listener: Listener) :
|
||||||
companion object {
|
companion object {
|
||||||
val DIFFER =
|
val DIFFER =
|
||||||
object : SimpleItemCallback<Item>() {
|
object : SimpleItemCallback<Item>() {
|
||||||
override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean {
|
override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean {
|
||||||
return when {
|
return when {
|
||||||
oldItem is Genre && newItem is Genre ->
|
oldItem is Genre && newItem is Genre ->
|
||||||
GenreDetailViewHolder.DIFFER.areItemsTheSame(oldItem, newItem)
|
GenreDetailViewHolder.DIFFER.areContentsTheSame(oldItem, newItem)
|
||||||
oldItem is Song && newItem is Song ->
|
oldItem is Song && newItem is Song ->
|
||||||
SongViewHolder.DIFFER.areItemsTheSame(oldItem, newItem)
|
SongViewHolder.DIFFER.areContentsTheSame(oldItem, newItem)
|
||||||
else -> DetailAdapter.DIFFER.areContentsTheSame(oldItem, newItem)
|
else -> DetailAdapter.DIFFER.areContentsTheSame(oldItem, newItem)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -113,7 +113,7 @@ private class GenreDetailViewHolder private constructor(private val binding: Ite
|
||||||
|
|
||||||
val DIFFER =
|
val DIFFER =
|
||||||
object : SimpleItemCallback<Genre>() {
|
object : SimpleItemCallback<Genre>() {
|
||||||
override fun areItemsTheSame(oldItem: Genre, newItem: Genre) =
|
override fun areContentsTheSame(oldItem: Genre, newItem: Genre) =
|
||||||
oldItem.rawName == newItem.rawName &&
|
oldItem.rawName == newItem.rawName &&
|
||||||
oldItem.songs.size == newItem.songs.size &&
|
oldItem.songs.size == newItem.songs.size &&
|
||||||
oldItem.durationMs == newItem.durationMs
|
oldItem.durationMs == newItem.durationMs
|
||||||
|
|
|
@ -372,10 +372,10 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
|
||||||
private fun handleNavigation(item: Music?) {
|
private fun handleNavigation(item: Music?) {
|
||||||
val action =
|
val action =
|
||||||
when (item) {
|
when (item) {
|
||||||
is Song -> HomeFragmentDirections.actionShowAlbum(item.album.id)
|
is Song -> HomeFragmentDirections.actionShowAlbum(item.album.uid)
|
||||||
is Album -> HomeFragmentDirections.actionShowAlbum(item.id)
|
is Album -> HomeFragmentDirections.actionShowAlbum(item.uid)
|
||||||
is Artist -> HomeFragmentDirections.actionShowArtist(item.id)
|
is Artist -> HomeFragmentDirections.actionShowArtist(item.uid.also { logD(it) })
|
||||||
is Genre -> HomeFragmentDirections.actionShowGenre(item.id)
|
is Genre -> HomeFragmentDirections.actionShowGenre(item.uid)
|
||||||
else -> return
|
else -> return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -81,6 +81,7 @@ class ArtistListFragment : HomeListFragment<Artist>() {
|
||||||
override fun onOpenMenu(item: Item, anchor: View) {
|
override fun onOpenMenu(item: Item, anchor: View) {
|
||||||
if (item is Artist) {
|
if (item is Artist) {
|
||||||
musicMenu(anchor, R.menu.menu_genre_artist_actions, item)
|
musicMenu(anchor, R.menu.menu_genre_artist_actions, item)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
error("Unexpected datatype when opening menu: ${item::class.java}")
|
error("Unexpected datatype when opening menu: ${item::class.java}")
|
||||||
|
|
|
@ -39,14 +39,13 @@ import org.oxycblt.auxio.ui.Sort
|
||||||
|
|
||||||
/** A basic keyer for music data. */
|
/** A basic keyer for music data. */
|
||||||
class MusicKeyer : Keyer<Music> {
|
class MusicKeyer : Keyer<Music> {
|
||||||
override fun key(data: Music, options: Options): String {
|
override fun key(data: Music, options: Options) =
|
||||||
return if (data is Song) {
|
if (data is Song) {
|
||||||
// Group up song covers with album covers for better caching
|
// Group up song covers with album covers for better caching
|
||||||
key(data.album, options)
|
data.album.uid.toString()
|
||||||
} else {
|
} else {
|
||||||
"${data::class.simpleName}: ${data.id}"
|
data.uid.toString()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -65,9 +64,8 @@ private constructor(private val context: Context, private val album: Album) : Ba
|
||||||
}
|
}
|
||||||
|
|
||||||
class SongFactory : Fetcher.Factory<Song> {
|
class SongFactory : Fetcher.Factory<Song> {
|
||||||
override fun create(data: Song, options: Options, imageLoader: ImageLoader): Fetcher {
|
override fun create(data: Song, options: Options, imageLoader: ImageLoader) =
|
||||||
return AlbumCoverFetcher(options.context, data.album)
|
AlbumCoverFetcher(options.context, data.album)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class AlbumFactory : Fetcher.Factory<Album> {
|
class AlbumFactory : Fetcher.Factory<Album> {
|
||||||
|
@ -114,7 +112,7 @@ private constructor(
|
||||||
// whenever possible. So, if there are more than four distinct artists in a genre, make
|
// whenever possible. So, if there are more than four distinct artists in a genre, make
|
||||||
// it so that one artist only adds one album cover to the mosaic. Otherwise, use order
|
// it so that one artist only adds one album cover to the mosaic. Otherwise, use order
|
||||||
// albums normally.
|
// albums normally.
|
||||||
val artists = genre.songs.groupBy { it.album.artist.id }.keys
|
val artists = genre.songs.groupBy { it.album.artist }.keys
|
||||||
val albums =
|
val albums =
|
||||||
Sort(Sort.Mode.ByName, true).albums(genre.songs.groupBy { it.album }.keys).run {
|
Sort(Sort.Mode.ByName, true).albums(genre.songs.groupBy { it.album }.keys).run {
|
||||||
if (artists.size > 4) {
|
if (artists.size > 4) {
|
||||||
|
|
|
@ -20,20 +20,30 @@
|
||||||
package org.oxycblt.auxio.music
|
package org.oxycblt.auxio.music
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.os.Parcelable
|
||||||
|
import java.security.MessageDigest
|
||||||
|
import java.util.UUID
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
import kotlin.reflect.KClass
|
||||||
|
import kotlinx.parcelize.IgnoredOnParcel
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
import org.oxycblt.auxio.BuildConfig
|
import org.oxycblt.auxio.BuildConfig
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.ui.Sort
|
import org.oxycblt.auxio.ui.Sort
|
||||||
import org.oxycblt.auxio.ui.recycler.Item
|
import org.oxycblt.auxio.ui.recycler.Item
|
||||||
import org.oxycblt.auxio.util.inRangeOrNull
|
import org.oxycblt.auxio.util.inRangeOrNull
|
||||||
|
import org.oxycblt.auxio.util.logE
|
||||||
|
import org.oxycblt.auxio.util.msToSecs
|
||||||
import org.oxycblt.auxio.util.nonZeroOrNull
|
import org.oxycblt.auxio.util.nonZeroOrNull
|
||||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||||
|
|
||||||
// --- MUSIC MODELS ---
|
// --- MUSIC MODELS ---
|
||||||
|
|
||||||
/** [Item] variant that represents a music item. */
|
/** [Item] variant that represents a music item. */
|
||||||
sealed class Music : Item() {
|
sealed class Music : Item {
|
||||||
|
abstract val uid: UID
|
||||||
|
|
||||||
/** The raw name of this item. Null if unknown. */
|
/** The raw name of this item. Null if unknown. */
|
||||||
abstract val rawName: String?
|
abstract val rawName: String?
|
||||||
|
|
||||||
|
@ -52,6 +62,106 @@ sealed class Music : Item() {
|
||||||
* become Unknown Artist, (124) would become its proper genre name, etc.
|
* become Unknown Artist, (124) would become its proper genre name, etc.
|
||||||
*/
|
*/
|
||||||
abstract fun resolveName(context: Context): String
|
abstract fun resolveName(context: Context): String
|
||||||
|
|
||||||
|
// Equality is based on UIDs, as some items (Especially artists) can have identical
|
||||||
|
// properties (Name) yet non-identical UIDs due to MusicBrainz tags
|
||||||
|
|
||||||
|
override fun hashCode() = uid.hashCode()
|
||||||
|
|
||||||
|
override fun equals(other: Any?) =
|
||||||
|
other is Music && javaClass == other.javaClass && uid == other.uid
|
||||||
|
|
||||||
|
/** A unique identifier for a piece of music. */
|
||||||
|
@Parcelize
|
||||||
|
class UID
|
||||||
|
private constructor(val datatype: String, val isMusicBrainz: Boolean, val uuid: UUID) :
|
||||||
|
Parcelable {
|
||||||
|
@IgnoredOnParcel private val hashCode: Int
|
||||||
|
|
||||||
|
init {
|
||||||
|
var result = datatype.hashCode()
|
||||||
|
result = 31 * result + isMusicBrainz.hashCode()
|
||||||
|
result = 31 * result + uuid.hashCode()
|
||||||
|
hashCode = result
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode() = hashCode
|
||||||
|
|
||||||
|
override fun equals(other: Any?) =
|
||||||
|
other is UID &&
|
||||||
|
datatype == other.datatype &&
|
||||||
|
isMusicBrainz == other.isMusicBrainz &&
|
||||||
|
uuid == other.uuid
|
||||||
|
|
||||||
|
override fun toString() = "$datatype/${if (isMusicBrainz) "musicbrainz" else "auxio"}:$uuid"
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromString(uid: String): UID? {
|
||||||
|
val split = uid.split(':', limit = 2)
|
||||||
|
if (split.size != 2) {
|
||||||
|
logE("Invalid uid: Malformed structure")
|
||||||
|
}
|
||||||
|
|
||||||
|
val namespace = split[0].split('/', limit = 2)
|
||||||
|
if (namespace.size != 2) {
|
||||||
|
logE("Invalid uid: Malformed namespace")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
val datatype = namespace[0]
|
||||||
|
val isMusicBrainz =
|
||||||
|
when (namespace[1]) {
|
||||||
|
"auxio" -> false
|
||||||
|
"musicbrainz" -> true
|
||||||
|
else -> {
|
||||||
|
logE("Invalid mid: Malformed uuid type")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val uuid =
|
||||||
|
try {
|
||||||
|
UUID.fromString(split[1])
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logE("Invalid uid: Malformed UUID")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return UID(datatype, isMusicBrainz, uuid)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hashed(clazz: KClass<*>, updates: MessageDigest.() -> Unit): UID {
|
||||||
|
val digest = MessageDigest.getInstance("MD5")
|
||||||
|
updates(digest)
|
||||||
|
|
||||||
|
val hash = digest.digest()
|
||||||
|
val uuid =
|
||||||
|
UUID(
|
||||||
|
hash[0]
|
||||||
|
.toLong()
|
||||||
|
.shl(56)
|
||||||
|
.or(hash[1].toLong().and(0xFF).shl(48))
|
||||||
|
.or(hash[2].toLong().and(0xFF).shl(40))
|
||||||
|
.or(hash[3].toLong().and(0xFF).shl(32))
|
||||||
|
.or(hash[4].toLong().and(0xFF).shl(24))
|
||||||
|
.or(hash[5].toLong().and(0xFF).shl(16))
|
||||||
|
.or(hash[6].toLong().and(0xFF).shl(8))
|
||||||
|
.or(hash[7].toLong().and(0xFF)),
|
||||||
|
hash[8]
|
||||||
|
.toLong()
|
||||||
|
.shl(56)
|
||||||
|
.or(hash[9].toLong().and(0xFF).shl(48))
|
||||||
|
.or(hash[10].toLong().and(0xFF).shl(40))
|
||||||
|
.or(hash[11].toLong().and(0xFF).shl(32))
|
||||||
|
.or(hash[12].toLong().and(0xFF).shl(24))
|
||||||
|
.or(hash[13].toLong().and(0xFF).shl(16))
|
||||||
|
.or(hash[14].toLong().and(0xFF).shl(8))
|
||||||
|
.or(hash[15].toLong().and(0xFF)))
|
||||||
|
|
||||||
|
return UID(unlikelyToBeNull(clazz.simpleName).lowercase(), false, uuid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -67,17 +177,8 @@ sealed class MusicParent : Music() {
|
||||||
* A song.
|
* A song.
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
data class Song(private val raw: Raw) : Music() {
|
class Song(private val raw: Raw) : Music() {
|
||||||
override val id: Long
|
override val uid: UID
|
||||||
get() {
|
|
||||||
var result = rawName.toMusicId()
|
|
||||||
result = 31 * result + album.rawName.toMusicId()
|
|
||||||
result = 31 * result + album.artist.rawName.toMusicId()
|
|
||||||
result = 31 * result + (track ?: 0)
|
|
||||||
result = 31 * result + (disc ?: 0)
|
|
||||||
result = 31 * result + durationMs
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
override val rawName = requireNotNull(raw.name) { "Invalid raw: No title" }
|
override val rawName = requireNotNull(raw.name) { "Invalid raw: No title" }
|
||||||
|
|
||||||
|
@ -113,20 +214,18 @@ data class Song(private val raw: Raw) : Music() {
|
||||||
val dateAdded = requireNotNull(raw.dateAdded) { "Invalid raw: No date added" }
|
val dateAdded = requireNotNull(raw.dateAdded) { "Invalid raw: No date added" }
|
||||||
|
|
||||||
/** The track number of this song in it's album.. */
|
/** The track number of this song in it's album.. */
|
||||||
val track = raw.track
|
val track: Int? = raw.track
|
||||||
|
|
||||||
/** The disc number of this song in it's album. */
|
/** The disc number of this song in it's album. */
|
||||||
val disc = raw.disc
|
val disc: Int? = raw.disc
|
||||||
|
|
||||||
private var _album: Album? = null
|
private var _album: Album? = null
|
||||||
/** The album of this song. */
|
/** The album of this song. */
|
||||||
val album: Album
|
val album: Album
|
||||||
get() = unlikelyToBeNull(_album)
|
get() = unlikelyToBeNull(_album)
|
||||||
|
|
||||||
private var _genres: MutableList<Genre> = mutableListOf()
|
// TODO: Multi-artist support
|
||||||
/** The genre of this song. Will be an "unknown genre" if the song does not have any. */
|
// private val _artists: MutableList<Artist> = mutableListOf()
|
||||||
val genres: List<Genre>
|
|
||||||
get() = _genres
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The raw artist name for this song in particular. First uses the artist tag, and then falls
|
* The raw artist name for this song in particular. First uses the artist tag, and then falls
|
||||||
|
@ -142,31 +241,29 @@ data class Song(private val raw: Raw) : Music() {
|
||||||
fun resolveIndividualArtistName(context: Context) =
|
fun resolveIndividualArtistName(context: Context) =
|
||||||
raw.artistName ?: album.artist.resolveName(context)
|
raw.artistName ?: album.artist.resolveName(context)
|
||||||
|
|
||||||
|
private val _genres: MutableList<Genre> = mutableListOf()
|
||||||
|
/** The genre of this song. Will be an "unknown genre" if the song does not have any. */
|
||||||
|
val genres: List<Genre>
|
||||||
|
get() = _genres
|
||||||
|
|
||||||
// --- INTERNAL FIELDS ---
|
// --- INTERNAL FIELDS ---
|
||||||
|
|
||||||
val _distinct =
|
val _rawAlbum =
|
||||||
rawName to
|
Album.Raw(
|
||||||
raw.albumName to
|
mediaStoreId = requireNotNull(raw.albumMediaStoreId) { "Invalid raw: No album id" },
|
||||||
raw.artistName to
|
name = requireNotNull(raw.albumName) { "Invalid raw: No album name" },
|
||||||
raw.albumArtistName to
|
sortName = raw.albumSortName,
|
||||||
raw.genreNames to
|
date = raw.date,
|
||||||
track to
|
releaseType = raw.albumReleaseType,
|
||||||
disc to
|
rawArtist =
|
||||||
durationMs
|
if (raw.albumArtistName != null) {
|
||||||
|
Artist.Raw(raw.albumArtistName, raw.albumArtistSortName)
|
||||||
val _rawAlbum: Album.Raw
|
} else {
|
||||||
|
Artist.Raw(raw.artistName, raw.artistSortName)
|
||||||
|
})
|
||||||
|
|
||||||
val _rawGenres = raw.genreNames?.map { Genre.Raw(it) } ?: listOf(Genre.Raw(null))
|
val _rawGenres = raw.genreNames?.map { Genre.Raw(it) } ?: listOf(Genre.Raw(null))
|
||||||
|
|
||||||
val _isMissingAlbum: Boolean
|
|
||||||
get() = _album == null
|
|
||||||
|
|
||||||
val _isMissingArtist: Boolean
|
|
||||||
get() = _album?._isMissingArtist ?: true
|
|
||||||
|
|
||||||
val _isMissingGenre: Boolean
|
|
||||||
get() = _genres.isEmpty()
|
|
||||||
|
|
||||||
fun _link(album: Album) {
|
fun _link(album: Album) {
|
||||||
_album = album
|
_album = album
|
||||||
}
|
}
|
||||||
|
@ -175,27 +272,28 @@ data class Song(private val raw: Raw) : Music() {
|
||||||
_genres.add(genre)
|
_genres.add(genre)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun _validate() {
|
||||||
|
(checkNotNull(_album) { "Malformed song: album is null" })._validate()
|
||||||
|
check(_genres.isNotEmpty()) { "Malformed song: genres are empty" }
|
||||||
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
val artistName: String?
|
// Generally, we calculate UIDs at the end since everything will definitely be initialized
|
||||||
val artistSortName: String?
|
// by now.
|
||||||
|
uid =
|
||||||
|
UID.hashed(this::class) {
|
||||||
|
update(rawName.lowercase())
|
||||||
|
update(_rawAlbum.name.lowercase())
|
||||||
|
update(_rawAlbum.date)
|
||||||
|
|
||||||
if (raw.albumArtistName != null) {
|
update(raw.artistName)
|
||||||
artistName = raw.albumArtistName
|
update(raw.albumArtistName)
|
||||||
artistSortName = raw.albumArtistSortName
|
|
||||||
} else {
|
|
||||||
artistName = raw.artistName
|
|
||||||
artistSortName = raw.artistSortName
|
|
||||||
}
|
|
||||||
|
|
||||||
_rawAlbum =
|
update(track)
|
||||||
Album.Raw(
|
update(disc)
|
||||||
mediaStoreId = raw.albumMediaStoreId,
|
|
||||||
name = raw.albumName,
|
update(durationMs.msToSecs())
|
||||||
sortName = raw.albumSortName,
|
}
|
||||||
date = raw.date,
|
|
||||||
releaseType = raw.albumReleaseType,
|
|
||||||
artistName,
|
|
||||||
artistSortName)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data class Raw(
|
data class Raw(
|
||||||
|
@ -225,43 +323,26 @@ data class Song(private val raw: Raw) : Music() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The data object for an album. */
|
/** The data object for an album. */
|
||||||
data class Album(private val raw: Raw, override val songs: List<Song>) : MusicParent() {
|
class Album(raw: Raw, override val songs: List<Song>) : MusicParent() {
|
||||||
init {
|
override val uid: UID
|
||||||
for (song in songs) {
|
|
||||||
song._link(this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override val id: Long
|
override val rawName = raw.name
|
||||||
get() {
|
|
||||||
var result = rawName.toMusicId()
|
|
||||||
result = 31 * result + artist.rawName.toMusicId()
|
|
||||||
result = 31 * result + (date?.year ?: 0)
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
override val rawName = requireNotNull(raw.name) { "Invalid raw: No name" }
|
|
||||||
|
|
||||||
override val rawSortName = raw.sortName
|
override val rawSortName = raw.sortName
|
||||||
|
|
||||||
override fun resolveName(context: Context) = rawName
|
override fun resolveName(context: Context) = rawName
|
||||||
|
|
||||||
/**
|
|
||||||
* The album cover URI for this album. Usually low quality, so using Coil is recommended
|
|
||||||
* instead.
|
|
||||||
*/
|
|
||||||
val coverUri = requireNotNull(raw.mediaStoreId) { "Invalid raw: No id" }.albumCoverUri
|
|
||||||
|
|
||||||
/** The latest date this album was released. */
|
/** The latest date this album was released. */
|
||||||
val date = raw.date
|
val date = raw.date
|
||||||
|
|
||||||
/** The release type of this album, such as "EP". Defaults to "Album". */
|
/** The release type of this album, such as "EP". Defaults to "Album". */
|
||||||
val releaseType = raw.releaseType ?: ReleaseType.Album(null)
|
val releaseType = raw.releaseType ?: ReleaseType.Album(null)
|
||||||
|
|
||||||
private var _artist: Artist? = null
|
/**
|
||||||
/** The parent artist of this album. */
|
* The album cover URI for this album. Usually low quality, so using Coil is recommended
|
||||||
val artist: Artist
|
* instead.
|
||||||
get() = unlikelyToBeNull(_artist)
|
*/
|
||||||
|
val coverUri = raw.mediaStoreId.albumCoverUri
|
||||||
|
|
||||||
/** The earliest date a song in this album was added. */
|
/** The earliest date a song in this album was added. */
|
||||||
val dateAdded = songs.minOf { it.dateAdded }
|
val dateAdded = songs.minOf { it.dateAdded }
|
||||||
|
@ -269,34 +350,50 @@ data class Album(private val raw: Raw, override val songs: List<Song>) : MusicPa
|
||||||
/** The total duration of songs in this album, in millis. */
|
/** The total duration of songs in this album, in millis. */
|
||||||
val durationMs = songs.sumOf { it.durationMs }
|
val durationMs = songs.sumOf { it.durationMs }
|
||||||
|
|
||||||
|
private var _artist: Artist? = null
|
||||||
|
/** The parent artist of this album. */
|
||||||
|
val artist: Artist
|
||||||
|
get() = unlikelyToBeNull(_artist)
|
||||||
|
|
||||||
// --- INTERNAL FIELDS ---
|
// --- INTERNAL FIELDS ---
|
||||||
|
|
||||||
val _rawArtist: Artist.Raw
|
val _rawArtist = raw.rawArtist
|
||||||
get() = Artist.Raw(name = raw.artistName, sortName = raw.artistSortName)
|
|
||||||
|
|
||||||
val _isMissingArtist: Boolean
|
|
||||||
get() = _artist == null
|
|
||||||
|
|
||||||
fun _link(artist: Artist) {
|
fun _link(artist: Artist) {
|
||||||
_artist = artist
|
_artist = artist
|
||||||
}
|
}
|
||||||
|
|
||||||
data class Raw(
|
fun _validate() {
|
||||||
val mediaStoreId: Long?,
|
checkNotNull(_artist) { "Invalid album: Artist is null " }
|
||||||
val name: String?,
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
uid =
|
||||||
|
UID.hashed(this::class) {
|
||||||
|
update(rawName)
|
||||||
|
update(_rawArtist.name)
|
||||||
|
update(date)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (song in songs) {
|
||||||
|
song._link(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Raw(
|
||||||
|
val mediaStoreId: Long,
|
||||||
|
val name: String,
|
||||||
val sortName: String?,
|
val sortName: String?,
|
||||||
val date: Date?,
|
val date: Date?,
|
||||||
val releaseType: ReleaseType?,
|
val releaseType: ReleaseType?,
|
||||||
val artistName: String?,
|
val rawArtist: Artist.Raw
|
||||||
val artistSortName: String?,
|
|
||||||
) {
|
) {
|
||||||
val groupingId: Long
|
private val hashCode = 31 * name.lowercase().hashCode() + rawArtist.hashCode()
|
||||||
|
|
||||||
init {
|
override fun hashCode() = hashCode
|
||||||
var groupingIdResult = artistName.toMusicId()
|
|
||||||
groupingIdResult = 31 * groupingIdResult + name.toMusicId()
|
override fun equals(other: Any?) =
|
||||||
groupingId = groupingIdResult
|
other is Raw && name.equals(other.name, true) && rawArtist == other.rawArtist
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -304,19 +401,12 @@ data class Album(private val raw: Raw, override val songs: List<Song>) : MusicPa
|
||||||
* The [MusicParent] for an *album* artist. This reflects a group of songs with the same(ish) album
|
* The [MusicParent] for an *album* artist. This reflects a group of songs with the same(ish) album
|
||||||
* artist or artist field, not the individual performers of an artist.
|
* artist or artist field, not the individual performers of an artist.
|
||||||
*/
|
*/
|
||||||
data class Artist(
|
class Artist(
|
||||||
private val raw: Raw,
|
raw: Raw,
|
||||||
/** The albums of this artist. */
|
/** The albums of this artist. */
|
||||||
val albums: List<Album>
|
val albums: List<Album>
|
||||||
) : MusicParent() {
|
) : MusicParent() {
|
||||||
init {
|
override val uid: UID
|
||||||
for (album in albums) {
|
|
||||||
album._link(this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override val id: Long
|
|
||||||
get() = rawName.toMusicId()
|
|
||||||
|
|
||||||
override val rawName = raw.name
|
override val rawName = raw.name
|
||||||
|
|
||||||
|
@ -329,59 +419,93 @@ data class Artist(
|
||||||
/** The total duration of songs in this artist, in millis. */
|
/** The total duration of songs in this artist, in millis. */
|
||||||
val durationMs = songs.sumOf { it.durationMs }
|
val durationMs = songs.sumOf { it.durationMs }
|
||||||
|
|
||||||
data class Raw(val name: String?, val sortName: String?) {
|
init {
|
||||||
val groupingId = name.toMusicId()
|
uid = UID.hashed(this::class) { update(rawName) }
|
||||||
|
|
||||||
|
for (album in albums) {
|
||||||
|
album._link(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Raw(val name: String?, val sortName: String?) {
|
||||||
|
private val hashCode = name?.lowercase().hashCode()
|
||||||
|
|
||||||
|
override fun hashCode() = hashCode
|
||||||
|
|
||||||
|
override fun equals(other: Any?) =
|
||||||
|
other is Raw &&
|
||||||
|
when {
|
||||||
|
name != null && other.name != null -> name.equals(other.name, true)
|
||||||
|
name == null && other.name == null -> true
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The data object for a genre. */
|
/** The data object for a genre. */
|
||||||
data class Genre(private val raw: Raw, override val songs: List<Song>) : MusicParent() {
|
class Genre(raw: Raw, override val songs: List<Song>) : MusicParent() {
|
||||||
init {
|
override val uid: UID
|
||||||
for (song in songs) {
|
|
||||||
song._link(this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override val rawName: String?
|
override val rawName = raw.name
|
||||||
get() = raw.name
|
|
||||||
|
|
||||||
// Sort tags don't make sense on genres
|
// Sort tags don't make sense on genres
|
||||||
override val rawSortName: String?
|
override val rawSortName = rawName
|
||||||
get() = rawName
|
|
||||||
|
|
||||||
override val id: Long
|
|
||||||
get() = rawName.toMusicId()
|
|
||||||
|
|
||||||
override fun resolveName(context: Context) = rawName ?: context.getString(R.string.def_genre)
|
override fun resolveName(context: Context) = rawName ?: context.getString(R.string.def_genre)
|
||||||
|
|
||||||
/** The total duration of the songs in this genre, in millis. */
|
/** The total duration of the songs in this genre, in millis. */
|
||||||
val durationMs = songs.sumOf { it.durationMs }
|
val durationMs = songs.sumOf { it.durationMs }
|
||||||
|
|
||||||
data class Raw(val name: String?) {
|
init {
|
||||||
override fun equals(other: Any?): Boolean {
|
uid = UID.hashed(this::class) { update(rawName) }
|
||||||
if (other !is Raw) return false
|
|
||||||
return when {
|
|
||||||
name != null && other.name != null -> name.equals(other.name, true)
|
|
||||||
name == null && other.name == null -> true
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode() = name?.lowercase().hashCode()
|
for (song in songs) {
|
||||||
|
song._link(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Raw(val name: String?) {
|
||||||
|
private val hashCode = name?.lowercase().hashCode()
|
||||||
|
|
||||||
|
override fun hashCode() = hashCode
|
||||||
|
|
||||||
|
override fun equals(other: Any?) =
|
||||||
|
other is Raw &&
|
||||||
|
when {
|
||||||
|
name != null && other.name != null -> name.equals(other.name, true)
|
||||||
|
name == null && other.name == null -> true
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun String?.toMusicId(): Long {
|
fun MessageDigest.update(string: String?) {
|
||||||
if (this == null) {
|
if (string == null) return
|
||||||
// Pre-calculated hash of MediaStore.UNKNOWN_STRING
|
update(string.lowercase().toByteArray())
|
||||||
return 54493231833456
|
}
|
||||||
}
|
|
||||||
|
|
||||||
var result = 0L
|
fun MessageDigest.update(date: Date?) {
|
||||||
for (ch in lowercase()) {
|
if (date == null) return
|
||||||
result = 31 * result + ch.lowercaseChar().code
|
update(date.toString().toByteArray())
|
||||||
}
|
}
|
||||||
return result
|
|
||||||
|
fun MessageDigest.update(n: Int?) {
|
||||||
|
if (n == null) return
|
||||||
|
update(byteArrayOf(n.toByte(), n.shr(8).toByte(), n.shr(16).toByte(), n.shr(24).toByte()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun MessageDigest.update(n: Long?) {
|
||||||
|
if (n == null) return
|
||||||
|
update(
|
||||||
|
byteArrayOf(
|
||||||
|
n.toByte(),
|
||||||
|
n.shr(8).toByte(),
|
||||||
|
n.shr(16).toByte(),
|
||||||
|
n.shr(24).toByte(),
|
||||||
|
n.shr(32).toByte(),
|
||||||
|
n.shr(40).toByte(),
|
||||||
|
n.shr(56).toByte(),
|
||||||
|
n.shr(64).toByte()))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -75,34 +75,38 @@ class MusicStore private constructor() {
|
||||||
val albums: List<Album>,
|
val albums: List<Album>,
|
||||||
val songs: List<Song>
|
val songs: List<Song>
|
||||||
) {
|
) {
|
||||||
private val genreIdMap = HashMap<Long, Genre>().apply { genres.forEach { put(it.id, it) } }
|
private val uidMap = HashMap<Music.UID, Music>()
|
||||||
private val artistIdMap =
|
|
||||||
HashMap<Long, Artist>().apply { artists.forEach { put(it.id, it) } }
|
|
||||||
private val albumIdMap = HashMap<Long, Album>().apply { albums.forEach { put(it.id, it) } }
|
|
||||||
private val songIdMap = HashMap<Long, Song>().apply { songs.forEach { put(it.id, it) } }
|
|
||||||
|
|
||||||
/** Find a [Song] by it's ID. Null if no song exists with that ID. */
|
init {
|
||||||
fun findSongById(songId: Long) = songIdMap[songId]
|
for (song in songs) {
|
||||||
|
uidMap[song.uid] = song
|
||||||
|
}
|
||||||
|
|
||||||
/** Find a [Album] by it's ID. Null if no album exists with that ID. */
|
for (album in albums) {
|
||||||
fun findAlbumById(albumId: Long) = albumIdMap[albumId]
|
uidMap[album.uid] = album
|
||||||
|
}
|
||||||
|
|
||||||
/** Find a [Artist] by it's ID. Null if no artist exists with that ID. */
|
for (artist in artists) {
|
||||||
fun findArtistById(artistId: Long) = artistIdMap[artistId]
|
uidMap[artist.uid] = artist
|
||||||
|
}
|
||||||
|
|
||||||
/** Find a [Genre] by it's ID. Null if no genre exists with that ID. */
|
for (genre in genres) {
|
||||||
fun findGenreById(genreId: Long) = genreIdMap[genreId]
|
uidMap[genre.uid] = genre
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST") fun <T : Music> find(uid: Music.UID): T? = uidMap[uid] as? T
|
||||||
|
|
||||||
/** Sanitize an old item to find the corresponding item in a new library. */
|
/** Sanitize an old item to find the corresponding item in a new library. */
|
||||||
fun sanitize(song: Song) = findSongById(song.id)
|
fun sanitize(song: Song) = find<Song>(song.uid)
|
||||||
/** Sanitize an old item to find the corresponding item in a new library. */
|
/** Sanitize an old item to find the corresponding item in a new library. */
|
||||||
fun sanitize(songs: List<Song>) = songs.mapNotNull { sanitize(it) }
|
fun sanitize(songs: List<Song>) = songs.mapNotNull { sanitize(it) }
|
||||||
/** Sanitize an old item to find the corresponding item in a new library. */
|
/** Sanitize an old item to find the corresponding item in a new library. */
|
||||||
fun sanitize(album: Album) = findAlbumById(album.id)
|
fun sanitize(album: Album) = find<Album>(album.uid)
|
||||||
/** Sanitize an old item to find the corresponding item in a new library. */
|
/** Sanitize an old item to find the corresponding item in a new library. */
|
||||||
fun sanitize(artist: Artist) = findArtistById(artist.id)
|
fun sanitize(artist: Artist) = find<Artist>(artist.uid)
|
||||||
/** Sanitize an old item to find the corresponding item in a new library. */
|
/** Sanitize an old item to find the corresponding item in a new library. */
|
||||||
fun sanitize(genre: Genre) = findGenreById(genre.id)
|
fun sanitize(genre: Genre) = find<Genre>(genre.uid)
|
||||||
|
|
||||||
/** Find a song for a [uri]. */
|
/** Find a song for a [uri]. */
|
||||||
fun findSongForUri(context: Context, uri: Uri) =
|
fun findSongForUri(context: Context, uri: Uri) =
|
||||||
|
|
|
@ -229,13 +229,7 @@ class Indexer {
|
||||||
|
|
||||||
// Sanity check: Ensure that all songs are linked up to albums/artists/genres.
|
// Sanity check: Ensure that all songs are linked up to albums/artists/genres.
|
||||||
for (song in songs) {
|
for (song in songs) {
|
||||||
if (song._isMissingAlbum || song._isMissingArtist || song._isMissingGenre) {
|
song._validate()
|
||||||
error(
|
|
||||||
"Found unlinked song: ${song.rawName} [" +
|
|
||||||
"missing album: ${song._isMissingAlbum} " +
|
|
||||||
"missing artist: ${song._isMissingArtist} " +
|
|
||||||
"missing genre: ${song._isMissingGenre}]")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
logD("Successfully built library in ${System.currentTimeMillis() - buildStart}ms")
|
logD("Successfully built library in ${System.currentTimeMillis() - buildStart}ms")
|
||||||
|
@ -262,7 +256,7 @@ class Indexer {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deduplicate songs to prevent (most) deformed music clones
|
// Deduplicate songs to prevent (most) deformed music clones
|
||||||
songs = songs.distinctBy { it._distinct }.toMutableList()
|
songs = songs.distinctBy { it.uid }.toMutableList()
|
||||||
|
|
||||||
// Ensure that sorting order is consistent so that grouping is also consistent.
|
// Ensure that sorting order is consistent so that grouping is also consistent.
|
||||||
Sort(Sort.Mode.ByName, true).songsInPlace(songs)
|
Sort(Sort.Mode.ByName, true).songsInPlace(songs)
|
||||||
|
@ -287,7 +281,7 @@ class Indexer {
|
||||||
*/
|
*/
|
||||||
private fun buildAlbums(songs: List<Song>): List<Album> {
|
private fun buildAlbums(songs: List<Song>): List<Album> {
|
||||||
val albums = mutableListOf<Album>()
|
val albums = mutableListOf<Album>()
|
||||||
val songsByAlbum = songs.groupBy { it._rawAlbum.groupingId }
|
val songsByAlbum = songs.groupBy { it._rawAlbum }
|
||||||
|
|
||||||
for (entry in songsByAlbum) {
|
for (entry in songsByAlbum) {
|
||||||
val albumSongs = entry.value
|
val albumSongs = entry.value
|
||||||
|
@ -296,8 +290,7 @@ class Indexer {
|
||||||
// This allows us to replicate the LAST_YEAR field, which is useful as it means that
|
// This allows us to replicate the LAST_YEAR field, which is useful as it means that
|
||||||
// weird years like "0" wont show up if there are alternatives.
|
// weird years like "0" wont show up if there are alternatives.
|
||||||
val templateSong =
|
val templateSong =
|
||||||
albumSongs.maxWith(
|
albumSongs.maxWith(compareBy(Sort.Mode.NullableComparator.DATE) { entry.key.date })
|
||||||
compareBy(Sort.Mode.NullableComparator.DATE) { it._rawAlbum.date })
|
|
||||||
|
|
||||||
albums.add(Album(templateSong._rawAlbum, albumSongs))
|
albums.add(Album(templateSong._rawAlbum, albumSongs))
|
||||||
}
|
}
|
||||||
|
@ -313,12 +306,11 @@ class Indexer {
|
||||||
*/
|
*/
|
||||||
private fun buildArtists(albums: List<Album>): List<Artist> {
|
private fun buildArtists(albums: List<Album>): List<Artist> {
|
||||||
val artists = mutableListOf<Artist>()
|
val artists = mutableListOf<Artist>()
|
||||||
val albumsByArtist = albums.groupBy { it._rawArtist.groupingId }
|
val albumsByArtist = albums.groupBy { it._rawArtist }
|
||||||
|
|
||||||
for (entry in albumsByArtist) {
|
for (entry in albumsByArtist) {
|
||||||
// The first album will suffice for template metadata.
|
// The first album will suffice for template metadata.
|
||||||
val templateAlbum = entry.value[0]
|
artists.add(Artist(entry.key, entry.value))
|
||||||
artists.add(Artist(templateAlbum._rawArtist, albums = entry.value))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
logD("Successfully built ${artists.size} artists")
|
logD("Successfully built ${artists.size} artists")
|
||||||
|
@ -340,7 +332,7 @@ class Indexer {
|
||||||
}
|
}
|
||||||
|
|
||||||
for (entry in songsByGenre) {
|
for (entry in songsByGenre) {
|
||||||
genres.add(Genre(entry.key, songs = entry.value))
|
genres.add(Genre(entry.key, entry.value))
|
||||||
}
|
}
|
||||||
|
|
||||||
logD("Successfully built ${genres.size} genres")
|
logD("Successfully built ${genres.size} genres")
|
||||||
|
|
|
@ -84,7 +84,7 @@ class ReplayGainAudioProcessor(context: Context) : BaseAudioProcessor() {
|
||||||
// User wants album gain to be used when in an album, track gain otherwise.
|
// User wants album gain to be used when in an album, track gain otherwise.
|
||||||
ReplayGainMode.DYNAMIC ->
|
ReplayGainMode.DYNAMIC ->
|
||||||
playbackManager.parent is Album &&
|
playbackManager.parent is Album &&
|
||||||
playbackManager.song?.album?.id == playbackManager.parent?.id
|
playbackManager.song?.album == playbackManager.parent
|
||||||
}
|
}
|
||||||
|
|
||||||
val resolvedGain =
|
val resolvedGain =
|
||||||
|
|
|
@ -21,11 +21,8 @@ import android.content.ContentValues
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.database.sqlite.SQLiteDatabase
|
import android.database.sqlite.SQLiteDatabase
|
||||||
import android.database.sqlite.SQLiteOpenHelper
|
import android.database.sqlite.SQLiteOpenHelper
|
||||||
import androidx.core.database.getLongOrNull
|
|
||||||
import androidx.core.database.sqlite.transaction
|
import androidx.core.database.sqlite.transaction
|
||||||
import org.oxycblt.auxio.music.Album
|
import org.oxycblt.auxio.music.Music
|
||||||
import org.oxycblt.auxio.music.Artist
|
|
||||||
import org.oxycblt.auxio.music.Genre
|
|
||||||
import org.oxycblt.auxio.music.MusicParent
|
import org.oxycblt.auxio.music.MusicParent
|
||||||
import org.oxycblt.auxio.music.MusicStore
|
import org.oxycblt.auxio.music.MusicStore
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
|
@ -78,11 +75,10 @@ class PlaybackStateDatabase private constructor(context: Context) :
|
||||||
private fun constructStateTable(command: StringBuilder): StringBuilder {
|
private fun constructStateTable(command: StringBuilder): StringBuilder {
|
||||||
command
|
command
|
||||||
.append("${StateColumns.COLUMN_ID} LONG PRIMARY KEY,")
|
.append("${StateColumns.COLUMN_ID} LONG PRIMARY KEY,")
|
||||||
.append("${StateColumns.COLUMN_SONG_ID} LONG,")
|
.append("${StateColumns.COLUMN_SONG_UID} STRING,")
|
||||||
.append("${StateColumns.COLUMN_POSITION} LONG NOT NULL,")
|
.append("${StateColumns.COLUMN_POSITION} LONG NOT NULL,")
|
||||||
.append("${StateColumns.COLUMN_PARENT_ID} LONG,")
|
.append("${StateColumns.COLUMN_PARENT_UID} STRING,")
|
||||||
.append("${StateColumns.COLUMN_INDEX} INTEGER NOT NULL,")
|
.append("${StateColumns.COLUMN_INDEX} INTEGER NOT NULL,")
|
||||||
.append("${StateColumns.COLUMN_PLAYBACK_MODE} INTEGER NOT NULL,")
|
|
||||||
.append("${StateColumns.COLUMN_IS_SHUFFLED} BOOLEAN NOT NULL,")
|
.append("${StateColumns.COLUMN_IS_SHUFFLED} BOOLEAN NOT NULL,")
|
||||||
.append("${StateColumns.COLUMN_REPEAT_MODE} INTEGER NOT NULL)")
|
.append("${StateColumns.COLUMN_REPEAT_MODE} INTEGER NOT NULL)")
|
||||||
|
|
||||||
|
@ -93,8 +89,7 @@ class PlaybackStateDatabase private constructor(context: Context) :
|
||||||
private fun constructQueueTable(command: StringBuilder): StringBuilder {
|
private fun constructQueueTable(command: StringBuilder): StringBuilder {
|
||||||
command
|
command
|
||||||
.append("${QueueColumns.ID} LONG PRIMARY KEY,")
|
.append("${QueueColumns.ID} LONG PRIMARY KEY,")
|
||||||
.append("${QueueColumns.SONG_ID} INTEGER NOT NULL,")
|
.append("${QueueColumns.SONG_UID} STRING NOT NULL)")
|
||||||
.append("${QueueColumns.ALBUM_ID} INTEGER NOT NULL)")
|
|
||||||
|
|
||||||
return command
|
return command
|
||||||
}
|
}
|
||||||
|
@ -109,17 +104,12 @@ class PlaybackStateDatabase private constructor(context: Context) :
|
||||||
|
|
||||||
// Correct the index to match up with a possibly shortened queue (file removals/changes)
|
// Correct the index to match up with a possibly shortened queue (file removals/changes)
|
||||||
var actualIndex = rawState.index
|
var actualIndex = rawState.index
|
||||||
while (queue.getOrNull(actualIndex)?.id != rawState.songId && actualIndex > -1) {
|
while (queue.getOrNull(actualIndex)?.uid?.also { logD(it) } != rawState.songUid &&
|
||||||
|
actualIndex > -1) {
|
||||||
actualIndex--
|
actualIndex--
|
||||||
}
|
}
|
||||||
|
|
||||||
val parent =
|
val parent = rawState.parentUid?.let { library.find<MusicParent>(it) }
|
||||||
when (rawState.playbackMode) {
|
|
||||||
PlaybackMode.ALL_SONGS -> null
|
|
||||||
PlaybackMode.IN_ALBUM -> rawState.parentId?.let(library::findAlbumById)
|
|
||||||
PlaybackMode.IN_ARTIST -> rawState.parentId?.let(library::findArtistById)
|
|
||||||
PlaybackMode.IN_GENRE -> rawState.parentId?.let(library::findGenreById)
|
|
||||||
}
|
|
||||||
|
|
||||||
return SavedState(
|
return SavedState(
|
||||||
index = actualIndex,
|
index = actualIndex,
|
||||||
|
@ -140,11 +130,10 @@ class PlaybackStateDatabase private constructor(context: Context) :
|
||||||
val indexIndex = cursor.getColumnIndexOrThrow(StateColumns.COLUMN_INDEX)
|
val indexIndex = cursor.getColumnIndexOrThrow(StateColumns.COLUMN_INDEX)
|
||||||
|
|
||||||
val posIndex = cursor.getColumnIndexOrThrow(StateColumns.COLUMN_POSITION)
|
val posIndex = cursor.getColumnIndexOrThrow(StateColumns.COLUMN_POSITION)
|
||||||
val playbackModeIndex = cursor.getColumnIndexOrThrow(StateColumns.COLUMN_PLAYBACK_MODE)
|
|
||||||
val repeatModeIndex = cursor.getColumnIndexOrThrow(StateColumns.COLUMN_REPEAT_MODE)
|
val repeatModeIndex = cursor.getColumnIndexOrThrow(StateColumns.COLUMN_REPEAT_MODE)
|
||||||
val shuffleIndex = cursor.getColumnIndexOrThrow(StateColumns.COLUMN_IS_SHUFFLED)
|
val shuffleIndex = cursor.getColumnIndexOrThrow(StateColumns.COLUMN_IS_SHUFFLED)
|
||||||
val songIdIndex = cursor.getColumnIndexOrThrow(StateColumns.COLUMN_SONG_ID)
|
val songUidIndex = cursor.getColumnIndexOrThrow(StateColumns.COLUMN_SONG_UID)
|
||||||
val parentIdIndex = cursor.getColumnIndexOrThrow(StateColumns.COLUMN_PARENT_ID)
|
val parentUidIndex = cursor.getColumnIndexOrThrow(StateColumns.COLUMN_PARENT_UID)
|
||||||
|
|
||||||
cursor.moveToFirst()
|
cursor.moveToFirst()
|
||||||
|
|
||||||
|
@ -154,10 +143,9 @@ class PlaybackStateDatabase private constructor(context: Context) :
|
||||||
repeatMode = RepeatMode.fromIntCode(cursor.getInt(repeatModeIndex))
|
repeatMode = RepeatMode.fromIntCode(cursor.getInt(repeatModeIndex))
|
||||||
?: RepeatMode.NONE,
|
?: RepeatMode.NONE,
|
||||||
isShuffled = cursor.getInt(shuffleIndex) == 1,
|
isShuffled = cursor.getInt(shuffleIndex) == 1,
|
||||||
songId = cursor.getLong(songIdIndex),
|
songUid = Music.UID.fromString(cursor.getString(songUidIndex))
|
||||||
parentId = cursor.getLongOrNull(parentIdIndex),
|
?: return@queryAll null,
|
||||||
playbackMode = PlaybackMode.fromInt(cursor.getInt(playbackModeIndex))
|
parentUid = cursor.getString(parentUidIndex)?.let(Music.UID::fromString))
|
||||||
?: PlaybackMode.ALL_SONGS)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -168,9 +156,12 @@ class PlaybackStateDatabase private constructor(context: Context) :
|
||||||
|
|
||||||
readableDatabase.queryAll(TABLE_NAME_QUEUE) { cursor ->
|
readableDatabase.queryAll(TABLE_NAME_QUEUE) { cursor ->
|
||||||
if (cursor.count == 0) return@queryAll
|
if (cursor.count == 0) return@queryAll
|
||||||
val songIndex = cursor.getColumnIndexOrThrow(QueueColumns.SONG_ID)
|
val songIndex = cursor.getColumnIndexOrThrow(QueueColumns.SONG_UID)
|
||||||
while (cursor.moveToNext()) {
|
while (cursor.moveToNext()) {
|
||||||
library.findSongById(cursor.getLong(songIndex))?.let(queue::add)
|
logD(cursor.getString(songIndex))
|
||||||
|
val uid = Music.UID.fromString(cursor.getString(songIndex)) ?: continue
|
||||||
|
val song = library.find<Song>(uid) ?: continue
|
||||||
|
queue.add(song)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -190,15 +181,8 @@ class PlaybackStateDatabase private constructor(context: Context) :
|
||||||
positionMs = state.positionMs,
|
positionMs = state.positionMs,
|
||||||
repeatMode = state.repeatMode,
|
repeatMode = state.repeatMode,
|
||||||
isShuffled = state.isShuffled,
|
isShuffled = state.isShuffled,
|
||||||
songId = state.queue[state.index].id,
|
songUid = state.queue[state.index].uid,
|
||||||
parentId = state.parent?.id,
|
parentUid = state.parent?.uid)
|
||||||
playbackMode =
|
|
||||||
when (state.parent) {
|
|
||||||
null -> PlaybackMode.ALL_SONGS
|
|
||||||
is Album -> PlaybackMode.IN_ALBUM
|
|
||||||
is Artist -> PlaybackMode.IN_ARTIST
|
|
||||||
is Genre -> PlaybackMode.IN_GENRE
|
|
||||||
})
|
|
||||||
|
|
||||||
writeRawState(rawState)
|
writeRawState(rawState)
|
||||||
writeQueue(state.queue)
|
writeQueue(state.queue)
|
||||||
|
@ -218,11 +202,10 @@ class PlaybackStateDatabase private constructor(context: Context) :
|
||||||
val stateData =
|
val stateData =
|
||||||
ContentValues(10).apply {
|
ContentValues(10).apply {
|
||||||
put(StateColumns.COLUMN_ID, 0)
|
put(StateColumns.COLUMN_ID, 0)
|
||||||
put(StateColumns.COLUMN_SONG_ID, rawState.songId)
|
put(StateColumns.COLUMN_SONG_UID, rawState.songUid.toString())
|
||||||
put(StateColumns.COLUMN_POSITION, rawState.positionMs)
|
put(StateColumns.COLUMN_POSITION, rawState.positionMs)
|
||||||
put(StateColumns.COLUMN_PARENT_ID, rawState.parentId)
|
put(StateColumns.COLUMN_PARENT_UID, rawState.parentUid?.toString())
|
||||||
put(StateColumns.COLUMN_INDEX, rawState.index)
|
put(StateColumns.COLUMN_INDEX, rawState.index)
|
||||||
put(StateColumns.COLUMN_PLAYBACK_MODE, rawState.playbackMode.intCode)
|
|
||||||
put(StateColumns.COLUMN_IS_SHUFFLED, rawState.isShuffled)
|
put(StateColumns.COLUMN_IS_SHUFFLED, rawState.isShuffled)
|
||||||
put(StateColumns.COLUMN_REPEAT_MODE, rawState.repeatMode.intCode)
|
put(StateColumns.COLUMN_REPEAT_MODE, rawState.repeatMode.intCode)
|
||||||
}
|
}
|
||||||
|
@ -255,8 +238,7 @@ class PlaybackStateDatabase private constructor(context: Context) :
|
||||||
val itemData =
|
val itemData =
|
||||||
ContentValues(4).apply {
|
ContentValues(4).apply {
|
||||||
put(QueueColumns.ID, idStart + i)
|
put(QueueColumns.ID, idStart + i)
|
||||||
put(QueueColumns.SONG_ID, song.id)
|
put(QueueColumns.SONG_UID, song.uid.toString())
|
||||||
put(QueueColumns.ALBUM_ID, song.album.id)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
insert(TABLE_NAME_QUEUE, null, itemData)
|
insert(TABLE_NAME_QUEUE, null, itemData)
|
||||||
|
@ -286,31 +268,28 @@ class PlaybackStateDatabase private constructor(context: Context) :
|
||||||
val positionMs: Long,
|
val positionMs: Long,
|
||||||
val repeatMode: RepeatMode,
|
val repeatMode: RepeatMode,
|
||||||
val isShuffled: Boolean,
|
val isShuffled: Boolean,
|
||||||
val songId: Long,
|
val songUid: Music.UID,
|
||||||
val parentId: Long?,
|
val parentUid: Music.UID?
|
||||||
val playbackMode: PlaybackMode
|
|
||||||
)
|
)
|
||||||
|
|
||||||
private object StateColumns {
|
private object StateColumns {
|
||||||
const val COLUMN_ID = "id"
|
const val COLUMN_ID = "id"
|
||||||
const val COLUMN_SONG_ID = "song"
|
const val COLUMN_SONG_UID = "song_uid"
|
||||||
const val COLUMN_POSITION = "position"
|
const val COLUMN_POSITION = "position"
|
||||||
const val COLUMN_PARENT_ID = "parent"
|
const val COLUMN_PARENT_UID = "parent"
|
||||||
const val COLUMN_INDEX = "queue_index"
|
const val COLUMN_INDEX = "queue_index"
|
||||||
const val COLUMN_PLAYBACK_MODE = "playback_mode"
|
|
||||||
const val COLUMN_IS_SHUFFLED = "is_shuffling"
|
const val COLUMN_IS_SHUFFLED = "is_shuffling"
|
||||||
const val COLUMN_REPEAT_MODE = "repeat_mode"
|
const val COLUMN_REPEAT_MODE = "repeat_mode"
|
||||||
}
|
}
|
||||||
|
|
||||||
private object QueueColumns {
|
private object QueueColumns {
|
||||||
const val ID = "id"
|
const val ID = "id"
|
||||||
const val SONG_ID = "song"
|
const val SONG_UID = "song_uid"
|
||||||
const val ALBUM_ID = "album"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val DB_NAME = "auxio_state_database.db"
|
const val DB_NAME = "auxio_state_database.db"
|
||||||
const val DB_VERSION = 7
|
const val DB_VERSION = 8
|
||||||
|
|
||||||
const val TABLE_NAME_STATE = "playback_state_table"
|
const val TABLE_NAME_STATE = "playback_state_table"
|
||||||
const val TABLE_NAME_QUEUE = "queue_table"
|
const val TABLE_NAME_QUEUE = "queue_table"
|
||||||
|
|
|
@ -449,7 +449,7 @@ class PlaybackStateManager private constructor() {
|
||||||
// While we could just save and reload the state, we instead sanitize the state
|
// While we could just save and reload the state, we instead sanitize the state
|
||||||
// at runtime for better performance (and to sidestep a co-routine on behalf of the caller).
|
// at runtime for better performance (and to sidestep a co-routine on behalf of the caller).
|
||||||
|
|
||||||
val oldSongId = song?.id
|
val oldSongUid = song?.uid
|
||||||
val oldPosition = playerState.calculateElapsedPosition()
|
val oldPosition = playerState.calculateElapsedPosition()
|
||||||
|
|
||||||
parent =
|
parent =
|
||||||
|
@ -463,7 +463,7 @@ class PlaybackStateManager private constructor() {
|
||||||
|
|
||||||
_queue = newLibrary.sanitize(_queue).toMutableList()
|
_queue = newLibrary.sanitize(_queue).toMutableList()
|
||||||
|
|
||||||
while (song?.id != oldSongId && index > -1) {
|
while (song?.uid != oldSongUid && index > -1) {
|
||||||
index--
|
index--
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -196,7 +196,7 @@ class MediaSessionComponent(private val context: Context, private val callback:
|
||||||
// instead of loading a bitmap.
|
// instead of loading a bitmap.
|
||||||
val description =
|
val description =
|
||||||
MediaDescriptionCompat.Builder()
|
MediaDescriptionCompat.Builder()
|
||||||
.setMediaId("Song:${song.id}")
|
.setMediaId(song.uid.toString())
|
||||||
.setTitle(song.resolveName(context))
|
.setTitle(song.resolveName(context))
|
||||||
.setSubtitle(song.resolveIndividualArtistName(context))
|
.setSubtitle(song.resolveIndividualArtistName(context))
|
||||||
.setIconUri(song.album.coverUri)
|
.setIconUri(song.album.coverUri)
|
||||||
|
|
|
@ -356,7 +356,7 @@ class PlaybackService :
|
||||||
}
|
}
|
||||||
is InternalPlayer.Action.Open -> {
|
is InternalPlayer.Action.Open -> {
|
||||||
library.findSongForUri(application, action.uri)?.let { song ->
|
library.findSongForUri(application, action.uri)?.let { song ->
|
||||||
playbackManager.play(song, settings.libPlaybackMode, settings)
|
playbackManager.play(song, null, settings)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -90,18 +90,18 @@ class SearchAdapter(private val listener: MenuItemListener) :
|
||||||
companion object {
|
companion object {
|
||||||
private val DIFFER =
|
private val DIFFER =
|
||||||
object : SimpleItemCallback<Item>() {
|
object : SimpleItemCallback<Item>() {
|
||||||
override fun areItemsTheSame(oldItem: Item, newItem: Item) =
|
override fun areContentsTheSame(oldItem: Item, newItem: Item) =
|
||||||
when {
|
when {
|
||||||
oldItem is Song && newItem is Song ->
|
oldItem is Song && newItem is Song ->
|
||||||
SongViewHolder.DIFFER.areItemsTheSame(oldItem, newItem)
|
SongViewHolder.DIFFER.areContentsTheSame(oldItem, newItem)
|
||||||
oldItem is Album && newItem is Album ->
|
oldItem is Album && newItem is Album ->
|
||||||
AlbumViewHolder.DIFFER.areItemsTheSame(oldItem, newItem)
|
AlbumViewHolder.DIFFER.areContentsTheSame(oldItem, newItem)
|
||||||
oldItem is Artist && newItem is Artist ->
|
oldItem is Artist && newItem is Artist ->
|
||||||
ArtistViewHolder.DIFFER.areItemsTheSame(oldItem, newItem)
|
ArtistViewHolder.DIFFER.areContentsTheSame(oldItem, newItem)
|
||||||
oldItem is Genre && newItem is Genre ->
|
oldItem is Genre && newItem is Genre ->
|
||||||
GenreViewHolder.DIFFER.areItemsTheSame(oldItem, newItem)
|
GenreViewHolder.DIFFER.areContentsTheSame(oldItem, newItem)
|
||||||
oldItem is Header && newItem is Header ->
|
oldItem is Header && newItem is Header ->
|
||||||
HeaderViewHolder.DIFFER.areItemsTheSame(oldItem, newItem)
|
HeaderViewHolder.DIFFER.areContentsTheSame(oldItem, newItem)
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -170,10 +170,10 @@ class SearchFragment :
|
||||||
findNavController()
|
findNavController()
|
||||||
.navigate(
|
.navigate(
|
||||||
when (item) {
|
when (item) {
|
||||||
is Song -> SearchFragmentDirections.actionShowAlbum(item.album.id)
|
is Song -> SearchFragmentDirections.actionShowAlbum(item.album.uid)
|
||||||
is Album -> SearchFragmentDirections.actionShowAlbum(item.id)
|
is Album -> SearchFragmentDirections.actionShowAlbum(item.uid)
|
||||||
is Artist -> SearchFragmentDirections.actionShowArtist(item.id)
|
is Artist -> SearchFragmentDirections.actionShowArtist(item.uid)
|
||||||
is Genre -> SearchFragmentDirections.actionShowGenre(item.id)
|
is Genre -> SearchFragmentDirections.actionShowGenre(item.uid)
|
||||||
else -> return
|
else -> return
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -42,7 +42,7 @@ import org.oxycblt.auxio.util.systemBarInsetsCompat
|
||||||
* similarly distorted by the insets, and thus I must go further and modify the edge effect to be at
|
* similarly distorted by the insets, and thus I must go further and modify the edge effect to be at
|
||||||
* least somewhat clamped to the insets themselves.
|
* least somewhat clamped to the insets themselves.
|
||||||
* 3. Touch events. Bottom sheets must always intercept touches in their bounds, or they will click
|
* 3. Touch events. Bottom sheets must always intercept touches in their bounds, or they will click
|
||||||
* the now overlapping content view that is only inset by it and not unhidden by it.
|
* the now overlapping content view that is only inset and not moved out of the way..
|
||||||
*
|
*
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -40,7 +40,7 @@ abstract class IndicatorAdapter<VH : RecyclerView.ViewHolder> : RecyclerView.Ada
|
||||||
holder.updateIndicator(
|
holder.updateIndicator(
|
||||||
currentItem != null &&
|
currentItem != null &&
|
||||||
item.javaClass == currentItem.javaClass &&
|
item.javaClass == currentItem.javaClass &&
|
||||||
item.id == currentItem.id,
|
item == currentItem,
|
||||||
isPlaying)
|
isPlaying)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -57,7 +57,7 @@ abstract class IndicatorAdapter<VH : RecyclerView.ViewHolder> : RecyclerView.Ada
|
||||||
if (oldItem != null) {
|
if (oldItem != null) {
|
||||||
val pos =
|
val pos =
|
||||||
currentList.indexOfFirst {
|
currentList.indexOfFirst {
|
||||||
it.javaClass == oldItem.javaClass && it.id == oldItem.id
|
it.javaClass == oldItem.javaClass && item == currentItem
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pos > -1) {
|
if (pos > -1) {
|
||||||
|
@ -68,8 +68,7 @@ abstract class IndicatorAdapter<VH : RecyclerView.ViewHolder> : RecyclerView.Ada
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item != null) {
|
if (item != null) {
|
||||||
val pos =
|
val pos = currentList.indexOfFirst { it.javaClass == item.javaClass && it == item }
|
||||||
currentList.indexOfFirst { it.javaClass == item.javaClass && it.id == item.id }
|
|
||||||
|
|
||||||
if (pos > -1) {
|
if (pos > -1) {
|
||||||
notifyItemChanged(pos, PAYLOAD_INDICATOR_CHANGED)
|
notifyItemChanged(pos, PAYLOAD_INDICATOR_CHANGED)
|
||||||
|
@ -85,8 +84,7 @@ abstract class IndicatorAdapter<VH : RecyclerView.ViewHolder> : RecyclerView.Ada
|
||||||
this.isPlaying = isPlaying
|
this.isPlaying = isPlaying
|
||||||
|
|
||||||
if (!updatedItem && item != null) {
|
if (!updatedItem && item != null) {
|
||||||
val pos =
|
val pos = currentList.indexOfFirst { it.javaClass == item.javaClass && it == item }
|
||||||
currentList.indexOfFirst { it.javaClass == item.javaClass && it.id == item.id }
|
|
||||||
|
|
||||||
if (pos > -1) {
|
if (pos > -1) {
|
||||||
notifyItemChanged(pos, PAYLOAD_INDICATOR_CHANGED)
|
notifyItemChanged(pos, PAYLOAD_INDICATOR_CHANGED)
|
||||||
|
|
|
@ -23,23 +23,14 @@ import androidx.recyclerview.widget.AdapterListUpdateCallback
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
|
||||||
/**
|
/** A marker for something that is a RecyclerView item. Has no functionality on it's own. */
|
||||||
* The base for all items in Auxio. Any datatype can derive this type and gain some behavior not
|
interface Item
|
||||||
* provided for free by the normal adapter implementations, such as certain types of diffing.
|
|
||||||
*/
|
|
||||||
abstract class Item {
|
|
||||||
/** A unique ID for this item. ***THIS IS NOT A MEDIASTORE ID!** */
|
|
||||||
abstract val id: Long
|
|
||||||
}
|
|
||||||
|
|
||||||
/** A data object used solely for the "Header" UI element. */
|
/** A data object used solely for the "Header" UI element. */
|
||||||
data class Header(
|
data class Header(
|
||||||
/** The string resource used for the header. */
|
/** The string resource used for the header. */
|
||||||
@StringRes val string: Int
|
@StringRes val string: Int
|
||||||
) : Item() {
|
) : Item
|
||||||
override val id: Long
|
|
||||||
get() = string.toLong()
|
|
||||||
}
|
|
||||||
|
|
||||||
/** An interface for detecting if an item has been clicked once. */
|
/** An interface for detecting if an item has been clicked once. */
|
||||||
interface ItemClickListener {
|
interface ItemClickListener {
|
||||||
|
@ -165,8 +156,5 @@ class SyncListDiffer<T>(
|
||||||
* [areContentsTheSame] any object that is derived from [Item].
|
* [areContentsTheSame] any object that is derived from [Item].
|
||||||
*/
|
*/
|
||||||
abstract class SimpleItemCallback<T : Item> : DiffUtil.ItemCallback<T>() {
|
abstract class SimpleItemCallback<T : Item> : DiffUtil.ItemCallback<T>() {
|
||||||
override fun areContentsTheSame(oldItem: T, newItem: T): Boolean {
|
final override fun areItemsTheSame(oldItem: T, newItem: T) = oldItem == newItem
|
||||||
if (oldItem.javaClass != newItem.javaClass) return false
|
|
||||||
return oldItem.id == newItem.id
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -62,7 +62,7 @@ class SongViewHolder private constructor(private val binding: ItemSongBinding) :
|
||||||
|
|
||||||
val DIFFER =
|
val DIFFER =
|
||||||
object : SimpleItemCallback<Song>() {
|
object : SimpleItemCallback<Song>() {
|
||||||
override fun areItemsTheSame(oldItem: Song, newItem: Song) =
|
override fun areContentsTheSame(oldItem: Song, newItem: Song) =
|
||||||
oldItem.rawName == newItem.rawName &&
|
oldItem.rawName == newItem.rawName &&
|
||||||
oldItem.individualArtistRawName == oldItem.individualArtistRawName
|
oldItem.individualArtistRawName == oldItem.individualArtistRawName
|
||||||
}
|
}
|
||||||
|
@ -102,7 +102,7 @@ private constructor(
|
||||||
|
|
||||||
val DIFFER =
|
val DIFFER =
|
||||||
object : SimpleItemCallback<Album>() {
|
object : SimpleItemCallback<Album>() {
|
||||||
override fun areItemsTheSame(oldItem: Album, newItem: Album) =
|
override fun areContentsTheSame(oldItem: Album, newItem: Album) =
|
||||||
oldItem.rawName == newItem.rawName &&
|
oldItem.rawName == newItem.rawName &&
|
||||||
oldItem.artist.rawName == newItem.artist.rawName &&
|
oldItem.artist.rawName == newItem.artist.rawName &&
|
||||||
oldItem.releaseType == newItem.releaseType
|
oldItem.releaseType == newItem.releaseType
|
||||||
|
@ -145,7 +145,7 @@ class ArtistViewHolder private constructor(private val binding: ItemParentBindin
|
||||||
|
|
||||||
val DIFFER =
|
val DIFFER =
|
||||||
object : SimpleItemCallback<Artist>() {
|
object : SimpleItemCallback<Artist>() {
|
||||||
override fun areItemsTheSame(oldItem: Artist, newItem: Artist) =
|
override fun areContentsTheSame(oldItem: Artist, newItem: Artist) =
|
||||||
oldItem.rawName == newItem.rawName &&
|
oldItem.rawName == newItem.rawName &&
|
||||||
oldItem.albums.size == newItem.albums.size &&
|
oldItem.albums.size == newItem.albums.size &&
|
||||||
oldItem.songs.size == newItem.songs.size
|
oldItem.songs.size == newItem.songs.size
|
||||||
|
@ -187,7 +187,7 @@ private constructor(
|
||||||
|
|
||||||
val DIFFER =
|
val DIFFER =
|
||||||
object : SimpleItemCallback<Genre>() {
|
object : SimpleItemCallback<Genre>() {
|
||||||
override fun areItemsTheSame(oldItem: Genre, newItem: Genre): Boolean =
|
override fun areContentsTheSame(oldItem: Genre, newItem: Genre): Boolean =
|
||||||
oldItem.rawName == newItem.rawName && oldItem.songs.size == newItem.songs.size
|
oldItem.rawName == newItem.rawName && oldItem.songs.size == newItem.songs.size
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -211,7 +211,7 @@ class HeaderViewHolder private constructor(private val binding: ItemHeaderBindin
|
||||||
|
|
||||||
val DIFFER =
|
val DIFFER =
|
||||||
object : SimpleItemCallback<Header>() {
|
object : SimpleItemCallback<Header>() {
|
||||||
override fun areItemsTheSame(oldItem: Header, newItem: Header): Boolean =
|
override fun areContentsTheSame(oldItem: Header, newItem: Header): Boolean =
|
||||||
oldItem.string == newItem.string
|
oldItem.string == newItem.string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
Now, it would be quite cool if we could implement shared element transitions
|
Now, it would be quite cool if we could implement shared element transitions
|
||||||
between elements in this navigation web. Sadly though, the shared element transition
|
between elements in this navigation web. Sadly though, the shared element transition
|
||||||
system is filled with so many bugs and visual errors to make this a terrible idea.
|
system is filled with so many bugs and visual errors to make this a terrible idea.
|
||||||
Just use the boring, yet sane and functional fade transitions.
|
Just use the boring, yet sane and functional axis transitions.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<fragment
|
<fragment
|
||||||
|
@ -17,8 +17,8 @@
|
||||||
android:label="ArtistDetailFragment"
|
android:label="ArtistDetailFragment"
|
||||||
tools:layout="@layout/fragment_detail">
|
tools:layout="@layout/fragment_detail">
|
||||||
<argument
|
<argument
|
||||||
android:name="artistId"
|
android:name="artistUid"
|
||||||
app:argType="long" />
|
app:argType="org.oxycblt.auxio.music.Music$UID" />
|
||||||
<action
|
<action
|
||||||
android:id="@+id/action_show_artist"
|
android:id="@+id/action_show_artist"
|
||||||
app:destination="@id/artist_detail_fragment" />
|
app:destination="@id/artist_detail_fragment" />
|
||||||
|
@ -32,8 +32,8 @@
|
||||||
android:label="AlbumDetailFragment"
|
android:label="AlbumDetailFragment"
|
||||||
tools:layout="@layout/fragment_detail">
|
tools:layout="@layout/fragment_detail">
|
||||||
<argument
|
<argument
|
||||||
android:name="albumId"
|
android:name="albumUid"
|
||||||
app:argType="long" />
|
app:argType="org.oxycblt.auxio.music.Music$UID" />
|
||||||
<action
|
<action
|
||||||
android:id="@+id/action_show_artist"
|
android:id="@+id/action_show_artist"
|
||||||
app:destination="@id/artist_detail_fragment" />
|
app:destination="@id/artist_detail_fragment" />
|
||||||
|
@ -47,8 +47,8 @@
|
||||||
android:label="GenreDetailFragment"
|
android:label="GenreDetailFragment"
|
||||||
tools:layout="@layout/fragment_detail">
|
tools:layout="@layout/fragment_detail">
|
||||||
<argument
|
<argument
|
||||||
android:name="genreId"
|
android:name="genreUid"
|
||||||
app:argType="long" />
|
app:argType="org.oxycblt.auxio.music.Music$UID" />
|
||||||
<action
|
<action
|
||||||
android:id="@+id/action_show_artist"
|
android:id="@+id/action_show_artist"
|
||||||
app:destination="@id/artist_detail_fragment" />
|
app:destination="@id/artist_detail_fragment" />
|
||||||
|
|
|
@ -40,7 +40,7 @@
|
||||||
android:label="song_detail_dialog"
|
android:label="song_detail_dialog"
|
||||||
tools:layout="@layout/dialog_song_detail">
|
tools:layout="@layout/dialog_song_detail">
|
||||||
<argument
|
<argument
|
||||||
android:name="songId"
|
android:name="songUid"
|
||||||
app:argType="long" />
|
app:argType="org.oxycblt.auxio.music.Music$UID" />
|
||||||
</dialog>
|
</dialog>
|
||||||
</navigation>
|
</navigation>
|
Loading…
Reference in a new issue