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

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

View file

@ -59,15 +59,15 @@ class DetailViewModel(application: Application) :
// --- SONG --- // --- SONG ---
private val _currentSong = MutableStateFlow<DetailSong?>(null) private val _currentSong = MutableStateFlow<Song?>(null)
/** /** The current [Song] to display. Null if there is nothing to show. */
* The current [DetailSong] to display. Null if there is nothing to show. val currentSong: StateFlow<Song?>
*
* TODO: De-couple Song and Properties?
*/
val currentSong: StateFlow<DetailSong?>
get() = _currentSong 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 --- // --- ALBUM ---
private val _currentAlbum = MutableStateFlow<Album?>(null) private val _currentAlbum = MutableStateFlow<Album?>(null)
@ -149,13 +149,8 @@ class DetailViewModel(application: Application) :
val song = currentSong.value val song = currentSong.value
if (song != null) { if (song != null) {
val newSong = library.sanitize(song.song) _currentSong.value = library.sanitize(song)?.also(::loadProperties)
if (newSong != null) { logD("Updated song to ${currentSong.value}")
loadDetailSong(newSong)
} else {
_currentSong.value = null
}
logD("Updated song to $newSong")
} }
val album = currentAlbum.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. * @param uid The UID of the [Song] to load. Must be valid.
*/ */
fun setSongUid(uid: Music.UID) { fun setSongUid(uid: Music.UID) {
if (_currentSong.value?.run { song.uid } == uid) { if (_currentSong.value?.uid == uid) {
// Nothing to do. // Nothing to do.
return return
} }
logD("Opening Song [uid: $uid]") logD("Opening Song [uid: $uid]")
loadDetailSong(requireMusic(uid)) _currentSong.value = requireMusic<Song>(uid).also(::loadProperties)
} }
/** /**
@ -202,7 +197,7 @@ class DetailViewModel(application: Application) :
return return
} }
logD("Opening Album [uid: $uid]") 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 return
} }
logD("Opening Artist [uid: $uid]") 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 return
} }
logD("Opening Genre [uid: $uid]") 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 = 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. * Start a new job to load a [DetailSong] based on the properties of the given [Song]'s file.
* @param song The song to load. * @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. // Clear any previous job in order to avoid stale data from appearing in the UI.
currentSongJob?.cancel() currentSongJob?.cancel()
_currentSong.value = DetailSong(song, null) _songProperties.value = null
currentSongJob = currentSongJob =
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
val info = loadProperties(song) val properties = this@DetailViewModel.loadPropertiesImpl(song)
yield() 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 // 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 // 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. // demand. Thus, we are stuck with the inferior OS-provided MediaExtractor.
@ -266,7 +261,7 @@ class DetailViewModel(application: Application) :
// that we can show. // that we can show.
logW("Unable to extract song attributes.") logW("Unable to extract song attributes.")
logW(e.stackTraceToString()) 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 // 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) MimeType(song.mimeType.fromExtension, formatMimeType)
} }
return DetailSong.Properties(bitrate, sampleRate, resolvedMimeType) return SongProperties(bitrate, sampleRate, resolvedMimeType)
} }
private fun refreshAlbumList(album: Album) { private fun refreshAlbumList(album: Album) {

View file

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