From a6f2d82107d28ac30f4ce01d609d36a9815bde17 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Mon, 23 Jan 2023 09:06:20 -0700 Subject: [PATCH] detail: split off song property extraction Split off the song property extraction code into a music package class. This is part of an initiative to remove non-parameter uses of contexts in ViewModels. --- .../java/org/oxycblt/auxio/IntegerTable.kt | 4 +- .../java/org/oxycblt/auxio/detail/Detail.kt | 43 ------ .../oxycblt/auxio/detail/DetailViewModel.kt | 96 +++----------- .../oxycblt/auxio/detail/SongDetailDialog.kt | 20 +-- .../auxio/detail/recycler/DetailAdapter.kt | 26 ++-- .../main/java/org/oxycblt/auxio/list/Data.kt | 14 +- .../auxio/list/recycler/ViewHolders.kt | 26 ++-- .../auxio/music/extractor/AudioInfo.kt | 123 ++++++++++++++++++ .../org/oxycblt/auxio/search/SearchAdapter.kt | 12 +- .../oxycblt/auxio/search/SearchViewModel.kt | 10 +- app/src/main/res/layout/item_header.xml | 11 +- app/src/main/res/layout/item_sort_header.xml | 17 +-- 12 files changed, 213 insertions(+), 189 deletions(-) delete mode 100644 app/src/main/java/org/oxycblt/auxio/detail/Detail.kt create mode 100644 app/src/main/java/org/oxycblt/auxio/music/extractor/AudioInfo.kt diff --git a/app/src/main/java/org/oxycblt/auxio/IntegerTable.kt b/app/src/main/java/org/oxycblt/auxio/IntegerTable.kt index e2cbcb5ce..3c724786a 100644 --- a/app/src/main/java/org/oxycblt/auxio/IntegerTable.kt +++ b/app/src/main/java/org/oxycblt/auxio/IntegerTable.kt @@ -31,8 +31,8 @@ object IntegerTable { const val VIEW_TYPE_ARTIST = 0xA002 /** GenreViewHolder */ const val VIEW_TYPE_GENRE = 0xA003 - /** HeaderViewHolder */ - const val VIEW_TYPE_HEADER = 0xA004 + /** BasicHeaderViewHolder */ + const val VIEW_TYPE_BASIC_HEADER = 0xA004 /** SortHeaderViewHolder */ const val VIEW_TYPE_SORT_HEADER = 0xA005 /** AlbumDetailViewHolder */ diff --git a/app/src/main/java/org/oxycblt/auxio/detail/Detail.kt b/app/src/main/java/org/oxycblt/auxio/detail/Detail.kt deleted file mode 100644 index 310c10cfd..000000000 --- a/app/src/main/java/org/oxycblt/auxio/detail/Detail.kt +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright (c) 2022 Auxio Project - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.oxycblt.auxio.detail - -import androidx.annotation.StringRes -import org.oxycblt.auxio.list.Item -import org.oxycblt.auxio.music.Song -import org.oxycblt.auxio.music.storage.MimeType - -/** - * A header variation that displays a button to open a sort menu. - * @param titleRes The string resource to use as the header title - * @author Alexander Capehart (OxygenCobalt) - */ -data class SortHeader(@StringRes val titleRes: Int) : Item - -/** - * 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. - * @author Alexander Capehart (OxygenCobalt) - */ -data class SongProperties( - val bitrateKbps: Int?, - val sampleRateHz: Int?, - val resolvedMimeType: MimeType -) diff --git a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt index c3b3580d3..4dada7534 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt @@ -18,8 +18,6 @@ package org.oxycblt.auxio.detail import android.app.Application -import android.media.MediaExtractor -import android.media.MediaFormat import androidx.annotation.StringRes import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope @@ -30,13 +28,14 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import kotlinx.coroutines.yield import org.oxycblt.auxio.R -import org.oxycblt.auxio.list.Header +import org.oxycblt.auxio.detail.recycler.SortHeader +import org.oxycblt.auxio.list.BasicHeader import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.music.* import org.oxycblt.auxio.music.MusicStore +import org.oxycblt.auxio.music.extractor.AudioInfo import org.oxycblt.auxio.music.library.Library import org.oxycblt.auxio.music.library.Sort -import org.oxycblt.auxio.music.storage.MimeType import org.oxycblt.auxio.music.tags.Disc import org.oxycblt.auxio.music.tags.ReleaseType import org.oxycblt.auxio.playback.PlaybackSettings @@ -54,6 +53,7 @@ class DetailViewModel(application: Application) : private val musicStore = MusicStore.getInstance() private val musicSettings = MusicSettings.from(application) private val playbackSettings = PlaybackSettings.from(application) + private val audioInfoProvider = AudioInfo.Provider.from(application) private var currentSongJob: Job? = null @@ -64,9 +64,9 @@ class DetailViewModel(application: Application) : val currentSong: StateFlow get() = _currentSong - private val _songProperties = MutableStateFlow(null) - /** The [SongProperties] of the currently shown [Song]. Null if not loaded yet. */ - val songProperties: StateFlow = _songProperties + private val _songAudioInfo = MutableStateFlow(null) + /** The [AudioInfo] of the currently shown [Song]. Null if not loaded yet. */ + val songAudioInfo: StateFlow = _songAudioInfo // --- ALBUM --- @@ -156,7 +156,7 @@ class DetailViewModel(application: Application) : val song = currentSong.value if (song != null) { - _currentSong.value = library.sanitize(song)?.also(::loadProperties) + _currentSong.value = library.sanitize(song)?.also(::refreshAudioInfo) logD("Updated song to ${currentSong.value}") } @@ -181,7 +181,7 @@ class DetailViewModel(application: Application) : /** * Set a new [currentSong] from it's [Music.UID]. If the [Music.UID] differs, [currentSong] and - * [songProperties] will be updated to align with the new [Song]. + * [songAudioInfo] will be updated to align with the new [Song]. * @param uid The UID of the [Song] to load. Must be valid. */ fun setSongUid(uid: Music.UID) { @@ -190,7 +190,7 @@ class DetailViewModel(application: Application) : return } logD("Opening Song [uid: $uid]") - _currentSong.value = requireMusic(uid)?.also(::loadProperties) + _currentSong.value = requireMusic(uid)?.also(::refreshAudioInfo) } /** @@ -238,83 +238,21 @@ class DetailViewModel(application: Application) : private fun requireMusic(uid: Music.UID) = musicStore.library?.find(uid) /** - * Start a new job to load a given [Song]'s [SongProperties]. Result is pushed to - * [songProperties]. + * Start a new job to load a given [Song]'s [AudioInfo]. Result is pushed to [songAudioInfo]. * @param song The song to load. */ - private fun loadProperties(song: Song) { + private fun refreshAudioInfo(song: Song) { // Clear any previous job in order to avoid stale data from appearing in the UI. currentSongJob?.cancel() - _songProperties.value = null + _songAudioInfo.value = null currentSongJob = viewModelScope.launch(Dispatchers.IO) { - val properties = this@DetailViewModel.loadPropertiesImpl(song) + val info = audioInfoProvider.extract(song) yield() - _songProperties.value = properties + _songAudioInfo.value = info } } - 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. - val extractor = MediaExtractor() - - try { - extractor.setDataSource(context, song.uri, emptyMap()) - } catch (e: Exception) { - // Can feasibly fail with invalid file formats. Note that this isn't considered - // an error condition in the UI, as there is still plenty of other song information - // that we can show. - logW("Unable to extract song attributes.") - logW(e.stackTraceToString()) - return SongProperties(null, null, song.mimeType) - } - - // Get the first track from the extractor (This is basically always the only - // track we need to analyze). - val format = extractor.getTrackFormat(0) - - // Accessing fields can throw an exception if the fields are not present, and - // the new method for using default values is not available on lower API levels. - // So, we are forced to handle the exception and map it to a saner null value. - val bitrate = - try { - // Convert bytes-per-second to kilobytes-per-second. - format.getInteger(MediaFormat.KEY_BIT_RATE) / 1000 - } catch (e: NullPointerException) { - logD("Unable to extract bit rate field") - null - } - - val sampleRate = - try { - format.getInteger(MediaFormat.KEY_SAMPLE_RATE) - } catch (e: NullPointerException) { - logE("Unable to extract sample rate field") - null - } - - val resolvedMimeType = - if (song.mimeType.fromFormat != null) { - // ExoPlayer was already able to populate the format. - song.mimeType - } else { - // ExoPlayer couldn't populate the format somehow, populate it here. - val formatMimeType = - try { - format.getString(MediaFormat.KEY_MIME) - } catch (e: NullPointerException) { - logE("Unable to extract mime type field") - null - } - - MimeType(song.mimeType.fromExtension, formatMimeType) - } - - return SongProperties(bitrate, sampleRate, resolvedMimeType) - } - private fun refreshAlbumList(album: Album) { logD("Refreshing album data") val data = mutableListOf(album) @@ -368,7 +306,7 @@ class DetailViewModel(application: Application) : logD("Release groups for this artist: ${byReleaseGroup.keys}") for (entry in byReleaseGroup.entries.sortedBy { it.key }) { - data.add(Header(entry.key.headerTitleRes)) + data.add(BasicHeader(entry.key.headerTitleRes)) data.addAll(entry.value) } @@ -386,7 +324,7 @@ class DetailViewModel(application: Application) : logD("Refreshing genre data") val data = mutableListOf(genre) // Genre is guaranteed to always have artists and songs. - data.add(Header(R.string.lbl_artists)) + data.add(BasicHeader(R.string.lbl_artists)) data.addAll(genre.artists) data.add(SortHeader(R.string.lbl_songs)) data.addAll(genreSongSort.songs(genre.songs)) diff --git a/app/src/main/java/org/oxycblt/auxio/detail/SongDetailDialog.kt b/app/src/main/java/org/oxycblt/auxio/detail/SongDetailDialog.kt index cf2d516d7..ea663824c 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/SongDetailDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/SongDetailDialog.kt @@ -27,6 +27,7 @@ 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.music.extractor.AudioInfo import org.oxycblt.auxio.playback.formatDurationMs import org.oxycblt.auxio.ui.ViewBindingDialogFragment import org.oxycblt.auxio.util.androidActivityViewModels @@ -54,10 +55,10 @@ class SongDetailDialog : ViewBindingDialogFragment() { super.onBindingCreated(binding, savedInstanceState) // DetailViewModel handles most initialization from the navigation argument. detailModel.setSongUid(args.itemUid) - collectImmediately(detailModel.currentSong, detailModel.songProperties, ::updateSong) + collectImmediately(detailModel.currentSong, detailModel.songAudioInfo, ::updateSong) } - private fun updateSong(song: Song?, properties: SongProperties?) { + private fun updateSong(song: Song?, info: AudioInfo?) { if (song == null) { // Song we were showing no longer exists. findNavController().navigateUp() @@ -65,28 +66,27 @@ class SongDetailDialog : ViewBindingDialogFragment() { } val binding = requireBinding() - if (properties != null) { - // Finished loading Song properties, populate and show the list of Song information. + if (info != null) { + // Finished loading song audio info, populate and show the list of Song information. binding.detailLoading.isInvisible = true binding.detailContainer.isInvisible = false val context = requireContext() binding.detailFileName.setText(song.path.name) binding.detailRelativeDir.setText(song.path.parent.resolveName(context)) - binding.detailFormat.setText(properties.resolvedMimeType.resolveName(context)) + binding.detailFormat.setText(info.resolvedMimeType.resolveName(context)) binding.detailSize.setText(Formatter.formatFileSize(context, song.size)) binding.detailDuration.setText(song.durationMs.formatDurationMs(true)) - if (properties.bitrateKbps != null) { - binding.detailBitrate.setText( - getString(R.string.fmt_bitrate, properties.bitrateKbps)) + if (info.bitrateKbps != null) { + binding.detailBitrate.setText(getString(R.string.fmt_bitrate, info.bitrateKbps)) } else { binding.detailBitrate.setText(R.string.def_bitrate) } - if (properties.sampleRateHz != null) { + if (info.sampleRateHz != null) { binding.detailSampleRate.setText( - getString(R.string.fmt_sample_rate, properties.sampleRateHz)) + getString(R.string.fmt_sample_rate, info.sampleRateHz)) } else { binding.detailSampleRate.setText(R.string.def_sample_rate) } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/recycler/DetailAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/recycler/DetailAdapter.kt index 2ce12a786..a529aa5ac 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/recycler/DetailAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/recycler/DetailAdapter.kt @@ -19,12 +19,13 @@ package org.oxycblt.auxio.detail.recycler import android.view.View import android.view.ViewGroup +import androidx.annotation.StringRes import androidx.appcompat.widget.TooltipCompat import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import org.oxycblt.auxio.IntegerTable import org.oxycblt.auxio.databinding.ItemSortHeaderBinding -import org.oxycblt.auxio.detail.SortHeader +import org.oxycblt.auxio.list.BasicHeader import org.oxycblt.auxio.list.Header import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.list.SelectableListListener @@ -52,21 +53,21 @@ abstract class DetailAdapter( override fun getItemViewType(position: Int) = when (getItem(position)) { // Implement support for headers and sort headers - is Header -> HeaderViewHolder.VIEW_TYPE + is BasicHeader -> BasicHeaderViewHolder.VIEW_TYPE is SortHeader -> SortHeaderViewHolder.VIEW_TYPE else -> super.getItemViewType(position) } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = when (viewType) { - HeaderViewHolder.VIEW_TYPE -> HeaderViewHolder.from(parent) + BasicHeaderViewHolder.VIEW_TYPE -> BasicHeaderViewHolder.from(parent) SortHeaderViewHolder.VIEW_TYPE -> SortHeaderViewHolder.from(parent) else -> error("Invalid item type $viewType") } override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { when (val item = getItem(position)) { - is Header -> (holder as HeaderViewHolder).bind(item) + is BasicHeader -> (holder as BasicHeaderViewHolder).bind(item) is SortHeader -> (holder as SortHeaderViewHolder).bind(item, listener) } } @@ -74,7 +75,7 @@ abstract class DetailAdapter( override fun isItemFullWidth(position: Int): Boolean { // Headers should be full-width in all configurations. val item = getItem(position) - return item is Header || item is SortHeader + return item is BasicHeader || item is SortHeader } /** An extended [SelectableListListener] for [DetailAdapter] implementations. */ @@ -105,8 +106,8 @@ abstract class DetailAdapter( object : SimpleDiffCallback() { override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean { return when { - oldItem is Header && newItem is Header -> - HeaderViewHolder.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem) + oldItem is BasicHeader && newItem is BasicHeader -> + BasicHeaderViewHolder.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem) oldItem is SortHeader && newItem is SortHeader -> SortHeaderViewHolder.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem) else -> false @@ -117,8 +118,15 @@ abstract class DetailAdapter( } /** - * A [RecyclerView.ViewHolder] that displays a [SortHeader], a variation on [Header] that adds a - * button opening a menu for sorting. Use [from] to create an instance. + * A header variation that displays a button to open a sort menu. + * @param titleRes The string resource to use as the header title + * @author Alexander Capehart (OxygenCobalt) + */ +data class SortHeader(@StringRes override val titleRes: Int) : Header + +/** + * A [RecyclerView.ViewHolder] that displays a [SortHeader], a variation on [BasicHeader] that adds + * a button opening a menu for sorting. Use [from] to create an instance. * @author Alexander Capehart (OxygenCobalt) */ private class SortHeaderViewHolder(private val binding: ItemSortHeaderBinding) : diff --git a/app/src/main/java/org/oxycblt/auxio/list/Data.kt b/app/src/main/java/org/oxycblt/auxio/list/Data.kt index 878a6a9d3..e77d0afb4 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/Data.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/Data.kt @@ -24,6 +24,16 @@ interface Item /** * A "header" used for delimiting groups of data. - * @param titleRes The string resource used for the header's title. + * @author Alexander Capehart (OxygenCobalt) */ -data class Header(@StringRes val titleRes: Int) : Item +interface Header : Item { + /** The string resource used for the header's title. */ + val titleRes: Int +} + +/** + * A basic header with no additional actions. + * @param titleRes The string resource used for the header's title. + * @author Alexander Capehart (OxygenCobalt) + */ +data class BasicHeader(@StringRes override val titleRes: Int) : Header diff --git a/app/src/main/java/org/oxycblt/auxio/list/recycler/ViewHolders.kt b/app/src/main/java/org/oxycblt/auxio/list/recycler/ViewHolders.kt index d72bf6814..aaafffc4b 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/recycler/ViewHolders.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/recycler/ViewHolders.kt @@ -24,7 +24,7 @@ import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.ItemHeaderBinding import org.oxycblt.auxio.databinding.ItemParentBinding import org.oxycblt.auxio.databinding.ItemSongBinding -import org.oxycblt.auxio.list.Header +import org.oxycblt.auxio.list.BasicHeader import org.oxycblt.auxio.list.SelectableListListener import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter import org.oxycblt.auxio.list.adapter.SimpleDiffCallback @@ -241,23 +241,23 @@ class GenreViewHolder private constructor(private val binding: ItemParentBinding } /** - * A [RecyclerView.ViewHolder] that displays a [Header]. Use [from] to create an instance. + * A [RecyclerView.ViewHolder] that displays a [BasicHeader]. Use [from] to create an instance. * @author Alexander Capehart (OxygenCobalt) */ -class HeaderViewHolder private constructor(private val binding: ItemHeaderBinding) : +class BasicHeaderViewHolder private constructor(private val binding: ItemHeaderBinding) : RecyclerView.ViewHolder(binding.root) { /** * Bind new data to this instance. - * @param header The new [Header] to bind. + * @param basicHeader The new [BasicHeader] to bind. */ - fun bind(header: Header) { - logD(binding.context.getString(header.titleRes)) - binding.title.text = binding.context.getString(header.titleRes) + fun bind(basicHeader: BasicHeader) { + logD(binding.context.getString(basicHeader.titleRes)) + binding.title.text = binding.context.getString(basicHeader.titleRes) } companion object { /** Unique ID for this ViewHolder type. */ - const val VIEW_TYPE = IntegerTable.VIEW_TYPE_HEADER + const val VIEW_TYPE = IntegerTable.VIEW_TYPE_BASIC_HEADER /** * Create a new instance. @@ -265,13 +265,15 @@ class HeaderViewHolder private constructor(private val binding: ItemHeaderBindin * @return A new instance. */ fun from(parent: View) = - HeaderViewHolder(ItemHeaderBinding.inflate(parent.context.inflater)) + BasicHeaderViewHolder(ItemHeaderBinding.inflate(parent.context.inflater)) /** A comparator that can be used with DiffUtil. */ val DIFF_CALLBACK = - object : SimpleDiffCallback
() { - override fun areContentsTheSame(oldItem: Header, newItem: Header): Boolean = - oldItem.titleRes == newItem.titleRes + object : SimpleDiffCallback() { + override fun areContentsTheSame( + oldItem: BasicHeader, + newItem: BasicHeader + ): Boolean = oldItem.titleRes == newItem.titleRes } } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/extractor/AudioInfo.kt b/app/src/main/java/org/oxycblt/auxio/music/extractor/AudioInfo.kt new file mode 100644 index 000000000..22f38d040 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/music/extractor/AudioInfo.kt @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2023 Auxio Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.oxycblt.auxio.music.extractor + +import android.content.Context +import android.media.MediaExtractor +import android.media.MediaFormat +import org.oxycblt.auxio.music.Song +import org.oxycblt.auxio.music.storage.MimeType +import org.oxycblt.auxio.util.logD +import org.oxycblt.auxio.util.logE +import org.oxycblt.auxio.util.logW + +/** + * 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. + * @author Alexander Capehart (OxygenCobalt) + */ +data class AudioInfo( + val bitrateKbps: Int?, + val sampleRateHz: Int?, + val resolvedMimeType: MimeType +) { + /** Implements the process of extracting [AudioInfo] from a given [Song]. */ + interface Provider { + /** + * Extract the [AudioInfo] of a given [Song]. + * @param song The [Song] to read. + * @return The [AudioInfo] of the [Song], if possible to obtain. + */ + suspend fun extract(song: Song): AudioInfo + + companion object { + /** + * Get a framework-backed implementation. + * @param context [Context] required. + */ + fun from(context: Context): Provider = RealAudioInfoProvider(context) + } + } +} + +private class RealAudioInfoProvider(private val context: Context) : AudioInfo.Provider { + // 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. + private val extractor = MediaExtractor() + + override suspend fun extract(song: Song): AudioInfo { + try { + extractor.setDataSource(context, song.uri, emptyMap()) + } catch (e: Exception) { + // Can feasibly fail with invalid file formats. Note that this isn't considered + // an error condition in the UI, as there is still plenty of other song information + // that we can show. + logW("Unable to extract song attributes.") + logW(e.stackTraceToString()) + return AudioInfo(null, null, song.mimeType) + } + + // Get the first track from the extractor (This is basically always the only + // track we need to analyze). + val format = extractor.getTrackFormat(0) + + // Accessing fields can throw an exception if the fields are not present, and + // the new method for using default values is not available on lower API levels. + // So, we are forced to handle the exception and map it to a saner null value. + val bitrate = + try { + // Convert bytes-per-second to kilobytes-per-second. + format.getInteger(MediaFormat.KEY_BIT_RATE) / 1000 + } catch (e: NullPointerException) { + logD("Unable to extract bit rate field") + null + } + + val sampleRate = + try { + format.getInteger(MediaFormat.KEY_SAMPLE_RATE) + } catch (e: NullPointerException) { + logE("Unable to extract sample rate field") + null + } + + val resolvedMimeType = + if (song.mimeType.fromFormat != null) { + // ExoPlayer was already able to populate the format. + song.mimeType + } else { + // ExoPlayer couldn't populate the format somehow, populate it here. + val formatMimeType = + try { + format.getString(MediaFormat.KEY_MIME) + } catch (e: NullPointerException) { + logE("Unable to extract mime type field") + null + } + + MimeType(song.mimeType.fromExtension, formatMimeType) + } + + extractor.release() + + return AudioInfo(bitrate, sampleRate, resolvedMimeType) + } +} diff --git a/app/src/main/java/org/oxycblt/auxio/search/SearchAdapter.kt b/app/src/main/java/org/oxycblt/auxio/search/SearchAdapter.kt index 62c157bd8..ae1350f60 100644 --- a/app/src/main/java/org/oxycblt/auxio/search/SearchAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchAdapter.kt @@ -44,7 +44,7 @@ class SearchAdapter(private val listener: SelectableListListener) : is Album -> AlbumViewHolder.VIEW_TYPE is Artist -> ArtistViewHolder.VIEW_TYPE is Genre -> GenreViewHolder.VIEW_TYPE - is Header -> HeaderViewHolder.VIEW_TYPE + is BasicHeader -> BasicHeaderViewHolder.VIEW_TYPE else -> super.getItemViewType(position) } @@ -54,7 +54,7 @@ class SearchAdapter(private val listener: SelectableListListener) : AlbumViewHolder.VIEW_TYPE -> AlbumViewHolder.from(parent) ArtistViewHolder.VIEW_TYPE -> ArtistViewHolder.from(parent) GenreViewHolder.VIEW_TYPE -> GenreViewHolder.from(parent) - HeaderViewHolder.VIEW_TYPE -> HeaderViewHolder.from(parent) + BasicHeaderViewHolder.VIEW_TYPE -> BasicHeaderViewHolder.from(parent) else -> error("Invalid item type $viewType") } @@ -65,11 +65,11 @@ class SearchAdapter(private val listener: SelectableListListener) : is Album -> (holder as AlbumViewHolder).bind(item, listener) is Artist -> (holder as ArtistViewHolder).bind(item, listener) is Genre -> (holder as GenreViewHolder).bind(item, listener) - is Header -> (holder as HeaderViewHolder).bind(item) + is BasicHeader -> (holder as BasicHeaderViewHolder).bind(item) } } - override fun isItemFullWidth(position: Int) = getItem(position) is Header + override fun isItemFullWidth(position: Int) = getItem(position) is BasicHeader /** * Make sure that the top header has a correctly configured divider visibility. This would @@ -94,8 +94,8 @@ class SearchAdapter(private val listener: SelectableListListener) : ArtistViewHolder.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem) oldItem is Genre && newItem is Genre -> GenreViewHolder.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem) - oldItem is Header && newItem is Header -> - HeaderViewHolder.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem) + oldItem is BasicHeader && newItem is BasicHeader -> + BasicHeaderViewHolder.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem) else -> false } } diff --git a/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt b/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt index 7a6b6f968..f69851cdd 100644 --- a/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt @@ -27,7 +27,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import kotlinx.coroutines.yield import org.oxycblt.auxio.R -import org.oxycblt.auxio.list.Header +import org.oxycblt.auxio.list.BasicHeader import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.music.* import org.oxycblt.auxio.music.MusicStore @@ -119,19 +119,19 @@ class SearchViewModel(application: Application) : return buildList { results.artists?.let { artists -> - add(Header(R.string.lbl_artists)) + add(BasicHeader(R.string.lbl_artists)) addAll(SORT.artists(artists)) } results.albums?.let { albums -> - add(Header(R.string.lbl_albums)) + add(BasicHeader(R.string.lbl_albums)) addAll(SORT.albums(albums)) } results.genres?.let { genres -> - add(Header(R.string.lbl_genres)) + add(BasicHeader(R.string.lbl_genres)) addAll(SORT.genres(genres)) } results.songs?.let { songs -> - add(Header(R.string.lbl_songs)) + add(BasicHeader(R.string.lbl_songs)) addAll(SORT.songs(songs)) } } diff --git a/app/src/main/res/layout/item_header.xml b/app/src/main/res/layout/item_header.xml index 0f7e21fa8..c623a41ee 100644 --- a/app/src/main/res/layout/item_header.xml +++ b/app/src/main/res/layout/item_header.xml @@ -1,15 +1,8 @@ - - - - - \ No newline at end of file + tools:text="Songs" /> \ No newline at end of file diff --git a/app/src/main/res/layout/item_sort_header.xml b/app/src/main/res/layout/item_sort_header.xml index f98c5488e..58738f8cf 100644 --- a/app/src/main/res/layout/item_sort_header.xml +++ b/app/src/main/res/layout/item_sort_header.xml @@ -1,38 +1,31 @@ - - -