detail: decouple detail song and properties

Decouple DetailSong into two fields for the current song and current
properties.

Combining them was a technical decision that no longer makes sense,
and separating them makes life much easier.
This commit is contained in:
Alexander Capehart 2023-01-01 17:00:08 -07:00
parent b103eb4749
commit 9ab729a069
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
3 changed files with 43 additions and 55 deletions

View file

@ -34,22 +34,14 @@ data class SortHeader(@StringRes val titleRes: Int) : Item
*/
data class DiscHeader(val disc: Int) : Item
/**
* A [Song] extension that adds information about it's file properties.
* @param song The internal song
* @param properties The properties of the song file. Null if parsing is ongoing.
*/
data class DetailSong(val song: Song, val properties: Properties?) {
/**
* The properties of a [Song]'s file.
* @param bitrateKbps The bit rate, in kilobytes-per-second. Null if it could not be parsed.
* @param sampleRateHz The sample rate, in hertz.
* @param resolvedMimeType The known mime type of the [Song] after it's file format was
* determined.
* @param resolvedMimeType The known mime type of the [Song] after it's file format was determined.
*/
data class Properties(
data class SongProperties(
val bitrateKbps: Int?,
val sampleRateHz: Int?,
val resolvedMimeType: MimeType
)
}

View file

@ -59,15 +59,15 @@ class DetailViewModel(application: Application) :
// --- SONG ---
private val _currentSong = MutableStateFlow<DetailSong?>(null)
/**
* The current [DetailSong] to display. Null if there is nothing to show.
*
* TODO: De-couple Song and Properties?
*/
val currentSong: StateFlow<DetailSong?>
private val _currentSong = MutableStateFlow<Song?>(null)
/** The current [Song] to display. Null if there is nothing to show. */
val currentSong: StateFlow<Song?>
get() = _currentSong
private val _songProperties = MutableStateFlow<SongProperties?>(null)
/** The [SongProperties] of the currently shown [Song]. Null if not loaded yet. */
val songProperties: StateFlow<SongProperties?> = _songProperties
// --- ALBUM ---
private val _currentAlbum = MutableStateFlow<Album?>(null)
@ -149,13 +149,8 @@ class DetailViewModel(application: Application) :
val song = currentSong.value
if (song != null) {
val newSong = library.sanitize(song.song)
if (newSong != null) {
loadDetailSong(newSong)
} else {
_currentSong.value = null
}
logD("Updated song to $newSong")
_currentSong.value = library.sanitize(song)?.also(::loadProperties)
logD("Updated song to ${currentSong.value}")
}
val album = currentAlbum.value
@ -183,12 +178,12 @@ class DetailViewModel(application: Application) :
* @param uid The UID of the [Song] to load. Must be valid.
*/
fun setSongUid(uid: Music.UID) {
if (_currentSong.value?.run { song.uid } == uid) {
if (_currentSong.value?.uid == uid) {
// Nothing to do.
return
}
logD("Opening Song [uid: $uid]")
loadDetailSong(requireMusic(uid))
_currentSong.value = requireMusic<Song>(uid).also(::loadProperties)
}
/**
@ -202,7 +197,7 @@ class DetailViewModel(application: Application) :
return
}
logD("Opening Album [uid: $uid]")
_currentAlbum.value = requireMusic<Album>(uid).also { refreshAlbumList(it) }
_currentAlbum.value = requireMusic<Album>(uid).also(::refreshAlbumList)
}
/**
@ -216,7 +211,7 @@ class DetailViewModel(application: Application) :
return
}
logD("Opening Artist [uid: $uid]")
_currentArtist.value = requireMusic<Artist>(uid).also { refreshArtistList(it) }
_currentArtist.value = requireMusic<Artist>(uid).also(::refreshArtistList)
}
/**
@ -230,7 +225,7 @@ class DetailViewModel(application: Application) :
return
}
logD("Opening Genre [uid: $uid]")
_currentGenre.value = requireMusic<Genre>(uid).also { refreshGenreList(it) }
_currentGenre.value = requireMusic<Genre>(uid).also(::refreshGenreList)
}
private fun <T : Music> requireMusic(uid: Music.UID): T =
@ -240,19 +235,19 @@ class DetailViewModel(application: Application) :
* Start a new job to load a [DetailSong] based on the properties of the given [Song]'s file.
* @param song The song to load.
*/
private fun loadDetailSong(song: Song) {
private fun loadProperties(song: Song) {
// Clear any previous job in order to avoid stale data from appearing in the UI.
currentSongJob?.cancel()
_currentSong.value = DetailSong(song, null)
_songProperties.value = null
currentSongJob =
viewModelScope.launch(Dispatchers.IO) {
val info = loadProperties(song)
val properties = this@DetailViewModel.loadPropertiesImpl(song)
yield()
_currentSong.value = DetailSong(song, info)
_songProperties.value = properties
}
}
private fun loadProperties(song: Song): DetailSong.Properties {
private fun loadPropertiesImpl(song: Song): SongProperties {
// While we would use ExoPlayer to extract this information, it doesn't support
// common data like bit rate in progressive data sources due to there being no
// demand. Thus, we are stuck with the inferior OS-provided MediaExtractor.
@ -266,7 +261,7 @@ class DetailViewModel(application: Application) :
// that we can show.
logW("Unable to extract song attributes.")
logW(e.stackTraceToString())
return DetailSong.Properties(null, null, song.mimeType)
return SongProperties(null, null, song.mimeType)
}
// Get the first track from the extractor (This is basically always the only
@ -310,7 +305,7 @@ class DetailViewModel(application: Application) :
MimeType(song.mimeType.fromExtension, formatMimeType)
}
return DetailSong.Properties(bitrate, sampleRate, resolvedMimeType)
return SongProperties(bitrate, sampleRate, resolvedMimeType)
}
private fun refreshAlbumList(album: Album) {

View file

@ -26,6 +26,7 @@ import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.DialogSongDetailBinding
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.formatDurationMs
import org.oxycblt.auxio.ui.ViewBindingDialogFragment
import org.oxycblt.auxio.util.androidActivityViewModels
@ -53,10 +54,10 @@ class SongDetailDialog : ViewBindingDialogFragment<DialogSongDetailBinding>() {
super.onBindingCreated(binding, savedInstanceState)
// DetailViewModel handles most initialization from the navigation argument.
detailModel.setSongUid(args.itemUid)
collectImmediately(detailModel.currentSong, ::updateSong)
collectImmediately(detailModel.currentSong, detailModel.songProperties, ::updateSong)
}
private fun updateSong(song: DetailSong?) {
private fun updateSong(song: Song?, properties: SongProperties?) {
if (song == null) {
// Song we were showing no longer exists.
findNavController().navigateUp()
@ -64,28 +65,28 @@ class SongDetailDialog : ViewBindingDialogFragment<DialogSongDetailBinding>() {
}
val binding = requireBinding()
if (song.properties != null) {
if (properties != null) {
// Finished loading Song properties, populate and show the list of Song information.
binding.detailLoading.isInvisible = true
binding.detailContainer.isInvisible = false
val context = requireContext()
binding.detailFileName.setText(song.song.path.name)
binding.detailRelativeDir.setText(song.song.path.parent.resolveName(context))
binding.detailFormat.setText(song.properties.resolvedMimeType.resolveName(context))
binding.detailSize.setText(Formatter.formatFileSize(context, song.song.size))
binding.detailDuration.setText(song.song.durationMs.formatDurationMs(true))
binding.detailFileName.setText(song.path.name)
binding.detailRelativeDir.setText(song.path.parent.resolveName(context))
binding.detailFormat.setText(properties.resolvedMimeType.resolveName(context))
binding.detailSize.setText(Formatter.formatFileSize(context, song.size))
binding.detailDuration.setText(song.durationMs.formatDurationMs(true))
if (song.properties.bitrateKbps != null) {
if (properties.bitrateKbps != null) {
binding.detailBitrate.setText(
getString(R.string.fmt_bitrate, song.properties.bitrateKbps))
getString(R.string.fmt_bitrate, properties.bitrateKbps))
} else {
binding.detailBitrate.setText(R.string.def_bitrate)
}
if (song.properties.sampleRateHz != null) {
if (properties.sampleRateHz != null) {
binding.detailSampleRate.setText(
getString(R.string.fmt_sample_rate, song.properties.sampleRateHz))
getString(R.string.fmt_sample_rate, properties.sampleRateHz))
} else {
binding.detailSampleRate.setText(R.string.def_sample_rate)
}