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 @@
-
-
-
-
-
\ No newline at end of file
+
\ No newline at end of file