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:
Alexander Capehart 2022-09-06 22:13:06 -06:00
parent 457013d047
commit 48ad45e4c3
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
31 changed files with 443 additions and 360 deletions

View file

@ -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

View file

@ -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 {

View file

@ -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()

View file

@ -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)
}
} }
} }

View file

@ -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
} }

View file

@ -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()
}

View file

@ -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

View file

@ -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)
} }

View file

@ -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
} }
} }

View file

@ -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
} }

View file

@ -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
} }
} }

View file

@ -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

View file

@ -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
} }

View file

@ -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}")

View file

@ -39,13 +39,12 @@ 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) {

View file

@ -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)
} }
init { fun _validate() {
val artistName: String? (checkNotNull(_album) { "Malformed song: album is null" })._validate()
val artistSortName: String? check(_genres.isNotEmpty()) { "Malformed song: genres are empty" }
if (raw.albumArtistName != null) {
artistName = raw.albumArtistName
artistSortName = raw.albumArtistSortName
} else {
artistName = raw.artistName
artistSortName = raw.artistSortName
} }
_rawAlbum = init {
Album.Raw( // Generally, we calculate UIDs at the end since everything will definitely be initialized
mediaStoreId = raw.albumMediaStoreId, // by now.
name = raw.albumName, uid =
sortName = raw.albumSortName, UID.hashed(this::class) {
date = raw.date, update(rawName.lowercase())
releaseType = raw.albumReleaseType, update(_rawAlbum.name.lowercase())
artistName, update(_rawAlbum.date)
artistSortName)
update(raw.artistName)
update(raw.albumArtistName)
update(track)
update(disc)
update(durationMs.msToSecs())
}
} }
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 { 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 -> name.equals(other.name, true)
name == null && other.name == null -> true name == null && other.name == null -> true
else -> false else -> false
} }
} }
override fun hashCode() = name?.lowercase().hashCode()
}
} }
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()))
} }
/** /**

View file

@ -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) =

View file

@ -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")

View file

@ -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 =

View file

@ -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"

View file

@ -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--
} }

View file

@ -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)

View file

@ -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)
} }
} }
} }

View file

@ -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
} }
} }

View file

@ -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
}) })

View file

@ -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
*/ */

View file

@ -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)

View file

@ -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
}
} }

View file

@ -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
} }
} }

View file

@ -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" />

View file

@ -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>