all: cleanup

General cleanup
This commit is contained in:
Alexander Capehart 2022-11-16 09:05:51 -07:00
parent a7bd48b64a
commit 086f7836bd
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
92 changed files with 822 additions and 1084 deletions

View file

@ -118,7 +118,7 @@ dependencies {
spotless { spotless {
kotlin { kotlin {
target "src/**/*.kt" target "src/**/*.kt"
ktlint() ktfmt().dropboxStyle()
licenseHeaderFile("NOTICE") licenseHeaderFile("NOTICE")
} }
} }

View file

@ -50,11 +50,8 @@ class AuxioApp : Application(), ImageLoaderFactory {
.setIcon(IconCompat.createWithResource(this, R.drawable.ic_shortcut_shuffle_24)) .setIcon(IconCompat.createWithResource(this, R.drawable.ic_shortcut_shuffle_24))
.setIntent( .setIntent(
Intent(this, MainActivity::class.java) Intent(this, MainActivity::class.java)
.setAction(INTENT_KEY_SHORTCUT_SHUFFLE) .setAction(INTENT_KEY_SHORTCUT_SHUFFLE))
) .build()))
.build()
)
)
} }
override fun newImageLoader() = override fun newImageLoader() =

View file

@ -22,7 +22,6 @@ import android.os.Bundle
import android.view.View import android.view.View
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.core.app.ActivityCompat
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import org.oxycblt.auxio.databinding.ActivityMainBinding import org.oxycblt.auxio.databinding.ActivityMainBinding

View file

@ -31,6 +31,8 @@ import androidx.navigation.fragment.findNavController
import com.google.android.material.bottomsheet.NeoBottomSheetBehavior import com.google.android.material.bottomsheet.NeoBottomSheetBehavior
import com.google.android.material.shape.MaterialShapeDrawable import com.google.android.material.shape.MaterialShapeDrawable
import com.google.android.material.transition.MaterialFadeThrough import com.google.android.material.transition.MaterialFadeThrough
import kotlin.math.max
import kotlin.math.min
import org.oxycblt.auxio.databinding.FragmentMainBinding import org.oxycblt.auxio.databinding.FragmentMainBinding
import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
@ -49,8 +51,6 @@ import org.oxycblt.auxio.util.getAttrColorCompat
import org.oxycblt.auxio.util.getDimen import org.oxycblt.auxio.util.getDimen
import org.oxycblt.auxio.util.systemBarInsetsCompat import org.oxycblt.auxio.util.systemBarInsetsCompat
import org.oxycblt.auxio.util.unlikelyToBeNull import org.oxycblt.auxio.util.unlikelyToBeNull
import kotlin.math.max
import kotlin.math.min
/** /**
* A wrapper around the home fragment that shows the playback fragment and controls the more * A wrapper around the home fragment that shows the playback fragment and controls the more
@ -89,13 +89,9 @@ class MainFragment :
// Send meaningful accessibility events for bottom sheets // Send meaningful accessibility events for bottom sheets
ViewCompat.setAccessibilityPaneTitle( ViewCompat.setAccessibilityPaneTitle(
binding.playbackSheet, binding.playbackSheet, context.getString(R.string.lbl_playback))
context.getString(R.string.lbl_playback)
)
ViewCompat.setAccessibilityPaneTitle( ViewCompat.setAccessibilityPaneTitle(
binding.queueSheet, binding.queueSheet, context.getString(R.string.lbl_queue))
context.getString(R.string.lbl_queue)
)
val queueSheetBehavior = binding.queueSheet.coordinatorLayoutBehavior as QueueSheetBehavior? val queueSheetBehavior = binding.queueSheet.coordinatorLayoutBehavior as QueueSheetBehavior?
if (queueSheetBehavior != null) { if (queueSheetBehavior != null) {
@ -104,8 +100,7 @@ class MainFragment :
unlikelyToBeNull(binding.handleWrapper).setOnClickListener { unlikelyToBeNull(binding.handleWrapper).setOnClickListener {
if (playbackSheetBehavior.state == NeoBottomSheetBehavior.STATE_EXPANDED && if (playbackSheetBehavior.state == NeoBottomSheetBehavior.STATE_EXPANDED &&
queueSheetBehavior.state == NeoBottomSheetBehavior.STATE_COLLAPSED queueSheetBehavior.state == NeoBottomSheetBehavior.STATE_COLLAPSED) {
) {
queueSheetBehavior.state = NeoBottomSheetBehavior.STATE_EXPANDED queueSheetBehavior.state = NeoBottomSheetBehavior.STATE_EXPANDED
} }
} }
@ -232,8 +227,7 @@ class MainFragment :
when (action) { when (action) {
is MainNavigationAction.Expand -> tryExpandAll() is MainNavigationAction.Expand -> tryExpandAll()
is MainNavigationAction.Collapse -> tryCollapseAll() is MainNavigationAction.Collapse -> tryCollapseAll()
is MainNavigationAction.Directions -> is MainNavigationAction.Directions -> findNavController().navigate(action.directions)
findNavController().navigate(action.directions)
} }
navModel.finishMainNavigation() navModel.finishMainNavigation()
@ -328,16 +322,14 @@ class MainFragment :
if (queueSheetBehavior != null && if (queueSheetBehavior != null &&
queueSheetBehavior.state != NeoBottomSheetBehavior.STATE_COLLAPSED && queueSheetBehavior.state != NeoBottomSheetBehavior.STATE_COLLAPSED &&
playbackSheetBehavior.state == NeoBottomSheetBehavior.STATE_EXPANDED playbackSheetBehavior.state == NeoBottomSheetBehavior.STATE_EXPANDED) {
) {
// Collapse the queue first if it is expanded. // Collapse the queue first if it is expanded.
queueSheetBehavior.state = NeoBottomSheetBehavior.STATE_COLLAPSED queueSheetBehavior.state = NeoBottomSheetBehavior.STATE_COLLAPSED
return return
} }
if (playbackSheetBehavior.state != NeoBottomSheetBehavior.STATE_COLLAPSED && if (playbackSheetBehavior.state != NeoBottomSheetBehavior.STATE_COLLAPSED &&
playbackSheetBehavior.state != NeoBottomSheetBehavior.STATE_HIDDEN playbackSheetBehavior.state != NeoBottomSheetBehavior.STATE_HIDDEN) {
) {
// Then collapse the playback sheet. // Then collapse the playback sheet.
playbackSheetBehavior.state = NeoBottomSheetBehavior.STATE_COLLAPSED playbackSheetBehavior.state = NeoBottomSheetBehavior.STATE_COLLAPSED
return return
@ -357,9 +349,9 @@ class MainFragment :
isEnabled = isEnabled =
playbackSheetBehavior.state == NeoBottomSheetBehavior.STATE_EXPANDED || playbackSheetBehavior.state == NeoBottomSheetBehavior.STATE_EXPANDED ||
queueSheetBehavior?.state == NeoBottomSheetBehavior.STATE_EXPANDED || queueSheetBehavior?.state == NeoBottomSheetBehavior.STATE_EXPANDED ||
exploreNavController.currentDestination?.id != exploreNavController.currentDestination?.id !=
exploreNavController.graph.startDestinationId exploreNavController.graph.startDestinationId
} }
} }
} }

View file

@ -29,7 +29,6 @@ import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs import androidx.navigation.fragment.navArgs
import androidx.recyclerview.widget.LinearSmoothScroller import androidx.recyclerview.widget.LinearSmoothScroller
import com.google.android.material.transition.MaterialSharedAxis import com.google.android.material.transition.MaterialSharedAxis
import org.oxycblt.auxio.MainFragmentDirections
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentDetailBinding import org.oxycblt.auxio.databinding.FragmentDetailBinding
import org.oxycblt.auxio.detail.recycler.AlbumDetailAdapter import org.oxycblt.auxio.detail.recycler.AlbumDetailAdapter
@ -42,7 +41,6 @@ import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.Sort import org.oxycblt.auxio.music.Sort
import org.oxycblt.auxio.music.picker.PickerMode import org.oxycblt.auxio.music.picker.PickerMode
import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.ui.MainNavigationAction
import org.oxycblt.auxio.ui.fragment.MusicFragment import org.oxycblt.auxio.ui.fragment.MusicFragment
import org.oxycblt.auxio.ui.recycler.Item import org.oxycblt.auxio.ui.recycler.Item
import org.oxycblt.auxio.util.canScroll import org.oxycblt.auxio.util.canScroll
@ -93,11 +91,7 @@ class AlbumDetailFragment :
collectImmediately(detailModel.currentAlbum, ::handleItemChange) collectImmediately(detailModel.currentAlbum, ::handleItemChange)
collectImmediately(detailModel.albumData, detailAdapter::submitList) collectImmediately(detailModel.albumData, detailAdapter::submitList)
collectImmediately( collectImmediately(
playbackModel.song, playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
playbackModel.parent,
playbackModel.isPlaying,
::updatePlayback
)
collect(navModel.exploreNavigationItem, ::handleNavigation) collect(navModel.exploreNavigationItem, ::handleNavigation)
} }
@ -130,7 +124,8 @@ class AlbumDetailFragment :
override fun onItemClick(item: Item) { override fun onItemClick(item: Item) {
check(item is Song) { "Unexpected datatype: ${item::class.simpleName}" } check(item is Song) { "Unexpected datatype: ${item::class.simpleName}" }
when (settings.detailPlaybackMode) { when (settings.detailPlaybackMode) {
null, MusicMode.ALBUMS -> playbackModel.playFromAlbum(item) null,
MusicMode.ALBUMS -> playbackModel.playFromAlbum(item)
MusicMode.SONGS -> playbackModel.playFromAll(item) MusicMode.SONGS -> playbackModel.playFromAll(item)
MusicMode.ARTISTS -> doArtistDependentAction(item, PickerMode.PLAY) MusicMode.ARTISTS -> doArtistDependentAction(item, PickerMode.PLAY)
else -> error("Unexpected playback mode: ${settings.detailPlaybackMode}") else -> error("Unexpected playback mode: ${settings.detailPlaybackMode}")
@ -233,8 +228,7 @@ class AlbumDetailFragment :
binding.detailRecycler.post { binding.detailRecycler.post {
// Make sure to increment the position to make up for the detail header // Make sure to increment the position to make up for the detail header
binding.detailRecycler.layoutManager?.startSmoothScroll( binding.detailRecycler.layoutManager?.startSmoothScroll(
CenterSmoothScroller(requireContext(), pos) CenterSmoothScroller(requireContext(), pos))
)
// If the recyclerview can scroll, its certain that it will have to scroll to // If the recyclerview can scroll, its certain that it will have to scroll to
// correctly center the playing item, so make sure that the Toolbar is lifted in // correctly center the playing item, so make sure that the Toolbar is lifted in

View file

@ -26,7 +26,6 @@ import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs import androidx.navigation.fragment.navArgs
import com.google.android.material.transition.MaterialSharedAxis import com.google.android.material.transition.MaterialSharedAxis
import org.oxycblt.auxio.MainFragmentDirections
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentDetailBinding import org.oxycblt.auxio.databinding.FragmentDetailBinding
import org.oxycblt.auxio.detail.recycler.ArtistDetailAdapter import org.oxycblt.auxio.detail.recycler.ArtistDetailAdapter
@ -40,7 +39,6 @@ import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.Sort import org.oxycblt.auxio.music.Sort
import org.oxycblt.auxio.music.picker.PickerMode import org.oxycblt.auxio.music.picker.PickerMode
import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.ui.MainNavigationAction
import org.oxycblt.auxio.ui.fragment.MusicFragment import org.oxycblt.auxio.ui.fragment.MusicFragment
import org.oxycblt.auxio.ui.recycler.Item import org.oxycblt.auxio.ui.recycler.Item
import org.oxycblt.auxio.util.collect import org.oxycblt.auxio.util.collect
@ -55,7 +53,9 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
* @author OxygenCobalt * @author OxygenCobalt
*/ */
class ArtistDetailFragment : class ArtistDetailFragment :
MusicFragment<FragmentDetailBinding>(), Toolbar.OnMenuItemClickListener, DetailAdapter.Listener { MusicFragment<FragmentDetailBinding>(),
Toolbar.OnMenuItemClickListener,
DetailAdapter.Listener {
private val detailModel: DetailViewModel by activityViewModels() private val detailModel: DetailViewModel by activityViewModels()
private val args: ArtistDetailFragmentArgs by navArgs() private val args: ArtistDetailFragmentArgs by navArgs()
@ -88,11 +88,7 @@ class ArtistDetailFragment :
collectImmediately(detailModel.currentArtist, ::handleItemChange) collectImmediately(detailModel.currentArtist, ::handleItemChange)
collectImmediately(detailModel.artistData, detailAdapter::submitList) collectImmediately(detailModel.artistData, detailAdapter::submitList)
collectImmediately( collectImmediately(
playbackModel.song, playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
playbackModel.parent,
playbackModel.isPlaying,
::updatePlayback
)
collect(navModel.exploreNavigationItem, ::handleNavigation) collect(navModel.exploreNavigationItem, ::handleNavigation)
} }
@ -122,7 +118,9 @@ class ArtistDetailFragment :
when (item) { when (item) {
is Song -> { is Song -> {
when (settings.detailPlaybackMode) { when (settings.detailPlaybackMode) {
null -> playbackModel.playFromArtist(item, unlikelyToBeNull(detailModel.currentArtist.value)) null ->
playbackModel.playFromArtist(
item, unlikelyToBeNull(detailModel.currentArtist.value))
MusicMode.SONGS -> playbackModel.playFromAll(item) MusicMode.SONGS -> playbackModel.playFromAll(item)
MusicMode.ALBUMS -> playbackModel.playFromAlbum(item) MusicMode.ALBUMS -> playbackModel.playFromAlbum(item)
MusicMode.ARTISTS -> doArtistDependentAction(item, PickerMode.PLAY) MusicMode.ARTISTS -> doArtistDependentAction(item, PickerMode.PLAY)

View file

@ -29,10 +29,10 @@ import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.AppBarLayout
import java.lang.reflect.Field
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.ui.AuxioAppBarLayout import org.oxycblt.auxio.ui.AuxioAppBarLayout
import org.oxycblt.auxio.util.lazyReflectedField import org.oxycblt.auxio.util.lazyReflectedField
import java.lang.reflect.Field
/** /**
* An [AuxioAppBarLayout] variant that also shows the name of the toolbar whenever the detail * An [AuxioAppBarLayout] variant that also shows the name of the toolbar whenever the detail
@ -145,6 +145,6 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
private const val TOOLBAR_FADE_DURATION = 150L private const val TOOLBAR_FADE_DURATION = 150L
private val TOOLBAR_TITLE_TEXT_FIELD: Field by private val TOOLBAR_TITLE_TEXT_FIELD: Field by
lazyReflectedField(Toolbar::class, "mTitleTextView") lazyReflectedField(Toolbar::class, "mTitleTextView")
} }
} }

View file

@ -160,11 +160,12 @@ class DetailViewModel(application: Application) :
private fun generateDetailSong(song: Song) { private fun generateDetailSong(song: Song) {
currentSongJob?.cancel() currentSongJob?.cancel()
_currentSong.value = DetailSong(song, null) _currentSong.value = DetailSong(song, null)
currentSongJob = viewModelScope.launch(Dispatchers.IO) { currentSongJob =
val info = generateDetailSongInfo(song) viewModelScope.launch(Dispatchers.IO) {
yield() val info = generateDetailSongInfo(song)
_currentSong.value = DetailSong(song, info) yield()
} _currentSong.value = DetailSong(song, info)
}
} }
private fun generateDetailSongInfo(song: Song): SongInfo { private fun generateDetailSongInfo(song: Song): SongInfo {

View file

@ -26,7 +26,6 @@ import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs import androidx.navigation.fragment.navArgs
import com.google.android.material.transition.MaterialSharedAxis import com.google.android.material.transition.MaterialSharedAxis
import org.oxycblt.auxio.MainFragmentDirections
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentDetailBinding import org.oxycblt.auxio.databinding.FragmentDetailBinding
import org.oxycblt.auxio.detail.recycler.DetailAdapter import org.oxycblt.auxio.detail.recycler.DetailAdapter
@ -41,7 +40,6 @@ import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.Sort import org.oxycblt.auxio.music.Sort
import org.oxycblt.auxio.music.picker.PickerMode import org.oxycblt.auxio.music.picker.PickerMode
import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.ui.MainNavigationAction
import org.oxycblt.auxio.ui.fragment.MusicFragment import org.oxycblt.auxio.ui.fragment.MusicFragment
import org.oxycblt.auxio.ui.recycler.Item import org.oxycblt.auxio.ui.recycler.Item
import org.oxycblt.auxio.util.collect import org.oxycblt.auxio.util.collect
@ -56,7 +54,9 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
* @author OxygenCobalt * @author OxygenCobalt
*/ */
class GenreDetailFragment : class GenreDetailFragment :
MusicFragment<FragmentDetailBinding>(), Toolbar.OnMenuItemClickListener, DetailAdapter.Listener { MusicFragment<FragmentDetailBinding>(),
Toolbar.OnMenuItemClickListener,
DetailAdapter.Listener {
private val detailModel: DetailViewModel by activityViewModels() private val detailModel: DetailViewModel by activityViewModels()
private val args: GenreDetailFragmentArgs by navArgs() private val args: GenreDetailFragmentArgs by navArgs()
@ -89,11 +89,7 @@ class GenreDetailFragment :
collectImmediately(detailModel.currentGenre, ::handleItemChange) collectImmediately(detailModel.currentGenre, ::handleItemChange)
collectImmediately(detailModel.genreData, detailAdapter::submitList) collectImmediately(detailModel.genreData, detailAdapter::submitList)
collectImmediately( collectImmediately(
playbackModel.song, playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
playbackModel.parent,
playbackModel.isPlaying,
::updatePlayback
)
collect(navModel.exploreNavigationItem, ::handleNavigation) collect(navModel.exploreNavigationItem, ::handleNavigation)
} }
@ -122,15 +118,16 @@ class GenreDetailFragment :
override fun onItemClick(item: Item) { override fun onItemClick(item: Item) {
when (item) { when (item) {
is Artist -> navModel.exploreNavigateTo(item) is Artist -> navModel.exploreNavigateTo(item)
is Song ->
is Song -> when (settings.detailPlaybackMode) { when (settings.detailPlaybackMode) {
null -> playbackModel.playFromGenre(item, unlikelyToBeNull(detailModel.currentGenre.value)) null ->
MusicMode.SONGS -> playbackModel.playFromAll(item) playbackModel.playFromGenre(
MusicMode.ALBUMS -> playbackModel.playFromAlbum(item) item, unlikelyToBeNull(detailModel.currentGenre.value))
MusicMode.ARTISTS -> doArtistDependentAction(item, PickerMode.PLAY) MusicMode.SONGS -> playbackModel.playFromAll(item)
else -> error("Unexpected playback mode: ${settings.detailPlaybackMode}") MusicMode.ALBUMS -> playbackModel.playFromAlbum(item)
} MusicMode.ARTISTS -> doArtistDependentAction(item, PickerMode.PLAY)
else -> error("Unexpected playback mode: ${settings.detailPlaybackMode}")
}
else -> error("Unexpected datatype: ${item::class.simpleName}") else -> error("Unexpected datatype: ${item::class.simpleName}")
} }
} }

View file

@ -74,16 +74,14 @@ class SongDetailDialog : ViewBindingDialogFragment<DialogSongDetailBinding>() {
if (song.info.bitrateKbps != null) { if (song.info.bitrateKbps != null) {
binding.detailBitrate.setText( binding.detailBitrate.setText(
getString(R.string.fmt_bitrate, song.info.bitrateKbps) getString(R.string.fmt_bitrate, song.info.bitrateKbps))
)
} else { } else {
binding.detailBitrate.setText(R.string.def_bitrate) binding.detailBitrate.setText(R.string.def_bitrate)
} }
if (song.info.sampleRate != null) { if (song.info.sampleRate != null) {
binding.detailSampleRate.setText( binding.detailSampleRate.setText(
getString(R.string.fmt_sample_rate, song.info.sampleRate) getString(R.string.fmt_sample_rate, song.info.sampleRate))
)
} else { } else {
binding.detailSampleRate.setText(R.string.def_sample_rate) binding.detailSampleRate.setText(R.string.def_sample_rate)
} }

View file

@ -119,9 +119,7 @@ private class AlbumDetailViewHolder private constructor(private val binding: Ite
} }
binding.detailInfo.apply { binding.detailInfo.apply {
val date = val date = item.date?.resolveDate(context) ?: context.getString(R.string.def_date)
item.date?.resolveDate(context)
?: context.getString(R.string.def_date)
val songCount = context.getPlural(R.plurals.fmt_song_count, item.songs.size) val songCount = context.getPlural(R.plurals.fmt_song_count, item.songs.size)

View file

@ -118,8 +118,7 @@ private class ArtistDetailViewHolder private constructor(private val binding: It
binding.context.getString( binding.context.getString(
R.string.fmt_two, R.string.fmt_two,
binding.context.getPlural(R.plurals.fmt_album_count, item.albums.size), binding.context.getPlural(R.plurals.fmt_album_count, item.albums.size),
binding.context.getPlural(R.plurals.fmt_song_count, item.songs.size) binding.context.getPlural(R.plurals.fmt_song_count, item.songs.size))
)
binding.detailPlayButton.isVisible = true binding.detailPlayButton.isVisible = true
binding.detailShuffleButton.isVisible = true binding.detailShuffleButton.isVisible = true
@ -143,13 +142,14 @@ private class ArtistDetailViewHolder private constructor(private val binding: It
fun new(parent: View) = fun new(parent: View) =
ArtistDetailViewHolder(ItemDetailBinding.inflate(parent.context.inflater)) ArtistDetailViewHolder(ItemDetailBinding.inflate(parent.context.inflater))
val DIFFER = object : SimpleItemCallback<Artist>() { val DIFFER =
override fun areContentsTheSame(oldItem: Artist, newItem: Artist) = object : SimpleItemCallback<Artist>() {
oldItem.rawName == newItem.rawName && override fun areContentsTheSame(oldItem: Artist, newItem: Artist) =
oldItem.areGenreContentsTheSame(newItem) && oldItem.rawName == newItem.rawName &&
oldItem.albums.size == newItem.albums.size && oldItem.areGenreContentsTheSame(newItem) &&
oldItem.songs.size == newItem.songs.size oldItem.albums.size == newItem.albums.size &&
} oldItem.songs.size == newItem.songs.size
}
} }
} }
@ -159,8 +159,7 @@ private class ArtistAlbumViewHolder private constructor(private val binding: Ite
binding.parentImage.bind(item) binding.parentImage.bind(item)
binding.parentName.text = item.resolveName(binding.context) binding.parentName.text = item.resolveName(binding.context)
binding.parentInfo.text = binding.parentInfo.text =
item.date?.resolveDate(binding.context) item.date?.resolveDate(binding.context) ?: binding.context.getString(R.string.def_date)
?: binding.context.getString(R.string.def_date)
// binding.parentMenu.setOnClickListener { listener.onOpenMenu(item, it) } // binding.parentMenu.setOnClickListener { listener.onOpenMenu(item, it) }
binding.root.setOnLongClickListener { binding.root.setOnLongClickListener {

View file

@ -35,7 +35,6 @@ import org.oxycblt.auxio.ui.recycler.MenuItemListener
import org.oxycblt.auxio.ui.recycler.SimpleItemCallback import org.oxycblt.auxio.ui.recycler.SimpleItemCallback
import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.context
import org.oxycblt.auxio.util.inflater import org.oxycblt.auxio.util.inflater
import org.oxycblt.auxio.util.logD
abstract class DetailAdapter<L : DetailAdapter.Listener>( abstract class DetailAdapter<L : DetailAdapter.Listener>(
private val listener: L, private val listener: L,
@ -43,8 +42,7 @@ abstract class DetailAdapter<L : DetailAdapter.Listener>(
) : IndicatorAdapter<RecyclerView.ViewHolder>(), AuxioRecyclerView.SpanSizeLookup { ) : IndicatorAdapter<RecyclerView.ViewHolder>(), AuxioRecyclerView.SpanSizeLookup {
private var isPlaying = false private var isPlaying = false
@Suppress("LeakingThis") @Suppress("LeakingThis") override fun getItemCount() = differ.currentList.size
override fun getItemCount() = differ.currentList.size
override fun getItemViewType(position: Int) = override fun getItemViewType(position: Int) =
when (differ.currentList[position]) { when (differ.currentList[position]) {
@ -85,8 +83,7 @@ abstract class DetailAdapter<L : DetailAdapter.Listener>(
return item is Header || item is SortHeader return item is Header || item is SortHeader
} }
@Suppress("LeakingThis") @Suppress("LeakingThis") protected val differ = AsyncListDiffer(this, diffCallback)
protected val differ = AsyncListDiffer(this, diffCallback)
override val currentList: List<Item> override val currentList: List<Item>
get() = differ.currentList get() = differ.currentList

View file

@ -27,9 +27,7 @@ import org.oxycblt.auxio.databinding.ItemDetailBinding
import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.formatDurationMs
import org.oxycblt.auxio.ui.recycler.ArtistViewHolder import org.oxycblt.auxio.ui.recycler.ArtistViewHolder
import org.oxycblt.auxio.ui.recycler.Header
import org.oxycblt.auxio.ui.recycler.Item import org.oxycblt.auxio.ui.recycler.Item
import org.oxycblt.auxio.ui.recycler.SimpleItemCallback import org.oxycblt.auxio.ui.recycler.SimpleItemCallback
import org.oxycblt.auxio.ui.recycler.SongViewHolder import org.oxycblt.auxio.ui.recycler.SongViewHolder
@ -105,11 +103,11 @@ private class GenreDetailViewHolder private constructor(private val binding: Ite
binding.detailType.text = binding.context.getString(R.string.lbl_genre) binding.detailType.text = binding.context.getString(R.string.lbl_genre)
binding.detailName.text = item.resolveName(binding.context) binding.detailName.text = item.resolveName(binding.context)
binding.detailSubhead.isVisible = false binding.detailSubhead.isVisible = false
binding.detailInfo.text = binding.context.getString( binding.detailInfo.text =
binding.context.getString(
R.string.fmt_two, R.string.fmt_two,
binding.context.getPlural(R.plurals.fmt_artist_count, item.artists.size), binding.context.getPlural(R.plurals.fmt_artist_count, item.artists.size),
binding.context.getPlural(R.plurals.fmt_song_count, item.songs.size) binding.context.getPlural(R.plurals.fmt_song_count, item.songs.size))
)
binding.detailPlayButton.setOnClickListener { listener.onPlayParent() } binding.detailPlayButton.setOnClickListener { listener.onPlayParent() }
binding.detailShuffleButton.setOnClickListener { listener.onShuffleParent() } binding.detailShuffleButton.setOnClickListener { listener.onShuffleParent() }

View file

@ -36,6 +36,8 @@ import androidx.viewpager2.widget.ViewPager2
import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.tabs.TabLayoutMediator import com.google.android.material.tabs.TabLayoutMediator
import com.google.android.material.transition.MaterialSharedAxis import com.google.android.material.transition.MaterialSharedAxis
import java.lang.reflect.Field
import kotlin.math.abs
import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.MainFragmentDirections import org.oxycblt.auxio.MainFragmentDirections
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
@ -63,8 +65,6 @@ import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.getColorCompat import org.oxycblt.auxio.util.getColorCompat
import org.oxycblt.auxio.util.lazyReflectedField import org.oxycblt.auxio.util.lazyReflectedField
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import java.lang.reflect.Field
import kotlin.math.abs
/** /**
* The main "Launching Point" fragment of Auxio, allowing navigation to the detail views for each * The main "Launching Point" fragment of Auxio, allowing navigation to the detail views for each
@ -112,8 +112,7 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
binding.homeToolbar.alpha = 1f - (abs(offset.toFloat()) / (range.toFloat() / 2)) binding.homeToolbar.alpha = 1f - (abs(offset.toFloat()) / (range.toFloat() / 2))
binding.homeContent.updatePadding( binding.homeContent.updatePadding(
bottom = binding.homeAppbar.totalScrollRange + offset bottom = binding.homeAppbar.totalScrollRange + offset)
)
} }
} }
@ -140,8 +139,7 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
object : ViewPager2.OnPageChangeCallback() { object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) = override fun onPageSelected(position: Int) =
homeModel.updateCurrentTab(position) homeModel.updateCurrentTab(position)
} })
)
TabLayoutMediator(binding.homeTabs, this, AdaptiveTabStrategy(context, homeModel)) TabLayoutMediator(binding.homeTabs, this, AdaptiveTabStrategy(context, homeModel))
.attach() .attach()
@ -186,11 +184,13 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
} }
R.id.action_settings -> { R.id.action_settings -> {
logD("Navigating to settings") logD("Navigating to settings")
navModel.mainNavigateTo(MainNavigationAction.Directions(MainFragmentDirections.actionShowSettings())) navModel.mainNavigateTo(
MainNavigationAction.Directions(MainFragmentDirections.actionShowSettings()))
} }
R.id.action_about -> { R.id.action_about -> {
logD("Navigating to about") logD("Navigating to about")
navModel.mainNavigateTo(MainNavigationAction.Directions(MainFragmentDirections.actionShowAbout())) navModel.mainNavigateTo(
MainNavigationAction.Directions(MainFragmentDirections.actionShowAbout()))
} }
R.id.submenu_sorting -> { R.id.submenu_sorting -> {
// Junk click event when opening the menu // Junk click event when opening the menu
@ -200,8 +200,7 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
homeModel.updateCurrentSort( homeModel.updateCurrentSort(
homeModel homeModel
.getSortForTab(homeModel.currentTab.value) .getSortForTab(homeModel.currentTab.value)
.withAscending(item.isChecked) .withAscending(item.isChecked))
)
} }
else -> { else -> {
// Sorting option was selected, mark it as selected and update the mode // Sorting option was selected, mark it as selected and update the mode
@ -209,8 +208,7 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
homeModel.updateCurrentSort( homeModel.updateCurrentSort(
homeModel homeModel
.getSortForTab(homeModel.currentTab.value) .getSortForTab(homeModel.currentTab.value)
.withMode(requireNotNull(Sort.Mode.fromItemId(item.itemId))) .withMode(requireNotNull(Sort.Mode.fromItemId(item.itemId))))
)
} }
} }
@ -289,7 +287,7 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
binding.homeTabs.isVisible = true binding.homeTabs.isVisible = true
toolbarParams.scrollFlags = toolbarParams.scrollFlags =
AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL or AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL or
AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS
} }
} }
@ -445,9 +443,9 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
companion object { companion object {
private val VIEW_PAGER_RECYCLER_FIELD: Field by private val VIEW_PAGER_RECYCLER_FIELD: Field by
lazyReflectedField(ViewPager2::class, "mRecyclerView") lazyReflectedField(ViewPager2::class, "mRecyclerView")
private val VIEW_PAGER_TOUCH_SLOP_FIELD: Field by private val VIEW_PAGER_TOUCH_SLOP_FIELD: Field by
lazyReflectedField(RecyclerView::class, "mTouchSlop") lazyReflectedField(RecyclerView::class, "mTouchSlop")
private const val KEY_LAST_TRANSITION_AXIS = private const val KEY_LAST_TRANSITION_AXIS =
BuildConfig.APPLICATION_ID + ".key.LAST_TRANSITION_AXIS" BuildConfig.APPLICATION_ID + ".key.LAST_TRANSITION_AXIS"
} }

View file

@ -142,13 +142,13 @@ class HomeViewModel(application: Application) :
_songs.value = settings.libSongSort.songs(library.songs) _songs.value = settings.libSongSort.songs(library.songs)
_albums.value = settings.libAlbumSort.albums(library.albums) _albums.value = settings.libAlbumSort.albums(library.albums)
_artists.value = settings.libArtistSort.artists( _artists.value =
if (settings.shouldHideCollaborators) { settings.libArtistSort.artists(
library.artists.filter { !it.isCollaborator } if (settings.shouldHideCollaborators) {
} else { library.artists.filter { !it.isCollaborator }
library.artists } else {
} library.artists
) })
_genres.value = settings.libGenreSort.genres(library.genres) _genres.value = settings.libGenreSort.genres(library.genres)
} }

View file

@ -21,6 +21,7 @@ import android.os.Bundle
import android.text.format.DateUtils import android.text.format.DateUtils
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import java.util.Formatter
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentHomeListBinding import org.oxycblt.auxio.databinding.FragmentHomeListBinding
import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Album
@ -35,7 +36,6 @@ import org.oxycblt.auxio.ui.recycler.Item
import org.oxycblt.auxio.ui.recycler.MenuItemListener import org.oxycblt.auxio.ui.recycler.MenuItemListener
import org.oxycblt.auxio.ui.recycler.SyncListDiffer import org.oxycblt.auxio.ui.recycler.SyncListDiffer
import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.collectImmediately
import java.util.Formatter
/** /**
* A [HomeListFragment] for showing a list of [Album]s. * A [HomeListFragment] for showing a list of [Album]s.
@ -67,7 +67,8 @@ class AlbumListFragment : HomeListFragment<Album>() {
is Sort.Mode.ByName -> album.collationKey?.run { sourceString.first().uppercase() } is Sort.Mode.ByName -> album.collationKey?.run { sourceString.first().uppercase() }
// By Artist -> Use name of first artist // By Artist -> Use name of first artist
is Sort.Mode.ByArtist -> album.artists[0].collationKey?.run { sourceString.first().uppercase() } is Sort.Mode.ByArtist ->
album.artists[0].collationKey?.run { sourceString.first().uppercase() }
// Year -> Use Full Year // Year -> Use Full Year
is Sort.Mode.ByDate -> album.date?.resolveDate(requireContext()) is Sort.Mode.ByDate -> album.date?.resolveDate(requireContext())
@ -83,12 +84,11 @@ class AlbumListFragment : HomeListFragment<Album>() {
val dateAddedMillis = album.dateAdded.secsToMs() val dateAddedMillis = album.dateAdded.secsToMs()
formatterSb.setLength(0) formatterSb.setLength(0)
DateUtils.formatDateRange( DateUtils.formatDateRange(
context, context,
formatter, formatter,
dateAddedMillis, dateAddedMillis,
dateAddedMillis, dateAddedMillis,
DateUtils.FORMAT_ABBREV_ALL DateUtils.FORMAT_ABBREV_ALL)
)
.toString() .toString()
} }

View file

@ -21,7 +21,7 @@ import android.os.Bundle
import android.text.format.DateUtils import android.text.format.DateUtils
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import org.oxycblt.auxio.MainFragmentDirections import java.util.Formatter
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentHomeListBinding import org.oxycblt.auxio.databinding.FragmentHomeListBinding
import org.oxycblt.auxio.music.MusicMode import org.oxycblt.auxio.music.MusicMode
@ -32,7 +32,6 @@ import org.oxycblt.auxio.music.picker.PickerMode
import org.oxycblt.auxio.playback.formatDurationMs import org.oxycblt.auxio.playback.formatDurationMs
import org.oxycblt.auxio.playback.secsToMs import org.oxycblt.auxio.playback.secsToMs
import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.ui.MainNavigationAction
import org.oxycblt.auxio.ui.recycler.IndicatorAdapter import org.oxycblt.auxio.ui.recycler.IndicatorAdapter
import org.oxycblt.auxio.ui.recycler.Item import org.oxycblt.auxio.ui.recycler.Item
import org.oxycblt.auxio.ui.recycler.MenuItemListener import org.oxycblt.auxio.ui.recycler.MenuItemListener
@ -40,7 +39,6 @@ import org.oxycblt.auxio.ui.recycler.SongViewHolder
import org.oxycblt.auxio.ui.recycler.SyncListDiffer import org.oxycblt.auxio.ui.recycler.SyncListDiffer
import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.context
import java.util.Formatter
/** /**
* A [HomeListFragment] for showing a list of [Song]s. * A [HomeListFragment] for showing a list of [Song]s.
@ -62,11 +60,7 @@ class SongListFragment : HomeListFragment<Song>() {
collectImmediately(homeModel.songs, homeAdapter::replaceList) collectImmediately(homeModel.songs, homeAdapter::replaceList)
collectImmediately( collectImmediately(
playbackModel.song, playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::handlePlayback)
playbackModel.parent,
playbackModel.isPlaying,
::handlePlayback
)
} }
override fun getPopup(pos: Int): String? { override fun getPopup(pos: Int): String? {
@ -80,10 +74,12 @@ class SongListFragment : HomeListFragment<Song>() {
is Sort.Mode.ByName -> song.collationKey?.run { sourceString.first().uppercase() } is Sort.Mode.ByName -> song.collationKey?.run { sourceString.first().uppercase() }
// Artist -> Use name of first artist // Artist -> Use name of first artist
is Sort.Mode.ByArtist -> song.album.artists[0].collationKey?.run { sourceString.first().uppercase() } is Sort.Mode.ByArtist ->
song.album.artists[0].collationKey?.run { sourceString.first().uppercase() }
// Album -> Use Album Name // Album -> Use Album Name
is Sort.Mode.ByAlbum -> song.album.collationKey?.run { sourceString.first().uppercase() } is Sort.Mode.ByAlbum ->
song.album.collationKey?.run { sourceString.first().uppercase() }
// Year -> Use Full Year // Year -> Use Full Year
is Sort.Mode.ByDate -> song.album.date?.resolveDate(requireContext()) is Sort.Mode.ByDate -> song.album.date?.resolveDate(requireContext())
@ -96,12 +92,11 @@ class SongListFragment : HomeListFragment<Song>() {
val dateAddedMillis = song.dateAdded.secsToMs() val dateAddedMillis = song.dateAdded.secsToMs()
formatterSb.setLength(0) formatterSb.setLength(0)
DateUtils.formatDateRange( DateUtils.formatDateRange(
context, context,
formatter, formatter,
dateAddedMillis, dateAddedMillis,
dateAddedMillis, dateAddedMillis,
DateUtils.FORMAT_ABBREV_ALL DateUtils.FORMAT_ABBREV_ALL)
)
.toString() .toString()
} }

View file

@ -62,12 +62,7 @@ sealed class Tab(open val mode: MusicMode) {
* Maps between the integer code in the tab sequence and the actual [MusicMode] instance. * Maps between the integer code in the tab sequence and the actual [MusicMode] instance.
*/ */
private val MODE_TABLE = private val MODE_TABLE =
arrayOf( arrayOf(MusicMode.SONGS, MusicMode.ALBUMS, MusicMode.ARTISTS, MusicMode.GENRES)
MusicMode.SONGS,
MusicMode.ALBUMS,
MusicMode.ARTISTS,
MusicMode.GENRES
)
/** Convert an array [tabs] into a sequence of tabs. */ /** Convert an array [tabs] into a sequence of tabs. */
fun toSequence(tabs: Array<Tab>): Int { fun toSequence(tabs: Array<Tab>): Int {

View file

@ -82,8 +82,7 @@ class TabViewHolder private constructor(private val binding: ItemTabBinding) :
MusicMode.ALBUMS -> R.string.lbl_albums MusicMode.ALBUMS -> R.string.lbl_albums
MusicMode.ARTISTS -> R.string.lbl_artists MusicMode.ARTISTS -> R.string.lbl_artists
MusicMode.GENRES -> R.string.lbl_genres MusicMode.GENRES -> R.string.lbl_genres
} })
)
isChecked = item is Tab.Visible isChecked = item is Tab.Visible
} }

View file

@ -88,8 +88,7 @@ class TabCustomizeDialog : ViewBindingDialogFragment<DialogTabsBinding>(), TabAd
when (tab) { when (tab) {
is Tab.Visible -> Tab.Invisible(tab.mode) is Tab.Visible -> Tab.Invisible(tab.mode)
is Tab.Invisible -> Tab.Visible(tab.mode) is Tab.Invisible -> Tab.Visible(tab.mode)
} })
)
} }
(requireDialog() as AlertDialog).getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = (requireDialog() as AlertDialog).getButton(AlertDialog.BUTTON_POSITIVE).isEnabled =

View file

@ -51,9 +51,7 @@ class BitmapProvider(private val context: Context) {
*/ */
@Synchronized @Synchronized
fun load(song: Song, target: Target) { fun load(song: Song, target: Target) {
val handle = synchronized(handleLock) { val handle = synchronized(handleLock) { ++currentHandle }
++currentHandle
}
currentRequest?.run { disposable.dispose() } currentRequest?.run { disposable.dispose() }
currentRequest = null currentRequest = null
@ -77,10 +75,8 @@ class BitmapProvider(private val context: Context) {
target.onCompleted(null) target.onCompleted(null)
} }
} }
} })
) .transformations(SquareFrameTransform.INSTANCE))
.transformations(SquareFrameTransform.INSTANCE)
)
currentRequest = Request(context.imageLoader.enqueue(request.build()), target) currentRequest = Request(context.imageLoader.enqueue(request.build()), target)
} }

View file

@ -28,18 +28,21 @@ enum class CoverMode {
MEDIA_STORE, MEDIA_STORE,
QUALITY; QUALITY;
val intCode: Int get() = when (this) { val intCode: Int
OFF -> IntegerTable.COVER_MODE_OFF get() =
MEDIA_STORE -> IntegerTable.COVER_MODE_MEDIA_STORE when (this) {
QUALITY -> IntegerTable.COVER_MODE_QUALITY OFF -> IntegerTable.COVER_MODE_OFF
} MEDIA_STORE -> IntegerTable.COVER_MODE_MEDIA_STORE
QUALITY -> IntegerTable.COVER_MODE_QUALITY
}
companion object { companion object {
fun fromIntCode(intCode: Int) = when (intCode) { fun fromIntCode(intCode: Int) =
IntegerTable.COVER_MODE_OFF -> OFF when (intCode) {
IntegerTable.COVER_MODE_MEDIA_STORE -> MEDIA_STORE IntegerTable.COVER_MODE_OFF -> OFF
IntegerTable.COVER_MODE_QUALITY -> QUALITY IntegerTable.COVER_MODE_MEDIA_STORE -> MEDIA_STORE
else -> null IntegerTable.COVER_MODE_QUALITY -> QUALITY
} else -> null
}
} }
} }

View file

@ -26,11 +26,11 @@ import androidx.annotation.AttrRes
import androidx.appcompat.widget.AppCompatImageView import androidx.appcompat.widget.AppCompatImageView
import androidx.core.widget.ImageViewCompat import androidx.core.widget.ImageViewCompat
import com.google.android.material.shape.MaterialShapeDrawable import com.google.android.material.shape.MaterialShapeDrawable
import kotlin.math.max
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.util.getColorCompat import org.oxycblt.auxio.util.getColorCompat
import org.oxycblt.auxio.util.getDrawableCompat import org.oxycblt.auxio.util.getDrawableCompat
import kotlin.math.max
/** /**
* View that displays the playback indicator. Nominally emulates [StyledImageView], but is much * View that displays the playback indicator. Nominally emulates [StyledImageView], but is much
@ -111,21 +111,15 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
0f, 0f,
0f, 0f,
drawable.intrinsicWidth.toFloat(), drawable.intrinsicWidth.toFloat(),
drawable.intrinsicHeight.toFloat() drawable.intrinsicHeight.toFloat())
)
indicatorMatrixDst.set(0f, 0f, iconSize.toFloat(), iconSize.toFloat()) indicatorMatrixDst.set(0f, 0f, iconSize.toFloat(), iconSize.toFloat())
indicatorMatrix.setRectToRect( indicatorMatrix.setRectToRect(
indicatorMatrixSrc, indicatorMatrixSrc, indicatorMatrixDst, Matrix.ScaleToFit.CENTER)
indicatorMatrixDst,
Matrix.ScaleToFit.CENTER
)
// Then actually center it into the icon, which the previous call does not // Then actually center it into the icon, which the previous call does not
// actually do. // actually do.
indicatorMatrix.postTranslate( indicatorMatrix.postTranslate(
(measuredWidth - iconSize) / 2f, (measuredWidth - iconSize) / 2f, (measuredHeight - iconSize) / 2f)
(measuredHeight - iconSize) / 2f
)
} }
} }
} }

View file

@ -95,9 +95,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
val styledAttrs = context.obtainStyledAttributes(attrs, R.styleable.StyledImageView) val styledAttrs = context.obtainStyledAttributes(attrs, R.styleable.StyledImageView)
val staticIcon = val staticIcon =
styledAttrs.getResourceId( styledAttrs.getResourceId(
R.styleable.StyledImageView_staticIcon, R.styleable.StyledImageView_staticIcon, ResourcesCompat.ID_NULL)
ResourcesCompat.ID_NULL
)
if (staticIcon != ResourcesCompat.ID_NULL) { if (staticIcon != ResourcesCompat.ID_NULL) {
this.staticIcon = context.getDrawableCompat(staticIcon) this.staticIcon = context.getDrawableCompat(staticIcon)
} }
@ -146,8 +144,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
adjustWidth, adjustWidth,
adjustHeight, adjustHeight,
bounds.width() - adjustWidth, bounds.width() - adjustWidth,
bounds.height() - adjustHeight bounds.height() - adjustHeight)
)
src.draw(canvas) src.draw(canvas)
} }

View file

@ -22,6 +22,7 @@ import android.graphics.Bitmap
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.graphics.Canvas import android.graphics.Canvas
import android.media.MediaMetadataRetriever import android.media.MediaMetadataRetriever
import android.util.Size as AndroidSize
import androidx.core.graphics.drawable.toDrawable import androidx.core.graphics.drawable.toDrawable
import coil.decode.DataSource import coil.decode.DataSource
import coil.decode.ImageSource import coil.decode.ImageSource
@ -37,6 +38,8 @@ import com.google.android.exoplayer2.MediaMetadata
import com.google.android.exoplayer2.MetadataRetriever import com.google.android.exoplayer2.MetadataRetriever
import com.google.android.exoplayer2.metadata.flac.PictureFrame import com.google.android.exoplayer2.metadata.flac.PictureFrame
import com.google.android.exoplayer2.metadata.id3.ApicFrame import com.google.android.exoplayer2.metadata.id3.ApicFrame
import java.io.ByteArrayInputStream
import java.io.InputStream
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import okio.buffer import okio.buffer
@ -46,9 +49,6 @@ import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logW import org.oxycblt.auxio.util.logW
import java.io.ByteArrayInputStream
import java.io.InputStream
import android.util.Size as AndroidSize
/** /**
* The base implementation for all image fetchers in Auxio. * The base implementation for all image fetchers in Auxio.
@ -58,8 +58,8 @@ import android.util.Size as AndroidSize
*/ */
abstract class BaseFetcher : Fetcher { abstract class BaseFetcher : Fetcher {
/** /**
* Fetch the [album] cover. This call respects user configuration and has proper * Fetch the [album] cover. This call respects user configuration and has proper redundancy in
* redundancy in the case that metadata fails to load. * the case that metadata fails to load.
*/ */
protected suspend fun fetchCover(context: Context, album: Album): InputStream? { protected suspend fun fetchCover(context: Context, album: Album): InputStream? {
val settings = Settings(context) val settings = Settings(context)
@ -191,8 +191,7 @@ abstract class BaseFetcher : Fetcher {
return SourceResult( return SourceResult(
source = ImageSource(stream.source().buffer(), context), source = ImageSource(stream.source().buffer(), context),
mimeType = null, mimeType = null,
dataSource = DataSource.DISK dataSource = DataSource.DISK)
)
} }
} }
@ -221,9 +220,7 @@ abstract class BaseFetcher : Fetcher {
// resolution. // resolution.
val bitmap = val bitmap =
SquareFrameTransform.INSTANCE.transform( SquareFrameTransform.INSTANCE.transform(
BitmapFactory.decodeStream(stream), BitmapFactory.decodeStream(stream), mosaicFrameSize)
mosaicFrameSize
)
canvas.drawBitmap(bitmap, x.toFloat(), y.toFloat(), null) canvas.drawBitmap(bitmap, x.toFloat(), y.toFloat(), null)
@ -241,8 +238,7 @@ abstract class BaseFetcher : Fetcher {
return DrawableResult( return DrawableResult(
drawable = mosaicBitmap.toDrawable(context.resources), drawable = mosaicBitmap.toDrawable(context.resources),
isSampled = true, isSampled = true,
dataSource = DataSource.DISK dataSource = DataSource.DISK)
)
} }
private fun Dimension.mosaicSize(): Int { private fun Dimension.mosaicSize(): Int {

View file

@ -27,6 +27,7 @@ import coil.fetch.SourceResult
import coil.key.Keyer import coil.key.Keyer
import coil.request.Options import coil.request.Options
import coil.size.Size import coil.size.Size
import kotlin.math.min
import okio.buffer import okio.buffer
import okio.source import okio.source
import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Album
@ -35,7 +36,6 @@ import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.Sort import org.oxycblt.auxio.music.Sort
import kotlin.math.min
/** A basic keyer for music data. */ /** A basic keyer for music data. */
class MusicKeyer : Keyer<Music> { class MusicKeyer : Keyer<Music> {
@ -60,8 +60,7 @@ private constructor(private val context: Context, private val album: Album) : Ba
SourceResult( SourceResult(
source = ImageSource(stream.source().buffer(), context), source = ImageSource(stream.source().buffer(), context),
mimeType = null, mimeType = null,
dataSource = DataSource.DISK dataSource = DataSource.DISK)
)
} }
class SongFactory : Fetcher.Factory<Song> { class SongFactory : Fetcher.Factory<Song> {

View file

@ -21,6 +21,11 @@ package org.oxycblt.auxio.music
import android.content.Context import android.content.Context
import android.os.Parcelable import android.os.Parcelable
import java.security.MessageDigest
import java.text.CollationKey
import java.text.Collator
import java.util.UUID
import kotlin.math.max
import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
@ -36,11 +41,6 @@ import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.ui.recycler.Item import org.oxycblt.auxio.ui.recycler.Item
import org.oxycblt.auxio.util.nonZeroOrNull import org.oxycblt.auxio.util.nonZeroOrNull
import org.oxycblt.auxio.util.unlikelyToBeNull import org.oxycblt.auxio.util.unlikelyToBeNull
import java.security.MessageDigest
import java.text.CollationKey
import java.text.Collator
import java.util.UUID
import kotlin.math.max
// --- MUSIC MODELS --- // --- MUSIC MODELS ---
@ -55,14 +55,14 @@ sealed class Music : Item {
abstract val rawSortName: String? abstract val rawSortName: String?
/** /**
* A key used by the sorting system that takes into account the sort tags of this item, * A key used by the sorting system that takes into account the sort tags of this item, any
* any (english) articles that prefix the names, and collation rules. * (english) articles that prefix the names, and collation rules.
*/ */
abstract val collationKey: CollationKey? abstract val collationKey: CollationKey?
/** /**
* Resolve a name from it's raw form to a form suitable to be shown in a UI. * Resolve a name from it's raw form to a form suitable to be shown in a UI. Null values will be
* Null values will be resolved into their string form with this function. * resolved into their string form with this function.
*/ */
abstract fun resolveName(context: Context): String abstract fun resolveName(context: Context): String
@ -75,26 +75,27 @@ sealed class Music : Item {
other is Music && javaClass == other.javaClass && uid == other.uid other is Music && javaClass == other.javaClass && uid == other.uid
/** /**
* Workaround to allow for easy collation key generation in the initializer without * Workaround to allow for easy collation key generation in the initializer without base-class
* base-class initialization issues or slow lazy initialization. * initialization issues or slow lazy initialization.
*/ */
protected fun makeCollationKeyImpl(): CollationKey? { protected fun makeCollationKeyImpl(): CollationKey? {
val sortName = (rawSortName ?: rawName)?.run { val sortName =
when { (rawSortName ?: rawName)?.run {
length > 5 && startsWith("the ", ignoreCase = true) -> substring(4) when {
length > 4 && startsWith("an ", ignoreCase = true) -> substring(3) length > 5 && startsWith("the ", ignoreCase = true) -> substring(4)
length > 3 && startsWith("a ", ignoreCase = true) -> substring(2) length > 4 && startsWith("an ", ignoreCase = true) -> substring(3)
else -> this length > 3 && startsWith("a ", ignoreCase = true) -> substring(2)
else -> this
}
} }
}
return COLLATOR.getCollationKey(sortName) return COLLATOR.getCollationKey(sortName)
} }
/** /**
* Called when the library has been linked and validation/construction steps dependent * Called when the library has been linked and validation/construction steps dependent on linked
* on linked items should run. It's also used to do last-step initialization of fields * items should run. It's also used to do last-step initialization of fields that require any
* that require any parent values that would not be present during startup. * parent values that would not be present during startup.
*/ */
abstract fun _finalize() abstract fun _finalize()
@ -107,15 +108,16 @@ sealed class Music : Item {
* external sources, as it can persist across app restarts and does not need to encode useless * external sources, as it can persist across app restarts and does not need to encode useless
* information about the relationships between items. * information about the relationships between items.
* *
* Note: While the core of a UID is a UUID. The whole is not technically a UUID, with * Note: While the core of a UID is a UUID. The whole is not technically a UUID, with string
* string representation in particular having multiple extensions to increase uniqueness. * representation in particular having multiple extensions to increase uniqueness. Please don't
* Please don't try to do anything interesting with this and just assume it's a black box * try to do anything interesting with this and just assume it's a black box that can only be
* that can only be compared, serialized, and deserialized. * compared, serialized, and deserialized.
* *
* @author OxygenCobalt * @author OxygenCobalt
*/ */
@Parcelize @Parcelize
class UID private constructor( class UID
private constructor(
private val format: Format, private val format: Format,
private val mode: MusicMode, private val mode: MusicMode,
private val uuid: UUID private val uuid: UUID
@ -130,9 +132,8 @@ sealed class Music : Item {
override fun hashCode() = hashCode override fun hashCode() = hashCode
override fun equals(other: Any?) = other is UID && override fun equals(other: Any?) =
format == other.format && other is UID && format == other.format && mode == other.mode && uuid == other.uuid
mode == other.mode && uuid == other.uuid
// UID string format is roughly: // UID string format is roughly:
// format_namespace:music_mode_int-uuid // format_namespace:music_mode_int-uuid
@ -151,11 +152,12 @@ sealed class Music : Item {
return null return null
} }
val format = when (split[0]) { val format =
Format.AUXIO.namespace -> Format.AUXIO when (split[0]) {
Format.MUSICBRAINZ.namespace -> Format.MUSICBRAINZ Format.AUXIO.namespace -> Format.AUXIO
else -> return null Format.MUSICBRAINZ.namespace -> Format.MUSICBRAINZ
} else -> return null
}
val ids = split[1].split('-', limit = 2) val ids = split[1].split('-', limit = 2)
if (ids.size != 2) { if (ids.size != 2) {
@ -168,9 +170,7 @@ sealed class Music : Item {
return UID(format, mode, uuid) return UID(format, mode, uuid)
} }
/** /** Make a UUID derived from the MD5 hash of the data digested in [updates]. */
* Make a UUID derived from the MD5 hash of the data digested in [updates].
*/
fun auxio(mode: MusicMode, updates: MessageDigest.() -> Unit): UID { fun auxio(mode: MusicMode, updates: MessageDigest.() -> Unit): UID {
// Auxio hashes consist of the MD5 hash of the non-subjective, consistent // Auxio hashes consist of the MD5 hash of the non-subjective, consistent
// tags in a music item. For easier use with MusicBrainz IDs, we transform // tags in a music item. For easier use with MusicBrainz IDs, we transform
@ -181,18 +181,13 @@ sealed class Music : Item {
return UID(Format.AUXIO, mode, uuid) return UID(Format.AUXIO, mode, uuid)
} }
/** /** Make a UUID derived from a MusicBrainz ID. */
* Make a UUID derived from a MusicBrainz ID. fun musicBrainz(mode: MusicMode, uuid: UUID): UID = UID(Format.MUSICBRAINZ, mode, uuid)
*/
fun musicBrainz(mode: MusicMode, uuid: UUID): UID =
UID(Format.MUSICBRAINZ, mode, uuid)
} }
} }
companion object { companion object {
private val COLLATOR = Collator.getInstance().apply { private val COLLATOR = Collator.getInstance().apply { strength = Collator.PRIMARY }
strength = Collator.PRIMARY
}
} }
} }
@ -207,7 +202,10 @@ sealed class MusicParent : Music() {
override fun hashCode() = 31 * uid.hashCode() + songs.hashCode() override fun hashCode() = 31 * uid.hashCode() + songs.hashCode()
override fun equals(other: Any?) = override fun equals(other: Any?) =
other is MusicParent && javaClass == other.javaClass && uid == other.uid && songs == other.songs other is MusicParent &&
javaClass == other.javaClass &&
uid == other.uid &&
songs == other.songs
} }
/** /**
@ -215,21 +213,22 @@ sealed class MusicParent : Music() {
* @author OxygenCobalt * @author OxygenCobalt
*/ */
class Song constructor(raw: Raw, settings: Settings) : Music() { class Song constructor(raw: Raw, settings: Settings) : Music() {
override val uid = raw.musicBrainzId?.toUuidOrNull()?.let { UID.musicBrainz(MusicMode.SONGS, it) } override val uid =
?: UID.auxio(MusicMode.SONGS) { raw.musicBrainzId?.toUuidOrNull()?.let { UID.musicBrainz(MusicMode.SONGS, it) }
// Song UIDs are based on the raw data without parsing so that they remain ?: UID.auxio(MusicMode.SONGS) {
// consistent across music setting changes. Parents are not held up to the // Song UIDs are based on the raw data without parsing so that they remain
// same standard since grouping is already inherently linked to settings. // consistent across music setting changes. Parents are not held up to the
update(raw.name) // same standard since grouping is already inherently linked to settings.
update(raw.albumName) update(raw.name)
update(raw.date) update(raw.albumName)
update(raw.date)
update(raw.track) update(raw.track)
update(raw.disc) update(raw.disc)
update(raw.artistNames) update(raw.artistNames)
update(raw.albumArtistNames) update(raw.albumArtistNames)
} }
override val rawName = requireNotNull(raw.name) { "Invalid raw: No title" } override val rawName = requireNotNull(raw.name) { "Invalid raw: No title" }
@ -258,15 +257,13 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
val path = val path =
Path( Path(
name = requireNotNull(raw.fileName) { "Invalid raw: No display name" }, name = requireNotNull(raw.fileName) { "Invalid raw: No display name" },
parent = requireNotNull(raw.directory) { "Invalid raw: No parent directory" } parent = requireNotNull(raw.directory) { "Invalid raw: No parent directory" })
)
/** The mime type of the audio file. Only intended for display. */ /** The mime type of the audio file. Only intended for display. */
val mimeType = val mimeType =
MimeType( MimeType(
fromExtension = requireNotNull(raw.extensionMimeType) { "Invalid raw: No mime type" }, fromExtension = requireNotNull(raw.extensionMimeType) { "Invalid raw: No mime type" },
fromFormat = raw.formatMimeType fromFormat = raw.formatMimeType)
)
/** The size of this audio file. */ /** The size of this audio file. */
val size = requireNotNull(raw.size) { "Invalid raw: No size" } val size = requireNotNull(raw.size) { "Invalid raw: No size" }
@ -280,8 +277,8 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
private var _album: Album? = null private var _album: Album? = null
/** /**
* The album of this song. Every song is guaranteed to have one and only one album, * The album of this song. Every song is guaranteed to have one and only one album, with a
* with a "directory" album being used if no album tag can be found. * "directory" album being used if no album tag can be found.
*/ */
val album: Album val album: Album
get() = unlikelyToBeNull(_album) get() = unlikelyToBeNull(_album)
@ -298,34 +295,41 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
private val albumArtistSortNames = raw.albumArtistSortNames.parseMultiValue(settings) private val albumArtistSortNames = raw.albumArtistSortNames.parseMultiValue(settings)
private val rawArtists = artistNames.mapIndexed { i, name -> private val rawArtists =
Artist.Raw(artistMusicBrainzIds.getOrNull(i)?.toUuidOrNull(), name, artistSortNames.getOrNull(i)) artistNames.mapIndexed { i, name ->
} Artist.Raw(
artistMusicBrainzIds.getOrNull(i)?.toUuidOrNull(),
name,
artistSortNames.getOrNull(i))
}
private val rawAlbumArtists = albumArtistNames.mapIndexed { i, name -> private val rawAlbumArtists =
Artist.Raw(albumArtistMusicBrainzIds.getOrNull(i)?.toUuidOrNull(), name, albumArtistSortNames.getOrNull(i)) albumArtistNames.mapIndexed { i, name ->
} Artist.Raw(
albumArtistMusicBrainzIds.getOrNull(i)?.toUuidOrNull(),
name,
albumArtistSortNames.getOrNull(i))
}
private val _artists = mutableListOf<Artist>() private val _artists = mutableListOf<Artist>()
/** /**
* The artists of this song. Most often one, but there could be multiple. These artists * The artists of this song. Most often one, but there could be multiple. These artists are
* are derived from the artists tag and not the album artists tag, so they may differ from * derived from the artists tag and not the album artists tag, so they may differ from the
* the artists of the album. * artists of the album.
*/ */
val artists: List<Artist> val artists: List<Artist>
get() = _artists get() = _artists
/** /**
* Resolve the artists of this song into a human-readable name. First tries to use artist * Resolve the artists of this song into a human-readable name. First tries to use artist tags,
* tags, then falls back to album artist tags. * then falls back to album artist tags.
*/ */
fun resolveArtistContents(context: Context) = fun resolveArtistContents(context: Context) = artists.joinToString { it.resolveName(context) }
artists.joinToString { it.resolveName(context) }
/** /**
* Utility method for recyclerview diffing that checks if resolveArtistContents is the * Utility method for recyclerview diffing that checks if resolveArtistContents is the same
* same without a context. * without a context.
*/ */
fun areArtistContentsTheSame(other: Song): Boolean { fun areArtistContentsTheSame(other: Song): Boolean {
for (i in 0 until max(artists.size, other.artists.size)) { for (i in 0 until max(artists.size, other.artists.size)) {
@ -348,19 +352,18 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
val genres: List<Genre> val genres: List<Genre>
get() = _genres get() = _genres
/** /** Resolve the genres of the song into a human-readable string. */
* Resolve the genres of the song into a human-readable string.
*/
fun resolveGenreContents(context: Context) = genres.joinToString { it.resolveName(context) } fun resolveGenreContents(context: Context) = genres.joinToString { it.resolveName(context) }
// --- INTERNAL FIELDS --- // --- INTERNAL FIELDS ---
val _rawGenres = raw.genreNames.parseId3GenreNames(settings) val _rawGenres =
.map { Genre.Raw(it) }.ifEmpty { listOf(Genre.Raw()) } raw.genreNames
.parseId3GenreNames(settings)
.map { Genre.Raw(it) }
.ifEmpty { listOf(Genre.Raw()) }
val _rawArtists = rawArtists.ifEmpty { rawAlbumArtists }.ifEmpty { val _rawArtists = rawArtists.ifEmpty { rawAlbumArtists }.ifEmpty { listOf(Artist.Raw()) }
listOf(Artist.Raw())
}
val _rawAlbum = val _rawAlbum =
Album.Raw( Album.Raw(
@ -369,8 +372,8 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
name = requireNotNull(raw.albumName) { "Invalid raw: No album name" }, name = requireNotNull(raw.albumName) { "Invalid raw: No album name" },
sortName = raw.albumSortName, sortName = raw.albumSortName,
releaseType = ReleaseType.parse(raw.albumReleaseTypes.parseMultiValue(settings)), releaseType = ReleaseType.parse(raw.albumReleaseTypes.parseMultiValue(settings)),
rawArtists = rawAlbumArtists.ifEmpty { rawArtists }.ifEmpty { listOf(Artist.Raw(null, null)) } rawArtists =
) rawAlbumArtists.ifEmpty { rawArtists }.ifEmpty { listOf(Artist.Raw(null, null)) })
fun _link(album: Album) { fun _link(album: Album) {
_album = album _album = album
@ -388,7 +391,7 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
checkNotNull(_album) { "Malformed song: No album" } checkNotNull(_album) { "Malformed song: No album" }
check(_artists.isNotEmpty()) { "Malformed song: No artists" } check(_artists.isNotEmpty()) { "Malformed song: No artists" }
for( i in _artists.indices ) { for (i in _artists.indices) {
// Non-destructively reorder the linked artists so that they align with // Non-destructively reorder the linked artists so that they align with
// the artist ordering within the song metadata. // the artist ordering within the song metadata.
val newIdx = _artists[i]._getOriginalPositionIn(_rawArtists) val newIdx = _artists[i]._getOriginalPositionIn(_rawArtists)
@ -398,7 +401,7 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
} }
check(_genres.isNotEmpty()) { "Malformed song: No genres" } check(_genres.isNotEmpty()) { "Malformed song: No genres" }
for( i in _genres.indices ) { for (i in _genres.indices) {
// Non-destructively reorder the linked genres so that they align with // Non-destructively reorder the linked genres so that they align with
// the genre ordering within the song metadata. // the genre ordering within the song metadata.
val newIdx = _genres[i]._getOriginalPositionIn(_rawGenres) val newIdx = _genres[i]._getOriginalPositionIn(_rawGenres)
@ -445,14 +448,15 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
* @author OxygenCobalt * @author OxygenCobalt
*/ */
class Album constructor(raw: Raw, override val songs: List<Song>) : MusicParent() { class Album constructor(raw: Raw, override val songs: List<Song>) : MusicParent() {
override val uid = raw.musicBrainzId?.let { UID.musicBrainz(MusicMode.ALBUMS, it) } override val uid =
?: UID.auxio(MusicMode.ALBUMS) { raw.musicBrainzId?.let { UID.musicBrainz(MusicMode.ALBUMS, it) }
// Hash based on only names despite the presence of a date to increase stability. ?: UID.auxio(MusicMode.ALBUMS) {
// I don't know if there is any situation where an artist will have two albums with // Hash based on only names despite the presence of a date to increase stability.
// the exact same name, but if there is, I would love to know. // I don't know if there is any situation where an artist will have two albums with
update(raw.name) // the exact same name, but if there is, I would love to know.
update(raw.rawArtists.map { it.name }) update(raw.name)
} update(raw.rawArtists.map { it.name })
}
override val rawName = raw.name override val rawName = raw.name
@ -481,21 +485,19 @@ class Album constructor(raw: Raw, override val songs: List<Song>) : MusicParent(
val dateAdded: Long val dateAdded: Long
/** /**
* The artists of this album. Usually one, but there may be more. These are derived from * The artists of this album. Usually one, but there may be more. These are derived from the
* the album artist first, so they may differ from the song artists. * album artist first, so they may differ from the song artists.
*/ */
private val _artists = mutableListOf<Artist>() private val _artists = mutableListOf<Artist>()
val artists: List<Artist> get() = _artists val artists: List<Artist>
get() = _artists
/** Resolve the artists of this album in a human-readable manner. */
fun resolveArtistContents(context: Context) = artists.joinToString { it.resolveName(context) }
/** /**
* Resolve the artists of this album in a human-readable manner. * Utility for RecyclerView differs to check if resolveArtistContents is the same without a
*/ * context.
fun resolveArtistContents(context: Context) =
artists.joinToString { it.resolveName(context) }
/**
* Utility for RecyclerView differs to check if resolveArtistContents is the same without
* a context.
*/ */
fun areArtistContentsTheSame(other: Album): Boolean { fun areArtistContentsTheSame(other: Album): Boolean {
for (i in 0 until max(artists.size, other.artists.size)) { for (i in 0 until max(artists.size, other.artists.size)) {
@ -547,7 +549,7 @@ class Album constructor(raw: Raw, override val songs: List<Song>) : MusicParent(
override fun _finalize() { override fun _finalize() {
check(songs.isNotEmpty()) { "Malformed album: Empty" } check(songs.isNotEmpty()) { "Malformed album: Empty" }
check(_artists.isNotEmpty()) { "Malformed album: No artists" } check(_artists.isNotEmpty()) { "Malformed album: No artists" }
for( i in _artists.indices ) { for (i in _artists.indices) {
// Non-destructively reorder the linked artists so that they align with // Non-destructively reorder the linked artists so that they align with
// the artist ordering within the song metadata. // the artist ordering within the song metadata.
val newIdx = _artists[i]._getOriginalPositionIn(_rawArtists) val newIdx = _artists[i]._getOriginalPositionIn(_rawArtists)
@ -572,9 +574,9 @@ class Album constructor(raw: Raw, override val songs: List<Song>) : MusicParent(
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (other !is Raw) return false if (other !is Raw) return false
if (musicBrainzId != null && other.musicBrainzId != null && if (musicBrainzId != null &&
musicBrainzId == other.musicBrainzId other.musicBrainzId != null &&
) { musicBrainzId == other.musicBrainzId) {
return true return true
} }
@ -584,15 +586,14 @@ class Album constructor(raw: Raw, override val songs: List<Song>) : MusicParent(
} }
/** /**
* An abstract artist. This is derived from both album artist values and artist values in * An abstract artist. This is derived from both album artist values and artist values in albums and
* albums and songs respectively. * songs respectively.
* @author OxygenCobalt * @author OxygenCobalt
*/ */
class Artist class Artist constructor(private val raw: Raw, songAlbums: List<Music>) : MusicParent() {
constructor(private val raw: Raw, songAlbums: List<Music>) : MusicParent() { override val uid =
override val uid = raw.musicBrainzId?.let { UID.musicBrainz(MusicMode.ARTISTS, it) } ?: UID.auxio( raw.musicBrainzId?.let { UID.musicBrainz(MusicMode.ARTISTS, it) }
MusicMode.ARTISTS ?: UID.auxio(MusicMode.ARTISTS) { update(raw.name) }
) { update(raw.name) }
override val rawName = raw.name override val rawName = raw.name
@ -602,9 +603,7 @@ constructor(private val raw: Raw, songAlbums: List<Music>) : MusicParent() {
override fun resolveName(context: Context) = rawName ?: context.getString(R.string.def_artist) override fun resolveName(context: Context) = rawName ?: context.getString(R.string.def_artist)
/** /** The songs of this artist. This might be empty. */
* The songs of this artist. This might be empty.
*/
override val songs: List<Song> override val songs: List<Song>
/** The total duration of songs in this artist, in millis. Null if there are no songs. */ /** The total duration of songs in this artist, in millis. Null if there are no songs. */
@ -618,14 +617,12 @@ constructor(private val raw: Raw, songAlbums: List<Music>) : MusicParent() {
private lateinit var genres: List<Genre> private lateinit var genres: List<Genre>
/** /** Resolve the combined genres of this artist into a human-readable string. */
* Resolve the combined genres of this artist into a human-readable string.
*/
fun resolveGenreContents(context: Context) = genres.joinToString { it.resolveName(context) } fun resolveGenreContents(context: Context) = genres.joinToString { it.resolveName(context) }
/** /**
* Utility for RecyclerView differs to check if resolveGenreContents is the same without * Utility for RecyclerView differs to check if resolveGenreContents is the same without a
* a context. * context.
*/ */
fun areGenreContentsTheSame(other: Artist): Boolean { fun areGenreContentsTheSame(other: Artist): Boolean {
for (i in 0 until max(genres.size, other.genres.size)) { for (i in 0 until max(genres.size, other.genres.size)) {
@ -639,11 +636,10 @@ constructor(private val raw: Raw, songAlbums: List<Music>) : MusicParent() {
return true return true
} }
init { init {
val distinctSongs = mutableSetOf<Song>() val distinctSongs = mutableSetOf<Song>()
val distinctAlbums = mutableSetOf<Album>() val distinctAlbums = mutableSetOf<Album>()
var noAlbums = true var noAlbums = true
for (music in songAlbums) { for (music in songAlbums) {
@ -653,13 +649,11 @@ constructor(private val raw: Raw, songAlbums: List<Music>) : MusicParent() {
distinctSongs.add(music) distinctSongs.add(music)
distinctAlbums.add(music.album) distinctAlbums.add(music.album)
} }
is Album -> { is Album -> {
music._link(this) music._link(this)
distinctAlbums.add(music) distinctAlbums.add(music)
noAlbums = false noAlbums = false
} }
else -> error("Unexpected input music ${music::class.simpleName}") else -> error("Unexpected input music ${music::class.simpleName}")
} }
} }
@ -677,11 +671,17 @@ constructor(private val raw: Raw, songAlbums: List<Music>) : MusicParent() {
override fun _finalize() { override fun _finalize() {
check(songs.isNotEmpty() || albums.isNotEmpty()) { "Malformed artist: Empty" } check(songs.isNotEmpty() || albums.isNotEmpty()) { "Malformed artist: Empty" }
genres = Sort(Sort.Mode.ByName, true).genres(songs.flatMapTo(mutableSetOf()) { it.genres }) genres =
.sortedByDescending { genre -> songs.count { it.genres.contains(genre) } } Sort(Sort.Mode.ByName, true)
.genres(songs.flatMapTo(mutableSetOf()) { it.genres })
.sortedByDescending { genre -> songs.count { it.genres.contains(genre) } }
} }
class Raw(val musicBrainzId: UUID? = null, val name: String? = null, val sortName: String? = null) { class Raw(
val musicBrainzId: UUID? = null,
val name: String? = null,
val sortName: String? = null
) {
private val hashCode = musicBrainzId?.hashCode() ?: name?.lowercase().hashCode() private val hashCode = musicBrainzId?.hashCode() ?: name?.lowercase().hashCode()
override fun hashCode() = hashCode override fun hashCode() = hashCode
@ -689,9 +689,9 @@ constructor(private val raw: Raw, songAlbums: List<Music>) : MusicParent() {
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (other !is Raw) return false if (other !is Raw) return false
if (musicBrainzId != null && other.musicBrainzId != null && if (musicBrainzId != null &&
musicBrainzId == other.musicBrainzId other.musicBrainzId != null &&
) { musicBrainzId == other.musicBrainzId) {
return true return true
} }
@ -743,8 +743,8 @@ class Genre constructor(private val raw: Raw, override val songs: List<Song>) :
durationMs = totalDuration durationMs = totalDuration
albums = Sort(Sort.Mode.ByName, true).albums(distinctAlbums) albums =
.sortedByDescending { album -> Sort(Sort.Mode.ByName, true).albums(distinctAlbums).sortedByDescending { album ->
album.songs.count { it.genres.contains(this) } album.songs.count { it.genres.contains(this) }
} }
@ -819,9 +819,7 @@ fun MessageDigest.update(n: Long?) {
n.shr(32).toByte(), n.shr(32).toByte(),
n.shr(40).toByte(), n.shr(40).toByte(),
n.shl(48).toByte(), n.shl(48).toByte(),
n.shr(56).toByte() n.shr(56).toByte()))
)
)
} }
/** /**
@ -851,6 +849,5 @@ fun ByteArray.toUuid(): UUID {
.or(get(12).toLong().and(0xFF).shl(24)) .or(get(12).toLong().and(0xFF).shl(24))
.or(get(13).toLong().and(0xFF).shl(16)) .or(get(13).toLong().and(0xFF).shl(16))
.or(get(14).toLong().and(0xFF).shl(8)) .or(get(14).toLong().and(0xFF).shl(8))
.or(get(15).toLong().and(0xFF)) .or(get(15).toLong().and(0xFF)))
)
} }

View file

@ -99,11 +99,10 @@ class MusicStore private constructor() {
} }
/** /**
* Find a music [T] by its [uid]. If the music does not exist, or if the music is * Find a music [T] by its [uid]. If the music does not exist, or if the music is not [T],
* not [T], null will be returned. * null will be returned.
*/ */
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST") fun <T : Music> find(uid: Music.UID) = uidMap[uid] as? T
fun <T : Music> find(uid: Music.UID) = 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) = find<Song>(song.uid) fun sanitize(song: Song) = find<Song>(song.uid)
@ -122,8 +121,8 @@ class MusicStore private constructor() {
/** Find a song for a [uri]. */ /** Find a song for a [uri]. */
fun findSongForUri(context: Context, uri: Uri) = fun findSongForUri(context: Context, uri: Uri) =
context.contentResolverSafe.useQuery(uri, arrayOf(OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE)) { context.contentResolverSafe.useQuery(
cursor -> uri, arrayOf(OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE)) { cursor ->
cursor.moveToFirst() cursor.moveToFirst()
// We are weirdly limited to DISPLAY_NAME and SIZE when trying to locate a // We are weirdly limited to DISPLAY_NAME and SIZE when trying to locate a
@ -132,9 +131,7 @@ class MusicStore private constructor() {
val displayName = val displayName =
cursor.getString(cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME)) cursor.getString(cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME))
val size = cursor.getLong( val size = cursor.getLong(cursor.getColumnIndexOrThrow(OpenableColumns.SIZE))
cursor.getColumnIndexOrThrow(OpenableColumns.SIZE)
)
songs.find { it.path.name == displayName && it.size == size } songs.find { it.path.name == displayName && it.size == size }
} }

View file

@ -44,9 +44,7 @@ class MusicViewModel : ViewModel(), Indexer.Callback {
indexer.registerCallback(this) indexer.registerCallback(this)
} }
/** /** Re-index the music library. */
* Re-index the music library.
*/
fun reindex() { fun reindex() {
indexer.requestReindex(true) indexer.requestReindex(true)
} }

View file

@ -18,10 +18,10 @@
package org.oxycblt.auxio.music package org.oxycblt.auxio.music
import androidx.annotation.IdRes import androidx.annotation.IdRes
import kotlin.math.max
import org.oxycblt.auxio.IntegerTable import org.oxycblt.auxio.IntegerTable
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.music.Sort.Mode import org.oxycblt.auxio.music.Sort.Mode
import kotlin.math.max
/** /**
* Represents the sort modes used in Auxio. * Represents the sort modes used in Auxio.
@ -79,11 +79,11 @@ data class Sort(val mode: Mode, val isAscending: Boolean) {
albums.sortWith(mode.getAlbumComparator(isAscending)) albums.sortWith(mode.getAlbumComparator(isAscending))
} }
fun artistsInPlace(artists: MutableList<Artist>) { private fun artistsInPlace(artists: MutableList<Artist>) {
artists.sortWith(mode.getArtistComparator(isAscending)) artists.sortWith(mode.getArtistComparator(isAscending))
} }
fun genresInPlace(genres: MutableList<Genre>) { private fun genresInPlace(genres: MutableList<Genre>) {
genres.sortWith(mode.getGenreComparator(isAscending)) genres.sortWith(mode.getGenreComparator(isAscending))
} }
@ -141,8 +141,7 @@ data class Sort(val mode: Mode, val isAscending: Boolean) {
compareByDynamic(ascending, BasicComparator.ALBUM) { it.album }, compareByDynamic(ascending, BasicComparator.ALBUM) { it.album },
compareBy(NullableComparator.INT) { it.disc }, compareBy(NullableComparator.INT) { it.disc },
compareBy(NullableComparator.INT) { it.track }, compareBy(NullableComparator.INT) { it.track },
compareBy(BasicComparator.SONG) compareBy(BasicComparator.SONG))
)
} }
/** Sort by the artist of an item, only supported by [Album] and [Song] */ /** Sort by the artist of an item, only supported by [Album] and [Song] */
@ -160,15 +159,13 @@ data class Sort(val mode: Mode, val isAscending: Boolean) {
compareByDescending(BasicComparator.ALBUM) { it.album }, compareByDescending(BasicComparator.ALBUM) { it.album },
compareBy(NullableComparator.INT) { it.disc }, compareBy(NullableComparator.INT) { it.disc },
compareBy(NullableComparator.INT) { it.track }, compareBy(NullableComparator.INT) { it.track },
compareBy(BasicComparator.SONG) compareBy(BasicComparator.SONG))
)
override fun getAlbumComparator(ascending: Boolean): Comparator<Album> = override fun getAlbumComparator(ascending: Boolean): Comparator<Album> =
MultiComparator( MultiComparator(
compareByDynamic(ascending, ListComparator.ARTISTS) { it.artists }, compareByDynamic(ascending, ListComparator.ARTISTS) { it.artists },
compareByDescending(NullableComparator.DATE) { it.date }, compareByDescending(NullableComparator.DATE) { it.date },
compareBy(BasicComparator.ALBUM) compareBy(BasicComparator.ALBUM))
)
} }
/** Sort by the date of an item, only supported by [Album] and [Song] */ /** Sort by the date of an item, only supported by [Album] and [Song] */
@ -185,14 +182,12 @@ data class Sort(val mode: Mode, val isAscending: Boolean) {
compareByDescending(BasicComparator.ALBUM) { it.album }, compareByDescending(BasicComparator.ALBUM) { it.album },
compareBy(NullableComparator.INT) { it.disc }, compareBy(NullableComparator.INT) { it.disc },
compareBy(NullableComparator.INT) { it.track }, compareBy(NullableComparator.INT) { it.track },
compareBy(BasicComparator.SONG) compareBy(BasicComparator.SONG))
)
override fun getAlbumComparator(ascending: Boolean): Comparator<Album> = override fun getAlbumComparator(ascending: Boolean): Comparator<Album> =
MultiComparator( MultiComparator(
compareByDynamic(ascending, NullableComparator.DATE) { it.date }, compareByDynamic(ascending, NullableComparator.DATE) { it.date },
compareBy(BasicComparator.ALBUM) compareBy(BasicComparator.ALBUM))
)
} }
/** Sort by the duration of the item. Supports all items. */ /** Sort by the duration of the item. Supports all items. */
@ -205,27 +200,20 @@ data class Sort(val mode: Mode, val isAscending: Boolean) {
override fun getSongComparator(ascending: Boolean): Comparator<Song> = override fun getSongComparator(ascending: Boolean): Comparator<Song> =
MultiComparator( MultiComparator(
compareByDynamic(ascending) { it.durationMs }, compareByDynamic(ascending) { it.durationMs }, compareBy(BasicComparator.SONG))
compareBy(BasicComparator.SONG)
)
override fun getAlbumComparator(ascending: Boolean): Comparator<Album> = override fun getAlbumComparator(ascending: Boolean): Comparator<Album> =
MultiComparator( MultiComparator(
compareByDynamic(ascending) { it.durationMs }, compareByDynamic(ascending) { it.durationMs }, compareBy(BasicComparator.ALBUM))
compareBy(BasicComparator.ALBUM)
)
override fun getArtistComparator(ascending: Boolean): Comparator<Artist> = override fun getArtistComparator(ascending: Boolean): Comparator<Artist> =
MultiComparator( MultiComparator(
compareByDynamic(ascending, NullableComparator.LONG) { it.durationMs }, compareByDynamic(ascending, NullableComparator.LONG) { it.durationMs },
compareBy(BasicComparator.ARTIST) compareBy(BasicComparator.ARTIST))
)
override fun getGenreComparator(ascending: Boolean): Comparator<Genre> = override fun getGenreComparator(ascending: Boolean): Comparator<Genre> =
MultiComparator( MultiComparator(
compareByDynamic(ascending) { it.durationMs }, compareByDynamic(ascending) { it.durationMs }, compareBy(BasicComparator.GENRE))
compareBy(BasicComparator.GENRE)
)
} }
/** Sort by the amount of songs. Only applicable to music parents. */ /** Sort by the amount of songs. Only applicable to music parents. */
@ -238,21 +226,16 @@ data class Sort(val mode: Mode, val isAscending: Boolean) {
override fun getAlbumComparator(ascending: Boolean): Comparator<Album> = override fun getAlbumComparator(ascending: Boolean): Comparator<Album> =
MultiComparator( MultiComparator(
compareByDynamic(ascending) { it.songs.size }, compareByDynamic(ascending) { it.songs.size }, compareBy(BasicComparator.ALBUM))
compareBy(BasicComparator.ALBUM)
)
override fun getArtistComparator(ascending: Boolean): Comparator<Artist> = override fun getArtistComparator(ascending: Boolean): Comparator<Artist> =
MultiComparator( MultiComparator(
compareByDynamic(ascending, NullableComparator.INT) { it.songs.size }, compareByDynamic(ascending, NullableComparator.INT) { it.songs.size },
compareBy(BasicComparator.ARTIST) compareBy(BasicComparator.ARTIST))
)
override fun getGenreComparator(ascending: Boolean): Comparator<Genre> = override fun getGenreComparator(ascending: Boolean): Comparator<Genre> =
MultiComparator( MultiComparator(
compareByDynamic(ascending) { it.songs.size }, compareByDynamic(ascending) { it.songs.size }, compareBy(BasicComparator.GENRE))
compareBy(BasicComparator.GENRE)
)
} }
/** Sort by the disc, and then track number of an item. Only supported by [Song]. */ /** Sort by the disc, and then track number of an item. Only supported by [Song]. */
@ -267,8 +250,7 @@ data class Sort(val mode: Mode, val isAscending: Boolean) {
MultiComparator( MultiComparator(
compareByDynamic(ascending, NullableComparator.INT) { it.disc }, compareByDynamic(ascending, NullableComparator.INT) { it.disc },
compareBy(NullableComparator.INT) { it.track }, compareBy(NullableComparator.INT) { it.track },
compareBy(BasicComparator.SONG) compareBy(BasicComparator.SONG))
)
} }
/** /**
@ -286,8 +268,7 @@ data class Sort(val mode: Mode, val isAscending: Boolean) {
MultiComparator( MultiComparator(
compareBy(NullableComparator.INT) { it.disc }, compareBy(NullableComparator.INT) { it.disc },
compareByDynamic(ascending, NullableComparator.INT) { it.track }, compareByDynamic(ascending, NullableComparator.INT) { it.track },
compareBy(BasicComparator.SONG) compareBy(BasicComparator.SONG))
)
} }
/** Sort by the time the item was added. Only supported by [Song] */ /** Sort by the time the item was added. Only supported by [Song] */
@ -300,15 +281,12 @@ data class Sort(val mode: Mode, val isAscending: Boolean) {
override fun getSongComparator(ascending: Boolean): Comparator<Song> = override fun getSongComparator(ascending: Boolean): Comparator<Song> =
MultiComparator( MultiComparator(
compareByDynamic(ascending) { it.dateAdded }, compareByDynamic(ascending) { it.dateAdded }, compareBy(BasicComparator.SONG))
compareBy(BasicComparator.SONG)
)
override fun getAlbumComparator(ascending: Boolean): Comparator<Album> = override fun getAlbumComparator(ascending: Boolean): Comparator<Album> =
MultiComparator( MultiComparator(
compareByDynamic(ascending) { album -> album.songs.minOf { it.dateAdded } }, compareByDynamic(ascending) { album -> album.songs.minOf { it.dateAdded } },
compareBy(BasicComparator.ALBUM) compareBy(BasicComparator.ALBUM))
)
} }
protected inline fun <T : Music, K> compareByDynamic( protected inline fun <T : Music, K> compareByDynamic(

View file

@ -19,22 +19,18 @@ package org.oxycblt.auxio.music
import android.content.Context import android.content.Context
import android.os.Build import android.os.Build
import android.text.format.DateUtils
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.R
import org.oxycblt.auxio.playback.secsToMs
import org.oxycblt.auxio.util.inRangeOrNull
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logE
import org.oxycblt.auxio.util.nonZeroOrNull
import java.time.ZoneId import java.time.ZoneId
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import java.time.temporal.TemporalQueries import java.time.temporal.TemporalQueries
import java.util.Formatter
import java.util.Locale import java.util.Locale
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.R
import org.oxycblt.auxio.util.inRangeOrNull
import org.oxycblt.auxio.util.logE
import org.oxycblt.auxio.util.nonZeroOrNull
/** /**
* An ISO-8601/RFC 3339 Date. * An ISO-8601/RFC 3339 Date.
@ -81,9 +77,8 @@ class Date private constructor(private val tokens: List<Int>) : Comparable<Date>
private val second = tokens.getOrNull(5) private val second = tokens.getOrNull(5)
/** /**
* Resolve this date into a string. This could result in a year string formatted * Resolve this date into a string. This could result in a year string formatted as "YYYY", or a
* as "YYYY", or a month and year string formatted as "MMM YYYY" depending on the * month and year string formatted as "MMM YYYY" depending on the situation.
* situation.
*/ */
fun resolveDate(context: Context): String { fun resolveDate(context: Context): String {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@ -101,17 +96,17 @@ class Date private constructor(private val tokens: List<Int>) : Comparable<Date>
@RequiresApi(Build.VERSION_CODES.O) @RequiresApi(Build.VERSION_CODES.O)
private fun resolveFullDate(context: Context) = private fun resolveFullDate(context: Context) =
if( month != null ) { if (month != null) {
val temporal = DateTimeFormatter.ISO_DATE.parse( val temporal =
"$year-$month-${day ?: 1}", DateTimeFormatter.ISO_DATE.parse(
TemporalQueries.localDate() "$year-$month-${day ?: 1}", TemporalQueries.localDate())
)
// When it comes to songs, we only want to show the month and year. This // When it comes to songs, we only want to show the month and year. This
// cannot be done with DateUtils due to it's dynamic nature, so instead // cannot be done with DateUtils due to it's dynamic nature, so instead
// it's done with the built-in date formatter. Since the legacy date API // it's done with the built-in date formatter. Since the legacy date API
// is awful, we only use instant and limit it to Android 8 onwards. // is awful, we only use instant and limit it to Android 8 onwards.
temporal.atStartOfDay(ZoneId.systemDefault()) temporal
.atStartOfDay(ZoneId.systemDefault())
.format(DateTimeFormatter.ofPattern("MMM yyyy", Locale.getDefault())) .format(DateTimeFormatter.ofPattern("MMM yyyy", Locale.getDefault()))
} else { } else {
resolveYear(context) resolveYear(context)
@ -161,8 +156,7 @@ class Date private constructor(private val tokens: List<Int>) : Comparable<Date>
companion object { companion object {
private val ISO8601_REGEX = private val ISO8601_REGEX =
Regex( Regex(
"""^(\d{4,})([-.](\d{2})([-.](\d{2})([T ](\d{2})([:.](\d{2})([:.](\d{2})(Z)?)?)?)?)?)?$""" """^(\d{4,})([-.](\d{2})([-.](\d{2})([T ](\d{2})([:.](\d{2})([:.](\d{2})(Z)?)?)?)?)?)?$""")
)
fun from(year: Int) = fromTokens(listOf(year)) fun from(year: Int) = fromTokens(listOf(year))
@ -246,11 +240,12 @@ sealed class ReleaseType {
data class Compilation(override val refinement: Refinement?) : ReleaseType() { data class Compilation(override val refinement: Refinement?) : ReleaseType() {
override val stringRes: Int override val stringRes: Int
get() = when (refinement) { get() =
null -> R.string.lbl_compilation when (refinement) {
Refinement.LIVE -> R.string.lbl_compilation_live null -> R.string.lbl_compilation
Refinement.REMIX -> R.string.lbl_compilation_remix Refinement.LIVE -> R.string.lbl_compilation_live
} Refinement.REMIX -> R.string.lbl_compilation_remix
}
} }
object Soundtrack : ReleaseType() { object Soundtrack : ReleaseType() {
@ -325,14 +320,15 @@ sealed class ReleaseType {
private inline fun parseSecondaryTypeImpl( private inline fun parseSecondaryTypeImpl(
type: String?, type: String?,
convertRefinement: (Refinement?) -> ReleaseType convertRefinement: (Refinement?) -> ReleaseType
) = when { ) =
// Parse all the types that have no children when {
type.equals("soundtrack", true) -> Soundtrack // Parse all the types that have no children
type.equals("mixtape/street", true) -> Mixtape type.equals("soundtrack", true) -> Soundtrack
type.equals("dj-mix", true) -> Mix type.equals("mixtape/street", true) -> Mixtape
type.equals("live", true) -> convertRefinement(Refinement.LIVE) type.equals("dj-mix", true) -> Mix
type.equals("remix", true) -> convertRefinement(Refinement.REMIX) type.equals("live", true) -> convertRefinement(Refinement.LIVE)
else -> convertRefinement(null) type.equals("remix", true) -> convertRefinement(Refinement.REMIX)
} else -> convertRefinement(null)
}
} }
} }

View file

@ -24,18 +24,18 @@ import android.database.sqlite.SQLiteOpenHelper
import androidx.core.database.getIntOrNull import androidx.core.database.getIntOrNull
import androidx.core.database.getStringOrNull import androidx.core.database.getStringOrNull
import androidx.core.database.sqlite.transaction import androidx.core.database.sqlite.transaction
import java.io.File
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logE import org.oxycblt.auxio.util.logE
import org.oxycblt.auxio.util.queryAll import org.oxycblt.auxio.util.queryAll
import org.oxycblt.auxio.util.requireBackgroundThread import org.oxycblt.auxio.util.requireBackgroundThread
import java.io.File
/** /**
* The extractor that caches music metadata for faster use later. The cache is only responsible for * The extractor that caches music metadata for faster use later. The cache is only responsible for
* storing "intrinsic" data, as in information derived from the file format and not * storing "intrinsic" data, as in information derived from the file format and not information from
* information from the media database or file system. The exceptions are the database ID and * the media database or file system. The exceptions are the database ID and modification times for
* modification times for files, as these are required for the cache to function well. * files, as these are required for the cache to function well.
* @author OxygenCobalt * @author OxygenCobalt
*/ */
class CacheExtractor(private val context: Context, private val noop: Boolean) { class CacheExtractor(private val context: Context, private val noop: Boolean) {
@ -55,9 +55,7 @@ class CacheExtractor(private val context: Context, private val noop: Boolean) {
} }
} }
/** /** Write a list of newly-indexed raw songs to the database. */
* Write a list of newly-indexed raw songs to the database.
*/
fun finalize(rawSongs: List<Song.Raw>) { fun finalize(rawSongs: List<Song.Raw>) {
cacheMap = null cacheMap = null
@ -75,14 +73,16 @@ class CacheExtractor(private val context: Context, private val noop: Boolean) {
} }
/** /**
* Maybe copy a cached raw song into this instance, assuming that it has not changed * Maybe copy a cached raw song into this instance, assuming that it has not changed since it
* since it was last saved. Returns true if a song was loaded. * was last saved. Returns true if a song was loaded.
*/ */
fun populateFromCache(rawSong: Song.Raw): Boolean { fun populateFromCache(rawSong: Song.Raw): Boolean {
val map = cacheMap ?: return false val map = cacheMap ?: return false
val cachedRawSong = map[rawSong.mediaStoreId] val cachedRawSong = map[rawSong.mediaStoreId]
if (cachedRawSong != null && cachedRawSong.dateAdded == rawSong.dateAdded && cachedRawSong.dateModified == rawSong.dateModified) { if (cachedRawSong != null &&
cachedRawSong.dateAdded == rawSong.dateAdded &&
cachedRawSong.dateModified == rawSong.dateModified) {
rawSong.musicBrainzId = cachedRawSong.musicBrainzId rawSong.musicBrainzId = cachedRawSong.musicBrainzId
rawSong.name = cachedRawSong.name rawSong.name = cachedRawSong.name
rawSong.sortName = cachedRawSong.sortName rawSong.sortName = cachedRawSong.sortName
@ -118,33 +118,35 @@ class CacheExtractor(private val context: Context, private val noop: Boolean) {
} }
} }
private class CacheDatabase(context: Context) : SQLiteOpenHelper(context, File(context.cacheDir, DB_NAME).absolutePath, null, DB_VERSION) { private class CacheDatabase(context: Context) :
SQLiteOpenHelper(context, File(context.cacheDir, DB_NAME).absolutePath, null, DB_VERSION) {
override fun onCreate(db: SQLiteDatabase) { override fun onCreate(db: SQLiteDatabase) {
val command = StringBuilder() val command =
.append("CREATE TABLE IF NOT EXISTS $TABLE_RAW_SONGS(") StringBuilder()
.append("${Columns.MEDIA_STORE_ID} LONG PRIMARY KEY,") .append("CREATE TABLE IF NOT EXISTS $TABLE_RAW_SONGS(")
.append("${Columns.DATE_ADDED} LONG NOT NULL,") .append("${Columns.MEDIA_STORE_ID} LONG PRIMARY KEY,")
.append("${Columns.DATE_MODIFIED} LONG NOT NULL,") .append("${Columns.DATE_ADDED} LONG NOT NULL,")
.append("${Columns.SIZE} LONG NOT NULL,") .append("${Columns.DATE_MODIFIED} LONG NOT NULL,")
.append("${Columns.DURATION} LONG NOT NULL,") .append("${Columns.SIZE} LONG NOT NULL,")
.append("${Columns.FORMAT_MIME_TYPE} STRING,") .append("${Columns.DURATION} LONG NOT NULL,")
.append("${Columns.MUSIC_BRAINZ_ID} STRING,") .append("${Columns.FORMAT_MIME_TYPE} STRING,")
.append("${Columns.NAME} STRING NOT NULL,") .append("${Columns.MUSIC_BRAINZ_ID} STRING,")
.append("${Columns.SORT_NAME} STRING,") .append("${Columns.NAME} STRING NOT NULL,")
.append("${Columns.TRACK} INT,") .append("${Columns.SORT_NAME} STRING,")
.append("${Columns.DISC} INT,") .append("${Columns.TRACK} INT,")
.append("${Columns.DATE} STRING,") .append("${Columns.DISC} INT,")
.append("${Columns.ALBUM_MUSIC_BRAINZ_ID} STRING,") .append("${Columns.DATE} STRING,")
.append("${Columns.ALBUM_NAME} STRING NOT NULL,") .append("${Columns.ALBUM_MUSIC_BRAINZ_ID} STRING,")
.append("${Columns.ALBUM_SORT_NAME} STRING,") .append("${Columns.ALBUM_NAME} STRING NOT NULL,")
.append("${Columns.ALBUM_RELEASE_TYPES} STRING,") .append("${Columns.ALBUM_SORT_NAME} STRING,")
.append("${Columns.ARTIST_MUSIC_BRAINZ_IDS} STRING,") .append("${Columns.ALBUM_RELEASE_TYPES} STRING,")
.append("${Columns.ARTIST_NAMES} STRING,") .append("${Columns.ARTIST_MUSIC_BRAINZ_IDS} STRING,")
.append("${Columns.ARTIST_SORT_NAMES} STRING,") .append("${Columns.ARTIST_NAMES} STRING,")
.append("${Columns.ALBUM_ARTIST_MUSIC_BRAINZ_IDS} STRING,") .append("${Columns.ARTIST_SORT_NAMES} STRING,")
.append("${Columns.ALBUM_ARTIST_NAMES} STRING,") .append("${Columns.ALBUM_ARTIST_MUSIC_BRAINZ_IDS} STRING,")
.append("${Columns.ALBUM_ARTIST_SORT_NAMES} STRING,") .append("${Columns.ALBUM_ARTIST_NAMES} STRING,")
.append("${Columns.GENRE_NAMES} STRING)") .append("${Columns.ALBUM_ARTIST_SORT_NAMES} STRING,")
.append("${Columns.GENRE_NAMES} STRING)")
db.execSQL(command.toString()) db.execSQL(command.toString())
} }
@ -185,18 +187,22 @@ private class CacheDatabase(context: Context) : SQLiteOpenHelper(context, File(c
val discIndex = cursor.getColumnIndexOrThrow(Columns.DISC) val discIndex = cursor.getColumnIndexOrThrow(Columns.DISC)
val dateIndex = cursor.getColumnIndexOrThrow(Columns.DATE) val dateIndex = cursor.getColumnIndexOrThrow(Columns.DATE)
val albumMusicBrainzIdIndex = cursor.getColumnIndexOrThrow(Columns.ALBUM_MUSIC_BRAINZ_ID) val albumMusicBrainzIdIndex =
cursor.getColumnIndexOrThrow(Columns.ALBUM_MUSIC_BRAINZ_ID)
val albumNameIndex = cursor.getColumnIndexOrThrow(Columns.ALBUM_NAME) val albumNameIndex = cursor.getColumnIndexOrThrow(Columns.ALBUM_NAME)
val albumSortNameIndex = cursor.getColumnIndexOrThrow(Columns.ALBUM_SORT_NAME) val albumSortNameIndex = cursor.getColumnIndexOrThrow(Columns.ALBUM_SORT_NAME)
val albumReleaseTypesIndex = cursor.getColumnIndexOrThrow(Columns.ALBUM_RELEASE_TYPES) val albumReleaseTypesIndex = cursor.getColumnIndexOrThrow(Columns.ALBUM_RELEASE_TYPES)
val artistMusicBrainzIdsIndex = cursor.getColumnIndexOrThrow(Columns.ARTIST_MUSIC_BRAINZ_IDS) val artistMusicBrainzIdsIndex =
cursor.getColumnIndexOrThrow(Columns.ARTIST_MUSIC_BRAINZ_IDS)
val artistNamesIndex = cursor.getColumnIndexOrThrow(Columns.ARTIST_NAMES) val artistNamesIndex = cursor.getColumnIndexOrThrow(Columns.ARTIST_NAMES)
val artistSortNamesIndex = cursor.getColumnIndexOrThrow(Columns.ARTIST_SORT_NAMES) val artistSortNamesIndex = cursor.getColumnIndexOrThrow(Columns.ARTIST_SORT_NAMES)
val albumArtistMusicBrainzIdsIndex = cursor.getColumnIndexOrThrow(Columns.ALBUM_ARTIST_MUSIC_BRAINZ_IDS) val albumArtistMusicBrainzIdsIndex =
cursor.getColumnIndexOrThrow(Columns.ALBUM_ARTIST_MUSIC_BRAINZ_IDS)
val albumArtistNamesIndex = cursor.getColumnIndexOrThrow(Columns.ALBUM_ARTIST_NAMES) val albumArtistNamesIndex = cursor.getColumnIndexOrThrow(Columns.ALBUM_ARTIST_NAMES)
val albumArtistSortNamesIndex = cursor.getColumnIndexOrThrow(Columns.ALBUM_ARTIST_SORT_NAMES) val albumArtistSortNamesIndex =
cursor.getColumnIndexOrThrow(Columns.ALBUM_ARTIST_SORT_NAMES)
val genresIndex = cursor.getColumnIndexOrThrow(Columns.GENRE_NAMES) val genresIndex = cursor.getColumnIndexOrThrow(Columns.GENRE_NAMES)
@ -223,26 +229,31 @@ private class CacheDatabase(context: Context) : SQLiteOpenHelper(context, File(c
raw.albumMusicBrainzId = cursor.getStringOrNull(albumMusicBrainzIdIndex) raw.albumMusicBrainzId = cursor.getStringOrNull(albumMusicBrainzIdIndex)
raw.albumName = cursor.getString(albumNameIndex) raw.albumName = cursor.getString(albumNameIndex)
raw.albumSortName = cursor.getStringOrNull(albumSortNameIndex) raw.albumSortName = cursor.getStringOrNull(albumSortNameIndex)
cursor.getStringOrNull(albumReleaseTypesIndex)?.parseMultiValue() cursor.getStringOrNull(albumReleaseTypesIndex)?.parseMultiValue()?.let {
?.let { raw.albumReleaseTypes = it } raw.albumReleaseTypes = it
}
cursor.getStringOrNull(artistMusicBrainzIdsIndex)?.let { cursor.getStringOrNull(artistMusicBrainzIdsIndex)?.let {
raw.artistMusicBrainzIds = it.parseMultiValue() raw.artistMusicBrainzIds = it.parseMultiValue()
} }
cursor.getStringOrNull(artistNamesIndex) cursor.getStringOrNull(artistNamesIndex)?.let {
?.let { raw.artistNames = it.parseMultiValue() } raw.artistNames = it.parseMultiValue()
cursor.getStringOrNull(artistSortNamesIndex) }
?.let { raw.artistSortNames = it.parseMultiValue() } cursor.getStringOrNull(artistSortNamesIndex)?.let {
raw.artistSortNames = it.parseMultiValue()
}
cursor.getStringOrNull(albumArtistMusicBrainzIdsIndex) cursor.getStringOrNull(albumArtistMusicBrainzIdsIndex)?.let {
?.let { raw.albumArtistMusicBrainzIds = it.parseMultiValue() } raw.albumArtistMusicBrainzIds = it.parseMultiValue()
cursor.getStringOrNull(albumArtistNamesIndex) }
?.let { raw.albumArtistNames = it.parseMultiValue() } cursor.getStringOrNull(albumArtistNamesIndex)?.let {
cursor.getStringOrNull(albumArtistSortNamesIndex) raw.albumArtistNames = it.parseMultiValue()
?.let { raw.albumArtistSortNames = it.parseMultiValue() } }
cursor.getStringOrNull(albumArtistSortNamesIndex)?.let {
raw.albumArtistSortNames = it.parseMultiValue()
}
cursor.getStringOrNull(genresIndex) cursor.getStringOrNull(genresIndex)?.let { raw.genreNames = it.parseMultiValue() }
?.let { raw.genreNames = it.parseMultiValue() }
map[id] = raw map[id] = raw
} }
@ -287,15 +298,23 @@ private class CacheDatabase(context: Context) : SQLiteOpenHelper(context, File(c
put(Columns.ALBUM_MUSIC_BRAINZ_ID, rawSong.albumMusicBrainzId) put(Columns.ALBUM_MUSIC_BRAINZ_ID, rawSong.albumMusicBrainzId)
put(Columns.ALBUM_NAME, rawSong.albumName) put(Columns.ALBUM_NAME, rawSong.albumName)
put(Columns.ALBUM_SORT_NAME, rawSong.albumSortName) put(Columns.ALBUM_SORT_NAME, rawSong.albumSortName)
put(Columns.ALBUM_RELEASE_TYPES, rawSong.albumReleaseTypes.toMultiValue()) put(
Columns.ALBUM_RELEASE_TYPES,
rawSong.albumReleaseTypes.toMultiValue())
put(Columns.ARTIST_MUSIC_BRAINZ_IDS, rawSong.artistMusicBrainzIds.toMultiValue()) put(
Columns.ARTIST_MUSIC_BRAINZ_IDS,
rawSong.artistMusicBrainzIds.toMultiValue())
put(Columns.ARTIST_NAMES, rawSong.artistNames.toMultiValue()) put(Columns.ARTIST_NAMES, rawSong.artistNames.toMultiValue())
put(Columns.ARTIST_SORT_NAMES, rawSong.artistSortNames.toMultiValue()) put(Columns.ARTIST_SORT_NAMES, rawSong.artistSortNames.toMultiValue())
put(Columns.ALBUM_ARTIST_MUSIC_BRAINZ_IDS, rawSong.albumArtistMusicBrainzIds.toMultiValue()) put(
Columns.ALBUM_ARTIST_MUSIC_BRAINZ_IDS,
rawSong.albumArtistMusicBrainzIds.toMultiValue())
put(Columns.ALBUM_ARTIST_NAMES, rawSong.albumArtistNames.toMultiValue()) put(Columns.ALBUM_ARTIST_NAMES, rawSong.albumArtistNames.toMultiValue())
put(Columns.ALBUM_ARTIST_SORT_NAMES, rawSong.albumArtistSortNames.toMultiValue()) put(
Columns.ALBUM_ARTIST_SORT_NAMES,
rawSong.albumArtistSortNames.toMultiValue())
put(Columns.GENRE_NAMES, rawSong.genreNames.toMultiValue()) put(Columns.GENRE_NAMES, rawSong.genreNames.toMultiValue())
} }

View file

@ -26,6 +26,7 @@ import android.provider.MediaStore
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.core.database.getIntOrNull import androidx.core.database.getIntOrNull
import androidx.core.database.getStringOrNull import androidx.core.database.getStringOrNull
import java.io.File
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.storage.Directory import org.oxycblt.auxio.music.storage.Directory
import org.oxycblt.auxio.music.storage.directoryCompat import org.oxycblt.auxio.music.storage.directoryCompat
@ -37,7 +38,6 @@ import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.util.contentResolverSafe import org.oxycblt.auxio.util.contentResolverSafe
import org.oxycblt.auxio.util.getSystemServiceCompat import org.oxycblt.auxio.util.getSystemServiceCompat
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import java.io.File
/* /*
* This file acts as the base for most the black magic required to get a remotely sensible music * This file acts as the base for most the black magic required to get a remotely sensible music
@ -100,7 +100,10 @@ import java.io.File
* music loading process. * music loading process.
* @author OxygenCobalt * @author OxygenCobalt
*/ */
abstract class MediaStoreExtractor(private val context: Context, private val cacheDatabase: CacheExtractor) { abstract class MediaStoreExtractor(
private val context: Context,
private val cacheDatabase: CacheExtractor
) {
private var cursor: Cursor? = null private var cursor: Cursor? = null
private var idIndex = -1 private var idIndex = -1
@ -173,13 +176,11 @@ abstract class MediaStoreExtractor(private val context: Context, private val cac
val cursor = val cursor =
requireNotNull( requireNotNull(
context.contentResolverSafe.queryCursor( context.contentResolverSafe.queryCursor(
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
projection, projection,
selector, selector,
args.toTypedArray() args.toTypedArray())) { "Content resolver failure: No Cursor returned" }
)
) { "Content resolver failure: No Cursor returned" }
.also { cursor = it } .also { cursor = it }
idIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns._ID) idIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns._ID)
@ -207,8 +208,7 @@ abstract class MediaStoreExtractor(private val context: Context, private val cac
// obscure formats where genre support is only really covered by this. // obscure formats where genre support is only really covered by this.
context.contentResolverSafe.useQuery( context.contentResolverSafe.useQuery(
MediaStore.Audio.Genres.EXTERNAL_CONTENT_URI, MediaStore.Audio.Genres.EXTERNAL_CONTENT_URI,
arrayOf(MediaStore.Audio.Genres._ID, MediaStore.Audio.Genres.NAME) arrayOf(MediaStore.Audio.Genres._ID, MediaStore.Audio.Genres.NAME)) { genreCursor ->
) { genreCursor ->
val idIndex = genreCursor.getColumnIndexOrThrow(MediaStore.Audio.Genres._ID) val idIndex = genreCursor.getColumnIndexOrThrow(MediaStore.Audio.Genres._ID)
val nameIndex = genreCursor.getColumnIndexOrThrow(MediaStore.Audio.Genres.NAME) val nameIndex = genreCursor.getColumnIndexOrThrow(MediaStore.Audio.Genres.NAME)
@ -218,8 +218,7 @@ abstract class MediaStoreExtractor(private val context: Context, private val cac
context.contentResolverSafe.useQuery( context.contentResolverSafe.useQuery(
MediaStore.Audio.Genres.Members.getContentUri(VOLUME_EXTERNAL, id), MediaStore.Audio.Genres.Members.getContentUri(VOLUME_EXTERNAL, id),
arrayOf(MediaStore.Audio.Genres.Members._ID) arrayOf(MediaStore.Audio.Genres.Members._ID)) { cursor ->
) { cursor ->
val songIdIndex = val songIdIndex =
cursor.getColumnIndexOrThrow(MediaStore.Audio.Genres.Members._ID) cursor.getColumnIndexOrThrow(MediaStore.Audio.Genres.Members._ID)
@ -292,15 +291,14 @@ abstract class MediaStoreExtractor(private val context: Context, private val cac
MediaStore.Audio.AudioColumns.ALBUM, MediaStore.Audio.AudioColumns.ALBUM,
MediaStore.Audio.AudioColumns.ALBUM_ID, MediaStore.Audio.AudioColumns.ALBUM_ID,
MediaStore.Audio.AudioColumns.ARTIST, MediaStore.Audio.AudioColumns.ARTIST,
AUDIO_COLUMN_ALBUM_ARTIST AUDIO_COLUMN_ALBUM_ARTIST)
)
protected abstract val dirSelector: String protected abstract val dirSelector: String
protected abstract fun addDirToSelectorArgs(dir: Directory, args: MutableList<String>): Boolean protected abstract fun addDirToSelectorArgs(dir: Directory, args: MutableList<String>): Boolean
/** /**
* Populate the "file data" of the cursor, or data that is required to access a cache entry * Populate the "file data" of the cursor, or data that is required to access a cache entry or
* or makes no sense to cache. This includes database IDs, modification dates, * makes no sense to cache. This includes database IDs, modification dates,
*/ */
protected open fun populateFileData(cursor: Cursor, raw: Song.Raw) { protected open fun populateFileData(cursor: Cursor, raw: Song.Raw) {
raw.mediaStoreId = cursor.getLong(idIndex) raw.mediaStoreId = cursor.getLong(idIndex)
@ -315,9 +313,7 @@ abstract class MediaStoreExtractor(private val context: Context, private val cac
raw.albumMediaStoreId = cursor.getLong(albumIdIndex) raw.albumMediaStoreId = cursor.getLong(albumIdIndex)
} }
/** /** Extract cursor metadata into [raw]. */
* Extract cursor metadata into [raw].
*/
protected open fun populateMetadata(cursor: Cursor, raw: Song.Raw) { protected open fun populateMetadata(cursor: Cursor, raw: Song.Raw) {
raw.name = cursor.getString(titleIndex) raw.name = cursor.getString(titleIndex)
@ -362,8 +358,7 @@ abstract class MediaStoreExtractor(private val context: Context, private val cac
* External has existed since at least API 21, but no constant existed for it until API 29. * External has existed since at least API 21, but no constant existed for it until API 29.
* This constant is safe to use. * This constant is safe to use.
*/ */
@Suppress("InlinedApi") @Suppress("InlinedApi") private const val VOLUME_EXTERNAL = MediaStore.VOLUME_EXTERNAL
private const val VOLUME_EXTERNAL = MediaStore.VOLUME_EXTERNAL
/** /**
* The base selector that works across all versions of android. Does not exclude * The base selector that works across all versions of android. Does not exclude
@ -377,8 +372,8 @@ abstract class MediaStoreExtractor(private val context: Context, private val cac
// speed, we only want to add redundancy on known issues, not with possible issues. // speed, we only want to add redundancy on known issues, not with possible issues.
/** /**
* A [MediaStoreExtractor] that completes the music loading process in a way compatible from * A [MediaStoreExtractor] that completes the music loading process in a way compatible from API 21
* API 21 onwards to API 29. * onwards to API 29.
* @author OxygenCobalt * @author OxygenCobalt
*/ */
class Api21MediaStoreExtractor(context: Context, cacheDatabase: CacheExtractor) : class Api21MediaStoreExtractor(context: Context, cacheDatabase: CacheExtractor) :
@ -471,8 +466,7 @@ open class BaseApi29MediaStoreExtractor(context: Context, cacheDatabase: CacheEx
super.projection + super.projection +
arrayOf( arrayOf(
MediaStore.Audio.AudioColumns.VOLUME_NAME, MediaStore.Audio.AudioColumns.VOLUME_NAME,
MediaStore.Audio.AudioColumns.RELATIVE_PATH MediaStore.Audio.AudioColumns.RELATIVE_PATH)
)
override val dirSelector: String override val dirSelector: String
get() = get() =
@ -502,8 +496,8 @@ open class BaseApi29MediaStoreExtractor(context: Context, cacheDatabase: CacheEx
} }
/** /**
* A [MediaStoreExtractor] that completes the music loading process in a way compatible with at least * A [MediaStoreExtractor] that completes the music loading process in a way compatible with at
* API 29. * least API 29.
* @author OxygenCobalt * @author OxygenCobalt
*/ */
@RequiresApi(Build.VERSION_CODES.Q) @RequiresApi(Build.VERSION_CODES.Q)
@ -534,8 +528,8 @@ open class Api29MediaStoreExtractor(context: Context, cacheDatabase: CacheExtrac
} }
/** /**
* A [MediaStoreExtractor] that completes the music loading process in a way compatible with at least * A [MediaStoreExtractor] that completes the music loading process in a way compatible with at
* API 30. * least API 30.
* @author OxygenCobalt * @author OxygenCobalt
*/ */
@RequiresApi(Build.VERSION_CODES.R) @RequiresApi(Build.VERSION_CODES.R)
@ -556,8 +550,7 @@ class Api30MediaStoreExtractor(context: Context, cacheDatabase: CacheExtractor)
super.projection + super.projection +
arrayOf( arrayOf(
MediaStore.Audio.AudioColumns.CD_TRACK_NUMBER, MediaStore.Audio.AudioColumns.CD_TRACK_NUMBER,
MediaStore.Audio.AudioColumns.DISC_NUMBER MediaStore.Audio.AudioColumns.DISC_NUMBER)
)
override fun populateMetadata(cursor: Cursor, raw: Song.Raw) { override fun populateMetadata(cursor: Cursor, raw: Song.Raw) {
super.populateMetadata(cursor, raw) super.populateMetadata(cursor, raw)

View file

@ -45,7 +45,10 @@ import org.oxycblt.auxio.util.logW
* *
* @author OxygenCobalt * @author OxygenCobalt
*/ */
class MetadataExtractor(private val context: Context, private val mediaStoreExtractor: MediaStoreExtractor) { class MetadataExtractor(
private val context: Context,
private val mediaStoreExtractor: MediaStoreExtractor
) {
private val taskPool: Array<Task?> = arrayOfNulls(TASK_CAPACITY) private val taskPool: Array<Task?> = arrayOfNulls(TASK_CAPACITY)
/** Initialize the sub-layers that this layer relies on. */ /** Initialize the sub-layers that this layer relies on. */
@ -116,8 +119,7 @@ class Task(context: Context, private val raw: Song.Raw) {
private val future = private val future =
MetadataRetriever.retrieveMetadata( MetadataRetriever.retrieveMetadata(
context, context,
MediaItem.fromUri(requireNotNull(raw.mediaStoreId) { "Invalid raw: No id" }.audioUri) MediaItem.fromUri(requireNotNull(raw.mediaStoreId) { "Invalid raw: No id" }.audioUri))
)
/** /**
* Get the song that this task is trying to complete. If the task is still busy, this will * Get the song that this task is trying to complete. If the task is still busy, this will
@ -215,20 +217,16 @@ class Task(context: Context, private val raw: Song.Raw) {
// 3. ID3v2.4 Release Date, as it is the second most common date type // 3. ID3v2.4 Release Date, as it is the second most common date type
// 4. ID3v2.3 Original Date, as it is like #1 // 4. ID3v2.3 Original Date, as it is like #1
// 5. ID3v2.3 Release Year, as it is the most common date type // 5. ID3v2.3 Release Year, as it is the most common date type
( (tags["TDOR"]?.run { get(0).parseTimestamp() }
tags["TDOR"]?.run { get(0).parseTimestamp() }
?: tags["TDRC"]?.run { get(0).parseTimestamp() } ?: tags["TDRC"]?.run { get(0).parseTimestamp() }
?: tags["TDRL"]?.run { get(0).parseTimestamp() } ?: parseId3v23Date(tags) ?: tags["TDRL"]?.run { get(0).parseTimestamp() } ?: parseId3v23Date(tags))
)
?.let { raw.date = it } ?.let { raw.date = it }
// Album // Album
tags["TXXX:MusicBrainz Album Id"]?.let { raw.albumMusicBrainzId = it[0] } tags["TXXX:MusicBrainz Album Id"]?.let { raw.albumMusicBrainzId = it[0] }
tags["TALB"]?.let { raw.albumName = it[0] } tags["TALB"]?.let { raw.albumName = it[0] }
tags["TSOA"]?.let { raw.albumSortName = it[0] } tags["TSOA"]?.let { raw.albumSortName = it[0] }
(tags["TXXX:MusicBrainz Album Type"] ?: tags["GRP1"])?.let { (tags["TXXX:MusicBrainz Album Type"] ?: tags["GRP1"])?.let { raw.albumReleaseTypes = it }
raw.albumReleaseTypes = it
}
// Artist // Artist
tags["TXXX:MusicBrainz Artist Id"]?.let { raw.artistMusicBrainzIds = it } tags["TXXX:MusicBrainz Artist Id"]?.let { raw.artistMusicBrainzIds = it }
@ -288,11 +286,9 @@ class Task(context: Context, private val raw: Song.Raw) {
// 2. Date, as it is the most common date type // 2. Date, as it is the most common date type
// 3. Year, as old vorbis tags tended to use this (I know this because it's the only // 3. Year, as old vorbis tags tended to use this (I know this because it's the only
// tag that android supports, so it must be 15 years old or more!) // tag that android supports, so it must be 15 years old or more!)
( (tags["ORIGINALDATE"]?.run { get(0).parseTimestamp() }
tags["ORIGINALDATE"]?.run { get(0).parseTimestamp() }
?: tags["DATE"]?.run { get(0).parseTimestamp() } ?: tags["DATE"]?.run { get(0).parseTimestamp() }
?: tags["YEAR"]?.run { get(0).parseYear() } ?: tags["YEAR"]?.run { get(0).parseYear() })
)
?.let { raw.date = it } ?.let { raw.date = it }
// Album // Album

View file

@ -18,10 +18,10 @@
package org.oxycblt.auxio.music.extractor package org.oxycblt.auxio.music.extractor
import androidx.core.text.isDigitsOnly import androidx.core.text.isDigitsOnly
import java.util.UUID
import org.oxycblt.auxio.music.Date import org.oxycblt.auxio.music.Date
import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.util.nonZeroOrNull import org.oxycblt.auxio.util.nonZeroOrNull
import java.util.UUID
/** /**
* Parse out the track number field as if the given Int is formatted as DTTT, where D Is the disc * Parse out the track number field as if the given Int is formatted as DTTT, where D Is the disc
@ -51,9 +51,7 @@ fun String.parseYear() = toIntOrNull()?.toDate()
/** Parse an ISO-8601 time-stamp from this field into a [Date]. */ /** Parse an ISO-8601 time-stamp from this field into a [Date]. */
fun String.parseTimestamp() = Date.from(this) fun String.parseTimestamp() = Date.from(this)
/** /** Parse a string by [selector], also handling string escaping. */
* Parse a string by [selector], also handling string escaping.
*/
inline fun String.splitEscaped(selector: (Char) -> Boolean): MutableList<String> { inline fun String.splitEscaped(selector: (Char) -> Boolean): MutableList<String> {
val split = mutableListOf<String>() val split = mutableListOf<String>()
var currentString = "" var currentString = ""
@ -110,11 +108,12 @@ fun String.maybeParseSeparators(settings: Settings): List<String> {
return splitEscaped { separators.contains(it) } return splitEscaped { separators.contains(it) }
} }
fun String.toUuidOrNull(): UUID? = try { fun String.toUuidOrNull(): UUID? =
UUID.fromString(this) try {
} catch (e: IllegalArgumentException) { UUID.fromString(this)
null } catch (e: IllegalArgumentException) {
} null
}
/** /**
* Parse a multi-value genre name using ID3v2 rules. If there is one value, the ID3v2.3 rules will * Parse a multi-value genre name using ID3v2 rules. If there is one value, the ID3v2.3 rules will
@ -392,5 +391,4 @@ private val GENRE_TABLE =
"Psybient", "Psybient",
// Auxio's extensions (Future garage is also based and deserves a slot) // Auxio's extensions (Future garage is also based and deserves a slot)
"Future Garage" "Future Garage")
)

View file

@ -27,10 +27,9 @@ import org.oxycblt.auxio.ui.recycler.ItemClickListener
import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.context
import org.oxycblt.auxio.util.inflater import org.oxycblt.auxio.util.inflater
/** /** The adapter that displays a list of artist choices in the picker UI. */
* The adapter that displays a list of artist choices in the picker UI. class ArtistChoiceAdapter(private val listener: ItemClickListener) :
*/ RecyclerView.Adapter<ArtistChoiceViewHolder>() {
class ArtistChoiceAdapter(private val listener: ItemClickListener) : RecyclerView.Adapter<ArtistChoiceViewHolder>() {
private var artists = listOf<Artist>() private var artists = listOf<Artist>()
override fun getItemCount() = artists.size override fun getItemCount() = artists.size
@ -45,8 +44,7 @@ class ArtistChoiceAdapter(private val listener: ItemClickListener) : RecyclerVie
if (newArtists != artists) { if (newArtists != artists) {
artists = newArtists artists = newArtists
@Suppress("NotifyDataSetChanged") @Suppress("NotifyDataSetChanged") notifyDataSetChanged()
notifyDataSetChanged()
} }
} }
} }
@ -55,13 +53,12 @@ class ArtistChoiceAdapter(private val listener: ItemClickListener) : RecyclerVie
* The ViewHolder that displays a artist choice. Smaller than other parent items due to dialog * The ViewHolder that displays a artist choice. Smaller than other parent items due to dialog
* constraints. * constraints.
*/ */
class ArtistChoiceViewHolder(private val binding: ItemPickerChoiceBinding) : DialogViewHolder(binding.root) { class ArtistChoiceViewHolder(private val binding: ItemPickerChoiceBinding) :
DialogViewHolder(binding.root) {
fun bind(artist: Artist, listener: ItemClickListener) { fun bind(artist: Artist, listener: ItemClickListener) {
binding.pickerImage.bind(artist) binding.pickerImage.bind(artist)
binding.pickerName.text = artist.resolveName(binding.context) binding.pickerName.text = artist.resolveName(binding.context)
binding.root.setOnClickListener { binding.root.setOnClickListener { listener.onItemClick(artist) }
listener.onItemClick(artist)
}
} }
companion object { companion object {

View file

@ -44,7 +44,8 @@ import org.oxycblt.auxio.util.collectImmediately
* *
* TODO: Clean up the picker flow to reduce the amount of duplication I had to do. * TODO: Clean up the picker flow to reduce the amount of duplication I had to do.
*/ */
class ArtistPickerDialog : ViewBindingDialogFragment<DialogMusicPickerBinding>(), ItemClickListener { class ArtistPickerDialog :
ViewBindingDialogFragment<DialogMusicPickerBinding>(), ItemClickListener {
private val pickerModel: PickerViewModel by viewModels() private val pickerModel: PickerViewModel by viewModels()
private val playbackModel: PlaybackViewModel by androidActivityViewModels() private val playbackModel: PlaybackViewModel by androidActivityViewModels()
private val navModel: NavigationViewModel by activityViewModels() private val navModel: NavigationViewModel by activityViewModels()
@ -56,9 +57,7 @@ class ArtistPickerDialog : ViewBindingDialogFragment<DialogMusicPickerBinding>()
DialogMusicPickerBinding.inflate(inflater) DialogMusicPickerBinding.inflate(inflater)
override fun onConfigDialog(builder: AlertDialog.Builder) { override fun onConfigDialog(builder: AlertDialog.Builder) {
builder builder.setTitle(R.string.lbl_artists).setNegativeButton(R.string.lbl_cancel, null)
.setTitle(R.string.lbl_artists)
.setNegativeButton(R.string.lbl_cancel, null)
} }
override fun onBindingCreated(binding: DialogMusicPickerBinding, savedInstanceState: Bundle?) { override fun onBindingCreated(binding: DialogMusicPickerBinding, savedInstanceState: Bundle?) {

View file

@ -17,9 +17,7 @@
package org.oxycblt.auxio.music.picker package org.oxycblt.auxio.music.picker
/** /** Represents the actions available to the picker UI. */
* Represents the actions available to the picker UI.
*/
enum class PickerMode { enum class PickerMode {
PLAY, PLAY,
SHOW SHOW

View file

@ -98,8 +98,7 @@ class MusicDirsDialog :
dirs = dirs =
MusicDirs( MusicDirs(
pendingDirs.mapNotNull { Directory.fromDocumentUri(storageManager, it) }, pendingDirs.mapNotNull { Directory.fromDocumentUri(storageManager, it) },
savedInstanceState.getBoolean(KEY_PENDING_MODE) savedInstanceState.getBoolean(KEY_PENDING_MODE))
)
} }
} }
@ -112,8 +111,7 @@ class MusicDirsDialog :
R.id.dirs_mode_include R.id.dirs_mode_include
} else { } else {
R.id.dirs_mode_exclude R.id.dirs_mode_exclude
} })
)
updateMode() updateMode()
addOnButtonCheckedListener { _, _, _ -> updateMode() } addOnButtonCheckedListener { _, _, _ -> updateMode() }
@ -123,9 +121,7 @@ class MusicDirsDialog :
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
outState.putStringArrayList( outState.putStringArrayList(
KEY_PENDING_DIRS, KEY_PENDING_DIRS, ArrayList(dirAdapter.dirs.map { it.toString() }))
ArrayList(dirAdapter.dirs.map { it.toString() })
)
outState.putBoolean(KEY_PENDING_MODE, isInclude(requireBinding())) outState.putBoolean(KEY_PENDING_MODE, isInclude(requireBinding()))
} }
@ -159,9 +155,7 @@ class MusicDirsDialog :
// Turn the raw URI into a document tree URI // Turn the raw URI into a document tree URI
val docUri = val docUri =
DocumentsContract.buildDocumentUriUsingTree( DocumentsContract.buildDocumentUriUsingTree(
uri, uri, DocumentsContract.getTreeDocumentId(uri))
DocumentsContract.getTreeDocumentId(uri)
)
// Turn it into a semi-usable path // Turn it into a semi-usable path
val treeUri = DocumentsContract.getTreeDocumentId(docUri) val treeUri = DocumentsContract.getTreeDocumentId(docUri)

View file

@ -30,10 +30,10 @@ import android.os.storage.StorageVolume
import android.provider.MediaStore import android.provider.MediaStore
import android.webkit.MimeTypeMap import android.webkit.MimeTypeMap
import com.google.android.exoplayer2.util.MimeTypes import com.google.android.exoplayer2.util.MimeTypes
import org.oxycblt.auxio.R
import org.oxycblt.auxio.util.lazyReflectedMethod
import java.io.File import java.io.File
import java.lang.reflect.Method import java.lang.reflect.Method
import org.oxycblt.auxio.R
import org.oxycblt.auxio.util.lazyReflectedMethod
/** A path to a file. [name] is the stripped file name, [parent] is the parent path. */ /** A path to a file. [name] is the stripped file name, [parent] is the parent path. */
data class Path(val name: String, val parent: Directory) data class Path(val name: String, val parent: Directory)
@ -70,9 +70,7 @@ class Directory private constructor(val volume: StorageVolume, val relativePath:
fun from(volume: StorageVolume, relativePath: String) = fun from(volume: StorageVolume, relativePath: String) =
Directory( Directory(
volume, volume, relativePath.removePrefix(File.separator).removeSuffix(File.separator))
relativePath.removePrefix(File.separator).removeSuffix(File.separator)
)
/** /**
* Converts an opaque document uri in the form of VOLUME:PATH into a [Directory]. This is a * Converts an opaque document uri in the form of VOLUME:PATH into a [Directory]. This is a
@ -198,15 +196,14 @@ val Long.albumCoverUri: Uri
@Suppress("NewApi") @Suppress("NewApi")
private val SM_API21_GET_VOLUME_LIST_METHOD: Method by private val SM_API21_GET_VOLUME_LIST_METHOD: Method by
lazyReflectedMethod(StorageManager::class, "getVolumeList") lazyReflectedMethod(StorageManager::class, "getVolumeList")
@Suppress("NewApi") @Suppress("NewApi")
private val SV_API21_GET_PATH_METHOD: Method by lazyReflectedMethod(StorageVolume::class, "getPath") private val SV_API21_GET_PATH_METHOD: Method by lazyReflectedMethod(StorageVolume::class, "getPath")
/** The "primary" storage volume containing the OS. May be an SD Card. */ /** The "primary" storage volume containing the OS. May be an SD Card. */
val StorageManager.primaryStorageVolumeCompat: StorageVolume val StorageManager.primaryStorageVolumeCompat: StorageVolume
@Suppress("NewApi") @Suppress("NewApi") get() = primaryStorageVolume
get() = primaryStorageVolume
/** /**
* A list of recognized volumes, retrieved in a compatible manner. Note that these volumes may be * A list of recognized volumes, retrieved in a compatible manner. Note that these volumes may be
@ -243,13 +240,11 @@ fun StorageVolume.getDescriptionCompat(context: Context): String = getDescriptio
/** If this volume is the primary volume. May still be removable storage. */ /** If this volume is the primary volume. May still be removable storage. */
val StorageVolume.isPrimaryCompat: Boolean val StorageVolume.isPrimaryCompat: Boolean
@SuppressLint("NewApi") @SuppressLint("NewApi") get() = isPrimary
get() = isPrimary
/** If this volume is emulated. */ /** If this volume is emulated. */
val StorageVolume.isEmulatedCompat: Boolean val StorageVolume.isEmulatedCompat: Boolean
@SuppressLint("NewApi") @SuppressLint("NewApi") get() = isEmulated
get() = isEmulated
/** /**
* If this volume corresponds to "Internal shared storage", represented in document URIs as * If this volume corresponds to "Internal shared storage", represented in document URIs as
@ -260,13 +255,11 @@ val StorageVolume.isInternalCompat: Boolean
/** Returns the UUID of the volume in a compatible manner. */ /** Returns the UUID of the volume in a compatible manner. */
val StorageVolume.uuidCompat: String? val StorageVolume.uuidCompat: String?
@SuppressLint("NewApi") @SuppressLint("NewApi") get() = uuid
get() = uuid
/** Returns the state of the volume in a compatible manner. */ /** Returns the state of the volume in a compatible manner. */
val StorageVolume.stateCompat: String val StorageVolume.stateCompat: String
@SuppressLint("NewApi") @SuppressLint("NewApi") get() = state
get() = state
/** /**
* Returns the name of this volume as it is used in [MediaStore]. This will be * Returns the name of this volume as it is used in [MediaStore]. This will be

View file

@ -133,9 +133,9 @@ class Indexer {
/** /**
* Start the indexing process. This should be done by [Controller] in a background thread. When * Start the indexing process. This should be done by [Controller] in a background thread. When
* complete, a new completion state will be pushed to each callback. * complete, a new completion state will be pushed to each callback.
* @param fresh Whether to use the cache when loading. * @param withCache Whether to use the cache when loading.
*/ */
suspend fun index(context: Context, fresh: Boolean) { suspend fun index(context: Context, withCache: Boolean) {
val notGranted = val notGranted =
ContextCompat.checkSelfPermission(context, PERMISSION_READ_AUDIO) == ContextCompat.checkSelfPermission(context, PERMISSION_READ_AUDIO) ==
PackageManager.PERMISSION_DENIED PackageManager.PERMISSION_DENIED
@ -148,12 +148,11 @@ class Indexer {
val response = val response =
try { try {
val start = System.currentTimeMillis() val start = System.currentTimeMillis()
val library = indexImpl(context, fresh) val library = indexImpl(context, withCache)
if (library != null) { if (library != null) {
logD( logD(
"Music indexing completed successfully in " + "Music indexing completed successfully in " +
"${System.currentTimeMillis() - start}ms" "${System.currentTimeMillis() - start}ms")
)
Response.Ok(library) Response.Ok(library)
} else { } else {
logE("No music found") logE("No music found")
@ -194,9 +193,7 @@ class Indexer {
emitIndexing(null) emitIndexing(null)
} }
/** /** Run the proper music loading process. */
* Run the proper music loading process.
*/
private suspend fun indexImpl(context: Context, withCache: Boolean): MusicStore.Library? { private suspend fun indexImpl(context: Context, withCache: Boolean): MusicStore.Library? {
// Create the chain of extractors. Each extractor builds on the previous and // Create the chain of extractors. Each extractor builds on the previous and
// enables version-specific features in order to create the best possible music // enables version-specific features in order to create the best possible music
@ -250,12 +247,15 @@ class Indexer {
} }
/** /**
* Does the initial query over the song database using [metadataExtractor]. The songs returned by * Does the initial query over the song database using [metadataExtractor]. The songs returned
* this function are **not** well-formed. The companion [buildAlbums], [buildArtists], and * by this function are **not** well-formed. The companion [buildAlbums], [buildArtists], and
* [buildGenres] functions must be called with the returned list so that all songs are properly * [buildGenres] functions must be called with the returned list so that all songs are properly
* linked up. * linked up.
*/ */
private suspend fun buildSongs(metadataExtractor: MetadataExtractor, settings: Settings): List<Song> { private suspend fun buildSongs(
metadataExtractor: MetadataExtractor,
settings: Settings
): List<Song> {
logD("Starting indexing process") logD("Starting indexing process")
val start = System.currentTimeMillis() val start = System.currentTimeMillis()
@ -317,9 +317,9 @@ class Indexer {
} }
/** /**
* Group up songs AND albums into artists. This process seems weird (because it is), but * Group up songs AND albums into artists. This process seems weird (because it is), but the
* the purpose is that the actual artist information of albums and songs often differs, * purpose is that the actual artist information of albums and songs often differs, and so they
* and so they are linked in different ways. * are linked in different ways.
*/ */
private fun buildArtists(songs: List<Song>, albums: List<Album>): List<Artist> { private fun buildArtists(songs: List<Song>, albums: List<Album>): List<Artist> {
val musicByArtist = mutableMapOf<Artist.Raw, MutableList<Music>>() val musicByArtist = mutableMapOf<Artist.Raw, MutableList<Music>>()

View file

@ -67,8 +67,7 @@ class IndexingNotification(private val context: Context) :
// Only update the notification every 1.5s to prevent rate-limiting. // Only update the notification every 1.5s to prevent rate-limiting.
logD("Updating state to $indexing") logD("Updating state to $indexing")
setContentText( setContentText(
context.getString(R.string.fmt_indexing, indexing.current, indexing.total) context.getString(R.string.fmt_indexing, indexing.current, indexing.total))
)
setProgress(indexing.total, indexing.current, false) setProgress(indexing.total, indexing.current, false)
return true return true
} }
@ -95,6 +94,4 @@ class ObservingNotification(context: Context) : ServiceNotification(context, IND
private val INDEXER_CHANNEL = private val INDEXER_CHANNEL =
ServiceNotification.ChannelInfo( ServiceNotification.ChannelInfo(
id = BuildConfig.APPLICATION_ID + ".channel.INDEXER", id = BuildConfig.APPLICATION_ID + ".channel.INDEXER", nameRes = R.string.lbl_indexer)
nameRes = R.string.lbl_indexer
)

View file

@ -79,9 +79,7 @@ class IndexerService : Service(), Indexer.Controller, Settings.Callback {
wakeLock = wakeLock =
getSystemServiceCompat(PowerManager::class) getSystemServiceCompat(PowerManager::class)
.newWakeLock( .newWakeLock(
PowerManager.PARTIAL_WAKE_LOCK, PowerManager.PARTIAL_WAKE_LOCK, BuildConfig.APPLICATION_ID + ":IndexerService")
BuildConfig.APPLICATION_ID + ":IndexerService"
)
settings = Settings(this, this) settings = Settings(this, this)
indexerContentObserver = SystemContentObserver() indexerContentObserver = SystemContentObserver()
@ -130,8 +128,7 @@ class IndexerService : Service(), Indexer.Controller, Settings.Callback {
when (state) { when (state) {
is Indexer.State.Complete -> { is Indexer.State.Complete -> {
if (state.response is Indexer.Response.Ok && if (state.response is Indexer.Response.Ok &&
state.response.library != musicStore.library state.response.library != musicStore.library) {
) {
logD("Applying new library") logD("Applying new library")
val newLibrary = state.response.library val newLibrary = state.response.library
@ -243,10 +240,7 @@ class IndexerService : Service(), Indexer.Controller, Settings.Callback {
) : ContentObserver(handler), Runnable { ) : ContentObserver(handler), Runnable {
init { init {
contentResolverSafe.registerContentObserver( contentResolverSafe.registerContentObserver(
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, true, this)
true,
this
)
} }
fun release() { fun release() {
@ -263,8 +257,7 @@ class IndexerService : Service(), Indexer.Controller, Settings.Callback {
// Check here if we should even start a reindex. This is much less bug-prone than // Check here if we should even start a reindex. This is much less bug-prone than
// registering and de-registering this component as this setting changes. // registering and de-registering this component as this setting changes.
if (settings.shouldBeObserving) { if (settings.shouldBeObserving) {
onSt onStartIndexing(true)
artIndexing(true)
} }
} }
} }

View file

@ -26,11 +26,12 @@ enum class ActionMode {
SHUFFLE; SHUFFLE;
val intCode: Int val intCode: Int
get() = when (this) { get() =
NEXT -> IntegerTable.ACTION_MODE_NEXT when (this) {
REPEAT -> IntegerTable.ACTION_MODE_REPEAT NEXT -> IntegerTable.ACTION_MODE_NEXT
SHUFFLE -> IntegerTable.ACTION_MODE_SHUFFLE REPEAT -> IntegerTable.ACTION_MODE_REPEAT
} SHUFFLE -> IntegerTable.ACTION_MODE_SHUFFLE
}
companion object { companion object {
/** Convert an int [code] into an instance, or null if it isn't valid. */ /** Convert an int [code] into an instance, or null if it isn't valid. */

View file

@ -89,16 +89,12 @@ class PlaybackPanelFragment :
binding.playbackArtist.apply { binding.playbackArtist.apply {
isSelected = true isSelected = true
setOnClickListener { setOnClickListener { playbackModel.song.value?.let { showCurrentArtist() } }
playbackModel.song.value?.let { showCurrentArtist() }
}
} }
binding.playbackAlbum.apply { binding.playbackAlbum.apply {
isSelected = true isSelected = true
setOnClickListener { setOnClickListener { playbackModel.song.value?.let { showCurrentAlbum() } }
playbackModel.song.value?.let { showCurrentAlbum() }
}
} }
binding.playbackSeekBar.callback = this binding.playbackSeekBar.callback = this
@ -136,9 +132,7 @@ class PlaybackPanelFragment :
val equalizerIntent = val equalizerIntent =
Intent(AudioEffect.ACTION_DISPLAY_AUDIO_EFFECT_CONTROL_PANEL) Intent(AudioEffect.ACTION_DISPLAY_AUDIO_EFFECT_CONTROL_PANEL)
.putExtra( .putExtra(
AudioEffect.EXTRA_AUDIO_SESSION, AudioEffect.EXTRA_AUDIO_SESSION, playbackModel.currentAudioSessionId)
playbackModel.currentAudioSessionId
)
.putExtra(AudioEffect.EXTRA_CONTENT_TYPE, AudioEffect.CONTENT_TYPE_MUSIC) .putExtra(AudioEffect.EXTRA_CONTENT_TYPE, AudioEffect.CONTENT_TYPE_MUSIC)
try { try {
@ -161,9 +155,7 @@ class PlaybackPanelFragment :
playbackModel.song.value?.let { song -> playbackModel.song.value?.let { song ->
navModel.mainNavigateTo( navModel.mainNavigateTo(
MainNavigationAction.Directions( MainNavigationAction.Directions(
MainFragmentDirections.actionShowDetails(song.uid) MainFragmentDirections.actionShowDetails(song.uid)))
)
)
} }
true true

View file

@ -60,7 +60,5 @@ class PlaybackSheetBehavior<V : View>(context: Context, attributeSet: AttributeS
MaterialShapeDrawable(sheetBackgroundDrawable.shapeAppearanceModel).apply { MaterialShapeDrawable(sheetBackgroundDrawable.shapeAppearanceModel).apply {
fillColor = sheetBackgroundDrawable.fillColor fillColor = sheetBackgroundDrawable.fillColor
}, },
sheetBackgroundDrawable sheetBackgroundDrawable))
)
)
} }

View file

@ -115,44 +115,32 @@ class PlaybackViewModel(application: Application) :
playbackManager.play(song, genre, settings) playbackManager.play(song, genre, settings)
} }
/** /** Play an [album]. */
* Play an [album].
*/
fun play(album: Album) { fun play(album: Album) {
playbackManager.play(null, album, settings, false) playbackManager.play(null, album, settings, false)
} }
/** /** Play an [artist]. */
* Play an [artist].
*/
fun play(artist: Artist) { fun play(artist: Artist) {
playbackManager.play(null, artist, settings, false) playbackManager.play(null, artist, settings, false)
} }
/** /** Play a [genre]. */
* Play a [genre].
*/
fun play(genre: Genre) { fun play(genre: Genre) {
playbackManager.play(null, genre, settings, false) playbackManager.play(null, genre, settings, false)
} }
/** /** Shuffle an [album]. */
* Shuffle an [album].
*/
fun shuffle(album: Album) { fun shuffle(album: Album) {
playbackManager.play(null, album, settings, true) playbackManager.play(null, album, settings, true)
} }
/** /** Shuffle an [artist]. */
* Shuffle an [artist].
*/
fun shuffle(artist: Artist) { fun shuffle(artist: Artist) {
playbackManager.play(null, artist, settings, true) playbackManager.play(null, artist, settings, true)
} }
/** /** Shuffle a [genre]. */
* Shuffle a [genre].
*/
fun shuffle(genre: Genre) { fun shuffle(genre: Genre) {
playbackManager.play(null, genre, settings, true) playbackManager.play(null, genre, settings, true)
} }
@ -249,8 +237,8 @@ class PlaybackViewModel(application: Application) :
// --- SAVE/RESTORE FUNCTIONS --- // --- SAVE/RESTORE FUNCTIONS ---
/** /**
* Force save the current [PlaybackStateManager] state to the database. [onDone] * Force save the current [PlaybackStateManager] state to the database. [onDone] will be called
* will be called with true if it was done, or false if an error occurred. * with true if it was done, or false if an error occurred.
*/ */
fun savePlaybackState(onDone: (Boolean) -> Unit) { fun savePlaybackState(onDone: (Boolean) -> Unit) {
viewModelScope.launch { viewModelScope.launch {

View file

@ -131,9 +131,7 @@ class QueueSongViewHolder private constructor(private val binding: ItemQueueSong
fillColor = binding.context.getAttrColorCompat(R.attr.colorSurface) fillColor = binding.context.getAttrColorCompat(R.attr.colorSurface)
elevation = binding.context.getDimen(R.dimen.elevation_normal) elevation = binding.context.getDimen(R.dimen.elevation_normal)
}, },
backgroundDrawable backgroundDrawable))
)
)
} }
@SuppressLint("ClickableViewAccessibility") @SuppressLint("ClickableViewAccessibility")

View file

@ -42,9 +42,7 @@ class QueueDragCallback(private val playbackModel: QueueViewModel) : ItemTouchHe
val queueHolder = viewHolder as QueueSongViewHolder val queueHolder = viewHolder as QueueSongViewHolder
return if (queueHolder.isEnabled) { return if (queueHolder.isEnabled) {
makeFlag( makeFlag(
ItemTouchHelper.ACTION_STATE_DRAG, ItemTouchHelper.ACTION_STATE_DRAG, ItemTouchHelper.UP or ItemTouchHelper.DOWN) or
ItemTouchHelper.UP or ItemTouchHelper.DOWN
) or
makeFlag(ItemTouchHelper.ACTION_STATE_SWIPE, ItemTouchHelper.START) makeFlag(ItemTouchHelper.ACTION_STATE_SWIPE, ItemTouchHelper.START)
} else { } else {
0 0
@ -134,9 +132,7 @@ class QueueDragCallback(private val playbackModel: QueueViewModel) : ItemTouchHe
target: RecyclerView.ViewHolder target: RecyclerView.ViewHolder
) = ) =
playbackModel.moveQueueDataItems( playbackModel.moveQueueDataItems(
viewHolder.bindingAdapterPosition, viewHolder.bindingAdapterPosition, target.bindingAdapterPosition)
target.bindingAdapterPosition
)
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
playbackModel.removeQueueDataItem(viewHolder.bindingAdapterPosition) playbackModel.removeQueueDataItem(viewHolder.bindingAdapterPosition)

View file

@ -61,18 +61,13 @@ class QueueFragment : ViewBindingFragment<FragmentQueueBinding>(), QueueItemList
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
invalidateDivider() invalidateDivider()
} }
} })
)
} }
// --- VIEWMODEL SETUP ---- // --- VIEWMODEL SETUP ----
collectImmediately( collectImmediately(
queueModel.queue, queueModel.queue, queueModel.index, playbackModel.isPlaying, ::updateQueue)
queueModel.index,
playbackModel.isPlaying,
::updateQueue
)
} }
override fun onDestroyBinding(binding: FragmentQueueBinding) { override fun onDestroyBinding(binding: FragmentQueueBinding) {
@ -102,7 +97,7 @@ class QueueFragment : ViewBindingFragment<FragmentQueueBinding>(), QueueItemList
binding.queueDivider.isInvisible = binding.queueDivider.isInvisible =
(binding.queueRecycler.layoutManager as LinearLayoutManager) (binding.queueRecycler.layoutManager as LinearLayoutManager)
.findFirstCompletelyVisibleItemPosition() < 1 .findFirstCompletelyVisibleItemPosition() < 1
queueModel.finishReplace() queueModel.finishReplace()
@ -127,6 +122,6 @@ class QueueFragment : ViewBindingFragment<FragmentQueueBinding>(), QueueItemList
val binding = requireBinding() val binding = requireBinding()
binding.queueDivider.isInvisible = binding.queueDivider.isInvisible =
(binding.queueRecycler.layoutManager as LinearLayoutManager) (binding.queueRecycler.layoutManager as LinearLayoutManager)
.findFirstCompletelyVisibleItemPosition() < 1 .findFirstCompletelyVisibleItemPosition() < 1
} }
} }

View file

@ -70,10 +70,6 @@ class QueueSheetBehavior<V : View>(context: Context, attributeSet: AttributeSet?
val bars = insets.systemBarInsetsCompat val bars = insets.systemBarInsetsCompat
expandedOffset = bars.top + barHeight + barSpacing expandedOffset = bars.top + barHeight + barSpacing
return insets.replaceSystemBarInsetsCompat( return insets.replaceSystemBarInsetsCompat(
bars.left, bars.left, bars.top, bars.right, expandedOffset + bars.bottom)
bars.top,
bars.right,
expandedOffset + bars.bottom
)
} }
} }

View file

@ -59,8 +59,7 @@ class QueueViewModel : ViewModel(), PlaybackStateManager.Callback {
/** Remove a queue item using it's recyclerview adapter index. */ /** Remove a queue item using it's recyclerview adapter index. */
fun removeQueueDataItem(adapterIndex: Int) { fun removeQueueDataItem(adapterIndex: Int) {
if (adapterIndex <= playbackManager.index || if (adapterIndex <= playbackManager.index ||
adapterIndex !in playbackManager.queue.indices adapterIndex !in playbackManager.queue.indices) {
) {
return return
} }

View file

@ -21,12 +21,12 @@ import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.widget.TextView import android.widget.TextView
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import kotlin.math.abs
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.DialogPreAmpBinding import org.oxycblt.auxio.databinding.DialogPreAmpBinding
import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.ui.fragment.ViewBindingDialogFragment import org.oxycblt.auxio.ui.fragment.ViewBindingDialogFragment
import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.context
import kotlin.math.abs
/** /**
* The dialog for customizing the ReplayGain pre-amp values. * The dialog for customizing the ReplayGain pre-amp values.
@ -82,7 +82,4 @@ class PreAmpCustomizeDialog : ViewBindingDialogFragment<DialogPreAmpBinding>() {
getString(R.string.fmt_db_neg, abs(valueDb)) getString(R.string.fmt_db_neg, abs(valueDb))
} }
} }
companion object {
}
} }

View file

@ -24,13 +24,13 @@ import com.google.android.exoplayer2.audio.BaseAudioProcessor
import com.google.android.exoplayer2.metadata.Metadata import com.google.android.exoplayer2.metadata.Metadata
import com.google.android.exoplayer2.metadata.id3.TextInformationFrame import com.google.android.exoplayer2.metadata.id3.TextInformationFrame
import com.google.android.exoplayer2.metadata.vorbis.VorbisComment import com.google.android.exoplayer2.metadata.vorbis.VorbisComment
import java.nio.ByteBuffer
import kotlin.math.pow
import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.playback.state.PlaybackStateManager
import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.unlikelyToBeNull import org.oxycblt.auxio.util.unlikelyToBeNull
import java.nio.ByteBuffer
import kotlin.math.pow
/** /**
* An [AudioProcessor] that handles ReplayGain values and their amplification of the audio stream. * An [AudioProcessor] that handles ReplayGain values and their amplification of the audio stream.

View file

@ -92,8 +92,7 @@ interface InternalPlayer {
// Not advancing, so don't move the position. // Not advancing, so don't move the position.
0f 0f
}, },
creationTime creationTime)
)
// Equality ignores the creation time to prevent functionally // Equality ignores the creation time to prevent functionally
// identical states from being equal. // identical states from being equal.
@ -120,8 +119,7 @@ interface InternalPlayer {
// main playing value is paused. // main playing value is paused.
isPlaying && isAdvancing, isPlaying && isAdvancing,
positionMs, positionMs,
SystemClock.elapsedRealtime() SystemClock.elapsedRealtime())
)
} }
} }

View file

@ -110,8 +110,7 @@ class PlaybackStateDatabase private constructor(context: Context) :
queue = queue, queue = queue,
positionMs = rawState.positionMs, positionMs = rawState.positionMs,
repeatMode = rawState.repeatMode, repeatMode = rawState.repeatMode,
isShuffled = rawState.isShuffled isShuffled = rawState.isShuffled)
)
} }
private fun readRawState(): RawState? { private fun readRawState(): RawState? {
@ -134,12 +133,11 @@ class PlaybackStateDatabase private constructor(context: Context) :
index = cursor.getInt(indexIndex), index = cursor.getInt(indexIndex),
positionMs = cursor.getLong(posIndex), positionMs = cursor.getLong(posIndex),
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,
songUid = Music.UID.fromString(cursor.getString(songUidIndex)) songUid = Music.UID.fromString(cursor.getString(songUidIndex))
?: return@queryAll null, ?: return@queryAll null,
parentUid = cursor.getString(parentUidIndex)?.let(Music.UID::fromString) parentUid = cursor.getString(parentUidIndex)?.let(Music.UID::fromString))
)
} }
} }
@ -175,8 +173,7 @@ class PlaybackStateDatabase private constructor(context: Context) :
repeatMode = state.repeatMode, repeatMode = state.repeatMode,
isShuffled = state.isShuffled, isShuffled = state.isShuffled,
songUid = state.queue[state.index].uid, songUid = state.queue[state.index].uid,
parentUid = state.parent?.uid parentUid = state.parent?.uid)
)
writeRawState(rawState) writeRawState(rawState)
writeQueue(state.queue) writeQueue(state.queue)

View file

@ -17,6 +17,7 @@
package org.oxycblt.auxio.playback.state package org.oxycblt.auxio.playback.state
import kotlin.math.max
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.BuildConfig
@ -31,7 +32,6 @@ import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logE import org.oxycblt.auxio.util.logE
import org.oxycblt.auxio.util.logW import org.oxycblt.auxio.util.logW
import kotlin.math.max
/** /**
* Master class (and possible god object) for the playback state. * Master class (and possible god object) for the playback state.
@ -268,11 +268,7 @@ class PlaybackStateManager private constructor() {
notifyShuffledChanged() notifyShuffledChanged()
} }
private fun orderQueue( private fun orderQueue(settings: Settings, shuffled: Boolean, keep: Song?) {
settings: Settings,
shuffled: Boolean,
keep: Song?
) {
val newIndex: Int val newIndex: Int
if (shuffled) { if (shuffled) {
@ -369,13 +365,14 @@ class PlaybackStateManager private constructor() {
val library = musicStore.library ?: return false val library = musicStore.library ?: return false
val internalPlayer = internalPlayer ?: return false val internalPlayer = internalPlayer ?: return false
val state = try { val state =
withContext(Dispatchers.IO) { database.read(library) } try {
} catch (e: Exception) { withContext(Dispatchers.IO) { database.read(library) }
logE("Unable to restore playback state.") } catch (e: Exception) {
logE(e.stackTraceToString()) logE("Unable to restore playback state.")
return false logE(e.stackTraceToString())
} return false
}
synchronized(this) { synchronized(this) {
if (state != null && (!isInitialized || force)) { if (state != null && (!isInitialized || force)) {
@ -484,8 +481,7 @@ class PlaybackStateManager private constructor() {
queue = _queue, queue = _queue,
positionMs = playerState.calculateElapsedPosition(), positionMs = playerState.calculateElapsedPosition(),
isShuffled = isShuffled, isShuffled = isShuffled,
repeatMode = repeatMode repeatMode = repeatMode)
)
// --- CALLBACKS --- // --- CALLBACKS ---

View file

@ -1,3 +1,20 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.playback.system package org.oxycblt.auxio.playback.system
import android.bluetooth.BluetoothProfile import android.bluetooth.BluetoothProfile
@ -6,18 +23,20 @@ import android.content.Context
import android.content.Intent import android.content.Intent
/** /**
* A [BroadcastReceiver] that handles connections from bluetooth headsets, starting playback if * A [BroadcastReceiver] that handles connections from bluetooth headsets, starting playback if they
* they occur. * occur.
* @author seijikun, OxygenCobalt * @author seijikun, OxygenCobalt
*/ */
class BluetoothHeadsetReceiver : BroadcastReceiver() { class BluetoothHeadsetReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) { override fun onReceive(context: Context, intent: Intent) {
if (intent.action == android.bluetooth.BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED) { if (intent.action == android.bluetooth.BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED) {
val newState = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, BluetoothProfile.STATE_DISCONNECTED) val newState =
intent.getIntExtra(
BluetoothProfile.EXTRA_STATE, BluetoothProfile.STATE_DISCONNECTED)
if (newState == BluetoothProfile.STATE_CONNECTED) { if (newState == BluetoothProfile.STATE_CONNECTED) {
// TODO: Initialize the service (Permission workflow must be figured out) // TODO: Initialize the service (Permission workflow must be figured out)
// Perhaps move this to the internal receivers? // Perhaps move this to the internal receivers?
} }
} }
} }
} }

View file

@ -140,22 +140,19 @@ class MediaSessionComponent(private val context: Context, private val callback:
.putText(MediaMetadataCompat.METADATA_KEY_ARTIST, artist) .putText(MediaMetadataCompat.METADATA_KEY_ARTIST, artist)
.putText( .putText(
MediaMetadataCompat.METADATA_KEY_ALBUM_ARTIST, MediaMetadataCompat.METADATA_KEY_ALBUM_ARTIST,
song.album.resolveArtistContents(context) song.album.resolveArtistContents(context))
)
.putText(MediaMetadataCompat.METADATA_KEY_AUTHOR, artist) .putText(MediaMetadataCompat.METADATA_KEY_AUTHOR, artist)
.putText(MediaMetadataCompat.METADATA_KEY_COMPOSER, artist) .putText(MediaMetadataCompat.METADATA_KEY_COMPOSER, artist)
.putText(MediaMetadataCompat.METADATA_KEY_WRITER, artist) .putText(MediaMetadataCompat.METADATA_KEY_WRITER, artist)
.putText( .putText(
METADATA_KEY_PARENT, METADATA_KEY_PARENT,
parent?.resolveName(context) ?: context.getString(R.string.lbl_all_songs) parent?.resolveName(context) ?: context.getString(R.string.lbl_all_songs))
)
.putText(MediaMetadataCompat.METADATA_KEY_GENRE, song.resolveGenreContents(context)) .putText(MediaMetadataCompat.METADATA_KEY_GENRE, song.resolveGenreContents(context))
.putText(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, title) .putText(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, title)
.putText(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, artist) .putText(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, artist)
.putText( .putText(
MediaMetadataCompat.METADATA_KEY_DISPLAY_DESCRIPTION, MediaMetadataCompat.METADATA_KEY_DISPLAY_DESCRIPTION,
parent?.resolveName(context) ?: context.getString(R.string.lbl_all_songs) parent?.resolveName(context) ?: context.getString(R.string.lbl_all_songs))
)
.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, song.durationMs) .putLong(MediaMetadataCompat.METADATA_KEY_DURATION, song.durationMs)
song.track?.let { song.track?.let {
@ -166,9 +163,7 @@ class MediaSessionComponent(private val context: Context, private val callback:
builder.putLong(MediaMetadataCompat.METADATA_KEY_DISC_NUMBER, it.toLong()) builder.putLong(MediaMetadataCompat.METADATA_KEY_DISC_NUMBER, it.toLong())
} }
song.date?.let { song.date?.let { builder.putString(MediaMetadataCompat.METADATA_KEY_DATE, it.toString()) }
builder.putString(MediaMetadataCompat.METADATA_KEY_DATE, it.toString())
}
// Cover loading is a mess. Android expects you to provide a clean, easy URI for it to // Cover loading is a mess. Android expects you to provide a clean, easy URI for it to
// leverage, but Auxio cannot do that as quality-of-life features like scaling or // leverage, but Auxio cannot do that as quality-of-life features like scaling or
@ -192,8 +187,7 @@ class MediaSessionComponent(private val context: Context, private val callback:
notification.updateMetadata(metadata) notification.updateMetadata(metadata)
callback.onPostNotification(notification, PostingReason.METADATA) callback.onPostNotification(notification, PostingReason.METADATA)
} }
} })
)
} }
private fun updateQueue(queue: List<Song>) { private fun updateQueue(queue: List<Song>) {
@ -230,8 +224,7 @@ class MediaSessionComponent(private val context: Context, private val callback:
RepeatMode.NONE -> PlaybackStateCompat.REPEAT_MODE_NONE RepeatMode.NONE -> PlaybackStateCompat.REPEAT_MODE_NONE
RepeatMode.TRACK -> PlaybackStateCompat.REPEAT_MODE_ONE RepeatMode.TRACK -> PlaybackStateCompat.REPEAT_MODE_ONE
RepeatMode.ALL -> PlaybackStateCompat.REPEAT_MODE_ALL RepeatMode.ALL -> PlaybackStateCompat.REPEAT_MODE_ALL
} })
)
invalidateSecondaryAction() invalidateSecondaryAction()
} }
@ -242,8 +235,7 @@ class MediaSessionComponent(private val context: Context, private val callback:
PlaybackStateCompat.SHUFFLE_MODE_ALL PlaybackStateCompat.SHUFFLE_MODE_ALL
} else { } else {
PlaybackStateCompat.SHUFFLE_MODE_NONE PlaybackStateCompat.SHUFFLE_MODE_NONE
} })
)
invalidateSecondaryAction() invalidateSecondaryAction()
} }
@ -328,8 +320,7 @@ class MediaSessionComponent(private val context: Context, private val callback:
playbackManager.reshuffle( playbackManager.reshuffle(
shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_ALL || shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_ALL ||
shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_GROUP, shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_GROUP,
settings settings)
)
} }
override fun onSkipToQueueItem(id: Long) { override fun onSkipToQueueItem(id: Long) {
@ -368,30 +359,29 @@ class MediaSessionComponent(private val context: Context, private val callback:
// Android 13+ leverages custom actions in the notification. // Android 13+ leverages custom actions in the notification.
val extraAction = when (settings.notifAction) { val extraAction =
ActionMode.SHUFFLE -> PlaybackStateCompat.CustomAction.Builder( when (settings.notifAction) {
PlaybackService.ACTION_INVERT_SHUFFLE, ActionMode.SHUFFLE ->
context.getString(R.string.desc_shuffle), PlaybackStateCompat.CustomAction.Builder(
if (playbackManager.isShuffled) { PlaybackService.ACTION_INVERT_SHUFFLE,
R.drawable.ic_shuffle_on_24 context.getString(R.string.desc_shuffle),
} else { if (playbackManager.isShuffled) {
R.drawable.ic_shuffle_off_24 R.drawable.ic_shuffle_on_24
} } else {
) R.drawable.ic_shuffle_off_24
})
else -> PlaybackStateCompat.CustomAction.Builder( else ->
PlaybackService.ACTION_INC_REPEAT_MODE, PlaybackStateCompat.CustomAction.Builder(
context.getString(R.string.desc_change_repeat), PlaybackService.ACTION_INC_REPEAT_MODE,
playbackManager.repeatMode.icon context.getString(R.string.desc_change_repeat),
) playbackManager.repeatMode.icon)
} }
val exitAction = val exitAction =
PlaybackStateCompat.CustomAction.Builder( PlaybackStateCompat.CustomAction.Builder(
PlaybackService.ACTION_EXIT, PlaybackService.ACTION_EXIT,
context.getString(R.string.desc_exit), context.getString(R.string.desc_exit),
R.drawable.ic_close_24 R.drawable.ic_close_24)
)
.build() .build()
state.addCustomAction(extraAction.build()) state.addCustomAction(extraAction.build())

View file

@ -52,12 +52,10 @@ class NotificationComponent(private val context: Context, sessionToken: MediaSes
addAction(buildRepeatAction(context, RepeatMode.NONE)) addAction(buildRepeatAction(context, RepeatMode.NONE))
addAction( addAction(
buildAction(context, PlaybackService.ACTION_SKIP_PREV, R.drawable.ic_skip_prev_24) buildAction(context, PlaybackService.ACTION_SKIP_PREV, R.drawable.ic_skip_prev_24))
)
addAction(buildPlayPauseAction(context, true)) addAction(buildPlayPauseAction(context, true))
addAction( addAction(
buildAction(context, PlaybackService.ACTION_SKIP_NEXT, R.drawable.ic_skip_next_24) buildAction(context, PlaybackService.ACTION_SKIP_NEXT, R.drawable.ic_skip_next_24))
)
addAction(buildAction(context, PlaybackService.ACTION_EXIT, R.drawable.ic_close_24)) addAction(buildAction(context, PlaybackService.ACTION_EXIT, R.drawable.ic_close_24))
setStyle(MediaStyle().setMediaSession(sessionToken).setShowActionsInCompactView(1, 2, 3)) setStyle(MediaStyle().setMediaSession(sessionToken).setShowActionsInCompactView(1, 2, 3))
@ -134,10 +132,7 @@ class NotificationComponent(private val context: Context, sessionToken: MediaSes
): NotificationCompat.Action { ): NotificationCompat.Action {
val action = val action =
NotificationCompat.Action.Builder( NotificationCompat.Action.Builder(
iconRes, iconRes, actionName, context.newBroadcastPendingIntent(actionName))
actionName,
context.newBroadcastPendingIntent(actionName)
)
return action.build() return action.build()
} }
@ -146,7 +141,6 @@ class NotificationComponent(private val context: Context, sessionToken: MediaSes
val CHANNEL_INFO = val CHANNEL_INFO =
ChannelInfo( ChannelInfo(
id = BuildConfig.APPLICATION_ID + ".channel.PLAYBACK", id = BuildConfig.APPLICATION_ID + ".channel.PLAYBACK",
nameRes = R.string.lbl_playback nameRes = R.string.lbl_playback)
)
} }
} }

View file

@ -39,6 +39,8 @@ import com.google.android.exoplayer2.ext.flac.LibflacAudioRenderer
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory
import com.google.android.exoplayer2.mediacodec.MediaCodecSelector import com.google.android.exoplayer2.mediacodec.MediaCodecSelector
import com.google.android.exoplayer2.source.DefaultMediaSourceFactory import com.google.android.exoplayer2.source.DefaultMediaSourceFactory
import kotlin.math.max
import kotlin.math.min
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@ -57,8 +59,6 @@ import org.oxycblt.auxio.ui.system.ForegroundManager
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.widgets.WidgetComponent import org.oxycblt.auxio.widgets.WidgetComponent
import org.oxycblt.auxio.widgets.WidgetProvider import org.oxycblt.auxio.widgets.WidgetProvider
import kotlin.math.max
import kotlin.math.min
/** /**
* A service that manages the system-side aspects of playback, such as: * A service that manages the system-side aspects of playback, such as:
@ -123,10 +123,8 @@ class PlaybackService :
handler, handler,
audioListener, audioListener,
AudioCapabilities.DEFAULT_AUDIO_CAPABILITIES, AudioCapabilities.DEFAULT_AUDIO_CAPABILITIES,
replayGainProcessor replayGainProcessor),
), LibflacAudioRenderer(handler, audioListener, replayGainProcessor))
LibflacAudioRenderer(handler, audioListener, replayGainProcessor)
)
} }
// Enable constant bitrate seeking so that certain MP3s/AACs are seekable // Enable constant bitrate seeking so that certain MP3s/AACs are seekable
@ -141,8 +139,7 @@ class PlaybackService :
.setUsage(C.USAGE_MEDIA) .setUsage(C.USAGE_MEDIA)
.setContentType(C.AUDIO_CONTENT_TYPE_MUSIC) .setContentType(C.AUDIO_CONTENT_TYPE_MUSIC)
.build(), .build(),
true true)
)
.build() .build()
player.addListener(this) player.addListener(this)
@ -227,10 +224,7 @@ class PlaybackService :
override fun makeState(durationMs: Long) = override fun makeState(durationMs: Long) =
InternalPlayer.State.new( InternalPlayer.State.new(
player.playWhenReady, player.playWhenReady, player.isPlaying, max(min(player.currentPosition, durationMs), 0))
player.isPlaying,
max(min(player.currentPosition, durationMs), 0)
)
override fun loadSong(song: Song?, play: Boolean) { override fun loadSong(song: Song?, play: Boolean) {
if (song == null) { if (song == null) {
@ -340,8 +334,7 @@ class PlaybackService :
override fun onSettingChanged(key: String) { override fun onSettingChanged(key: String) {
if (key == getString(R.string.set_key_replay_gain) || if (key == getString(R.string.set_key_replay_gain) ||
key == getString(R.string.set_key_pre_amp_with) || key == getString(R.string.set_key_pre_amp_with) ||
key == getString(R.string.set_key_pre_amp_without) key == getString(R.string.set_key_pre_amp_without)) {
) {
onTracksChanged(player.currentTracks) onTracksChanged(player.currentTracks)
} }
} }
@ -353,8 +346,7 @@ class PlaybackService :
Intent(event) Intent(event)
.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, packageName) .putExtra(AudioEffect.EXTRA_PACKAGE_NAME, packageName)
.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, audioSessionId) .putExtra(AudioEffect.EXTRA_AUDIO_SESSION, audioSessionId)
.putExtra(AudioEffect.EXTRA_CONTENT_TYPE, AudioEffect.CONTENT_TYPE_MUSIC) .putExtra(AudioEffect.EXTRA_CONTENT_TYPE, AudioEffect.CONTENT_TYPE_MUSIC))
)
} }
/** Stop the foreground state and hide the notification */ /** Stop the foreground state and hide the notification */
@ -378,9 +370,7 @@ class PlaybackService :
is InternalPlayer.Action.RestoreState -> { is InternalPlayer.Action.RestoreState -> {
restoreScope.launch { restoreScope.launch {
playbackManager.restoreState( playbackManager.restoreState(
PlaybackStateDatabase.getInstance(this@PlaybackService), PlaybackStateDatabase.getInstance(this@PlaybackService), false)
false
)
} }
} }
is InternalPlayer.Action.ShuffleAll -> { is InternalPlayer.Action.ShuffleAll -> {
@ -475,8 +465,7 @@ class PlaybackService :
private fun maybeResumeFromPlug() { private fun maybeResumeFromPlug() {
if (playbackManager.song != null && if (playbackManager.song != null &&
settings.headsetAutoplay && settings.headsetAutoplay &&
initialHeadsetPlugEventHandled initialHeadsetPlugEventHandled) {
) {
logD("Device connected, resuming") logD("Device connected, resuming")
playbackManager.changePlaying(true) playbackManager.changePlaying(true)
} }

View file

@ -20,11 +20,11 @@ package org.oxycblt.auxio.playback.ui
import android.content.Context import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import com.google.android.material.slider.Slider import com.google.android.material.slider.Slider
import kotlin.math.max
import org.oxycblt.auxio.databinding.ViewSeekBarBinding import org.oxycblt.auxio.databinding.ViewSeekBarBinding
import org.oxycblt.auxio.playback.formatDurationDs import org.oxycblt.auxio.playback.formatDurationDs
import org.oxycblt.auxio.util.inflater import org.oxycblt.auxio.util.inflater
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import kotlin.math.max
/** /**
* A wrapper around [Slider] that shows not only position and duration values, but also basically * A wrapper around [Slider] that shows not only position and duration values, but also basically

View file

@ -80,13 +80,14 @@ class SearchFragment :
override fun onBindingCreated(binding: FragmentSearchBinding, savedInstanceState: Bundle?) { override fun onBindingCreated(binding: FragmentSearchBinding, savedInstanceState: Bundle?) {
binding.searchToolbar.apply { binding.searchToolbar.apply {
val itemIdToSelect = when (searchModel.filterMode) { val itemIdToSelect =
MusicMode.SONGS -> R.id.option_filter_songs when (searchModel.filterMode) {
MusicMode.ALBUMS -> R.id.option_filter_albums MusicMode.SONGS -> R.id.option_filter_songs
MusicMode.ARTISTS -> R.id.option_filter_artists MusicMode.ALBUMS -> R.id.option_filter_albums
MusicMode.GENRES -> R.id.option_filter_genres MusicMode.ARTISTS -> R.id.option_filter_artists
null -> R.id.option_filter_all MusicMode.GENRES -> R.id.option_filter_genres
} null -> R.id.option_filter_all
}
menu.findItem(itemIdToSelect).isChecked = true menu.findItem(itemIdToSelect).isChecked = true
@ -119,11 +120,7 @@ class SearchFragment :
collectImmediately(searchModel.searchResults, ::handleResults) collectImmediately(searchModel.searchResults, ::handleResults)
collectImmediately( collectImmediately(
playbackModel.song, playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::handlePlayback)
playbackModel.parent,
playbackModel.isPlaying,
::handlePlayback
)
collect(navModel.exploreNavigationItem, ::handleNavigation) collect(navModel.exploreNavigationItem, ::handleNavigation)
} }
@ -148,12 +145,13 @@ class SearchFragment :
override fun onItemClick(item: Item) { override fun onItemClick(item: Item) {
when (item) { when (item) {
is Song -> when (settings.libPlaybackMode) { is Song ->
MusicMode.SONGS -> playbackModel.playFromAll(item) when (settings.libPlaybackMode) {
MusicMode.ALBUMS -> playbackModel.playFromAlbum(item) MusicMode.SONGS -> playbackModel.playFromAll(item)
MusicMode.ARTISTS -> doArtistDependentAction(item, PickerMode.PLAY) MusicMode.ALBUMS -> playbackModel.playFromAlbum(item)
else -> error("Unexpected playback mode: ${settings.libPlaybackMode}") MusicMode.ARTISTS -> doArtistDependentAction(item, PickerMode.PLAY)
} else -> error("Unexpected playback mode: ${settings.libPlaybackMode}")
}
is MusicParent -> navModel.exploreNavigateTo(item) is MusicParent -> navModel.exploreNavigateTo(item)
} }
} }
@ -194,8 +192,7 @@ class SearchFragment :
is Artist -> SearchFragmentDirections.actionShowArtist(item.uid) is Artist -> SearchFragmentDirections.actionShowArtist(item.uid)
is Genre -> SearchFragmentDirections.actionShowGenre(item.uid) is Genre -> SearchFragmentDirections.actionShowGenre(item.uid)
else -> return else -> return
} })
)
imm.hide() imm.hide()
} }

View file

@ -23,6 +23,7 @@ import androidx.annotation.IdRes
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import java.text.Normalizer
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
@ -42,7 +43,6 @@ import org.oxycblt.auxio.ui.recycler.Header
import org.oxycblt.auxio.ui.recycler.Item import org.oxycblt.auxio.ui.recycler.Item
import org.oxycblt.auxio.util.application import org.oxycblt.auxio.util.application
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import java.text.Normalizer
/** /**
* The [ViewModel] for search functionality. * The [ViewModel] for search functionality.
@ -87,43 +87,44 @@ class SearchViewModel(application: Application) :
logD("Performing search for $query") logD("Performing search for $query")
// Searching can be quite expensive, so get on a co-routine // Searching can be quite expensive, so get on a co-routine
currentSearchJob = viewModelScope.launch { currentSearchJob =
val sort = Sort(Sort.Mode.ByName, true) viewModelScope.launch {
val results = mutableListOf<Item>() val sort = Sort(Sort.Mode.ByName, true)
val results = mutableListOf<Item>()
// Note: a filter mode of null means to not filter at all. // Note: a filter mode of null means to not filter at all.
if (filterMode == null || filterMode == MusicMode.ARTISTS) { if (filterMode == null || filterMode == MusicMode.ARTISTS) {
library.artists.filterArtistsBy(query)?.let { artists -> library.artists.filterArtistsBy(query)?.let { artists ->
results.add(Header(R.string.lbl_artists)) results.add(Header(R.string.lbl_artists))
results.addAll(sort.artists(artists)) results.addAll(sort.artists(artists))
}
} }
}
if (filterMode == null || filterMode == MusicMode.ALBUMS) { if (filterMode == null || filterMode == MusicMode.ALBUMS) {
library.albums.filterAlbumsBy(query)?.let { albums -> library.albums.filterAlbumsBy(query)?.let { albums ->
results.add(Header(R.string.lbl_albums)) results.add(Header(R.string.lbl_albums))
results.addAll(sort.albums(albums)) results.addAll(sort.albums(albums))
}
} }
}
if (filterMode == null || filterMode == MusicMode.GENRES) { if (filterMode == null || filterMode == MusicMode.GENRES) {
library.genres.filterGenresBy(query)?.let { genres -> library.genres.filterGenresBy(query)?.let { genres ->
results.add(Header(R.string.lbl_genres)) results.add(Header(R.string.lbl_genres))
results.addAll(sort.genres(genres)) results.addAll(sort.genres(genres))
}
} }
}
if (filterMode == null || filterMode == MusicMode.SONGS) { if (filterMode == null || filterMode == MusicMode.SONGS) {
library.songs.filterSongsBy(query)?.let { songs -> library.songs.filterSongsBy(query)?.let { songs ->
results.add(Header(R.string.lbl_songs)) results.add(Header(R.string.lbl_songs))
results.addAll(sort.songs(songs)) results.addAll(sort.songs(songs))
}
} }
}
yield() yield()
_searchResults.value = results _searchResults.value = results
} }
} }
/** /**
@ -162,14 +163,14 @@ class SearchViewModel(application: Application) :
private inline fun <T : Music> List<T>.baseFilterBy(value: String, fallback: (T) -> Boolean) = private inline fun <T : Music> List<T>.baseFilterBy(value: String, fallback: (T) -> Boolean) =
filter { filter {
// The basic comparison is first by the *normalized* name, as that allows a // The basic comparison is first by the *normalized* name, as that allows a
// non-unicode search to match with some unicode characters. In an ideal world, we // non-unicode search to match with some unicode characters. In an ideal world, we
// would just want to leverage CollationKey, but that is not designed for a contains // would just want to leverage CollationKey, but that is not designed for a contains
// algorithm. If that fails, filter impls have fallback values, primarily around // algorithm. If that fails, filter impls have fallback values, primarily around
// sort tags or file names. // sort tags or file names.
it.resolveNameNormalized(application).contains(value, ignoreCase = true) || it.resolveNameNormalized(application).contains(value, ignoreCase = true) ||
fallback(it) fallback(it)
} }
.ifEmpty { null } .ifEmpty { null }
private fun Music.resolveNameNormalized(context: Context): String { private fun Music.resolveNameNormalized(context: Context): String {

View file

@ -84,8 +84,7 @@ class AboutFragment : ViewBindingFragment<FragmentAboutBinding>() {
binding.aboutTotalDuration.text = binding.aboutTotalDuration.text =
getString( getString(
R.string.fmt_lib_total_duration, R.string.fmt_lib_total_duration,
songs.sumOf { it.durationMs }.formatDurationMs(false) songs.sumOf { it.durationMs }.formatDurationMs(false))
)
} }
private fun updateAlbumCount(albums: List<Album>) { private fun updateAlbumCount(albums: List<Album>) {

View file

@ -84,11 +84,12 @@ class Settings(private val context: Context, private val callback: Callback? = n
if (inner.contains(OldKeys.KEY_SHOW_COVERS) || inner.contains(OldKeys.KEY_QUALITY_COVERS)) { if (inner.contains(OldKeys.KEY_SHOW_COVERS) || inner.contains(OldKeys.KEY_QUALITY_COVERS)) {
logD("Migrating cover settings") logD("Migrating cover settings")
val mode = when { val mode =
!inner.getBoolean(OldKeys.KEY_SHOW_COVERS, true) -> CoverMode.OFF when {
!inner.getBoolean(OldKeys.KEY_QUALITY_COVERS, true) -> CoverMode.MEDIA_STORE !inner.getBoolean(OldKeys.KEY_SHOW_COVERS, true) -> CoverMode.OFF
else -> CoverMode.QUALITY !inner.getBoolean(OldKeys.KEY_QUALITY_COVERS, true) -> CoverMode.MEDIA_STORE
} else -> CoverMode.QUALITY
}
inner.edit { inner.edit {
putInt(context.getString(R.string.set_key_cover_mode), mode.intCode) putInt(context.getString(R.string.set_key_cover_mode), mode.intCode)
@ -100,11 +101,12 @@ class Settings(private val context: Context, private val callback: Callback? = n
if (inner.contains(OldKeys.KEY_ALT_NOTIF_ACTION)) { if (inner.contains(OldKeys.KEY_ALT_NOTIF_ACTION)) {
logD("Migrating ${OldKeys.KEY_ALT_NOTIF_ACTION}") logD("Migrating ${OldKeys.KEY_ALT_NOTIF_ACTION}")
val mode = if (inner.getBoolean(OldKeys.KEY_ALT_NOTIF_ACTION, false)) { val mode =
ActionMode.SHUFFLE if (inner.getBoolean(OldKeys.KEY_ALT_NOTIF_ACTION, false)) {
} else { ActionMode.SHUFFLE
ActionMode.REPEAT } else {
} ActionMode.REPEAT
}
inner.edit { inner.edit {
putInt(context.getString(R.string.set_key_notif_action), mode.intCode) putInt(context.getString(R.string.set_key_notif_action), mode.intCode)
@ -125,10 +127,11 @@ class Settings(private val context: Context, private val callback: Callback? = n
if (inner.contains(OldKeys.KEY_LIB_PLAYBACK_MODE)) { if (inner.contains(OldKeys.KEY_LIB_PLAYBACK_MODE)) {
logD("Migrating ${OldKeys.KEY_LIB_PLAYBACK_MODE}") logD("Migrating ${OldKeys.KEY_LIB_PLAYBACK_MODE}")
val mode = inner.getInt( val mode =
OldKeys.KEY_LIB_PLAYBACK_MODE, inner
IntegerTable.PLAYBACK_MODE_ALL_SONGS .getInt(OldKeys.KEY_LIB_PLAYBACK_MODE, IntegerTable.PLAYBACK_MODE_ALL_SONGS)
).migratePlaybackMode() ?: MusicMode.SONGS .migratePlaybackMode()
?: MusicMode.SONGS
inner.edit { inner.edit {
putInt(context.getString(R.string.set_key_library_song_playback_mode), mode.intCode) putInt(context.getString(R.string.set_key_library_song_playback_mode), mode.intCode)
@ -140,13 +143,13 @@ class Settings(private val context: Context, private val callback: Callback? = n
if (inner.contains(OldKeys.KEY_DETAIL_PLAYBACK_MODE)) { if (inner.contains(OldKeys.KEY_DETAIL_PLAYBACK_MODE)) {
logD("Migrating ${OldKeys.KEY_DETAIL_PLAYBACK_MODE}") logD("Migrating ${OldKeys.KEY_DETAIL_PLAYBACK_MODE}")
val mode = inner.getInt(OldKeys.KEY_DETAIL_PLAYBACK_MODE, Int.MIN_VALUE).migratePlaybackMode() val mode =
inner.getInt(OldKeys.KEY_DETAIL_PLAYBACK_MODE, Int.MIN_VALUE).migratePlaybackMode()
inner.edit { inner.edit {
putInt( putInt(
context.getString(R.string.set_key_detail_song_playback_mode), context.getString(R.string.set_key_detail_song_playback_mode),
mode?.intCode ?: Int.MIN_VALUE mode?.intCode ?: Int.MIN_VALUE)
)
remove(OldKeys.KEY_DETAIL_PLAYBACK_MODE) remove(OldKeys.KEY_DETAIL_PLAYBACK_MODE)
apply() apply()
} }
@ -173,8 +176,7 @@ class Settings(private val context: Context, private val callback: Callback? = n
get() = get() =
inner.getInt( inner.getInt(
context.getString(R.string.set_key_theme), context.getString(R.string.set_key_theme),
AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
)
/** Whether the dark theme should be black or not */ /** Whether the dark theme should be black or not */
val useBlackTheme: Boolean val useBlackTheme: Boolean
@ -182,7 +184,8 @@ class Settings(private val context: Context, private val callback: Callback? = n
/** The current accent. */ /** The current accent. */
var accent: Accent var accent: Accent
get() = Accent.from(inner.getInt(context.getString(R.string.set_key_accent), Accent.DEFAULT)) get() =
Accent.from(inner.getInt(context.getString(R.string.set_key_accent), Accent.DEFAULT))
set(value) { set(value) {
inner.edit { inner.edit {
putInt(context.getString(R.string.set_key_accent), value.index) putInt(context.getString(R.string.set_key_accent), value.index)
@ -194,8 +197,7 @@ class Settings(private val context: Context, private val callback: Callback? = n
var libTabs: Array<Tab> var libTabs: Array<Tab>
get() = get() =
Tab.fromSequence( Tab.fromSequence(
inner.getInt(context.getString(R.string.set_key_lib_tabs), Tab.SEQUENCE_DEFAULT) inner.getInt(context.getString(R.string.set_key_lib_tabs), Tab.SEQUENCE_DEFAULT))
)
?: unlikelyToBeNull(Tab.fromSequence(Tab.SEQUENCE_DEFAULT)) ?: unlikelyToBeNull(Tab.fromSequence(Tab.SEQUENCE_DEFAULT))
set(value) { set(value) {
inner.edit { inner.edit {
@ -216,15 +218,15 @@ class Settings(private val context: Context, private val callback: Callback? = n
val actionMode: ActionMode val actionMode: ActionMode
get() = get() =
ActionMode.fromIntCode( ActionMode.fromIntCode(
inner.getInt(context.getString(R.string.set_key_bar_action), Int.MIN_VALUE) inner.getInt(context.getString(R.string.set_key_bar_action), Int.MIN_VALUE))
)
?: ActionMode.NEXT ?: ActionMode.NEXT
/** /** The custom action to display in the notification. */
* The custom action to display in the notification.
*/
val notifAction: ActionMode val notifAction: ActionMode
get() = ActionMode.fromIntCode(inner.getInt(context.getString(R.string.set_key_notif_action), Int.MIN_VALUE)) ?: ActionMode.REPEAT get() =
ActionMode.fromIntCode(
inner.getInt(context.getString(R.string.set_key_notif_action), Int.MIN_VALUE))
?: ActionMode.REPEAT
/** Whether to resume playback when a headset is connected (may not work well in all cases) */ /** Whether to resume playback when a headset is connected (may not work well in all cases) */
val headsetAutoplay: Boolean val headsetAutoplay: Boolean
@ -234,8 +236,7 @@ class Settings(private val context: Context, private val callback: Callback? = n
val replayGainMode: ReplayGainMode val replayGainMode: ReplayGainMode
get() = get() =
ReplayGainMode.fromIntCode( ReplayGainMode.fromIntCode(
inner.getInt(context.getString(R.string.set_key_replay_gain), Int.MIN_VALUE) inner.getInt(context.getString(R.string.set_key_replay_gain), Int.MIN_VALUE))
)
?: ReplayGainMode.DYNAMIC ?: ReplayGainMode.DYNAMIC
/** The current ReplayGain pre-amp configuration */ /** The current ReplayGain pre-amp configuration */
@ -243,8 +244,7 @@ class Settings(private val context: Context, private val callback: Callback? = n
get() = get() =
ReplayGainPreAmp( ReplayGainPreAmp(
inner.getFloat(context.getString(R.string.set_key_pre_amp_with), 0f), inner.getFloat(context.getString(R.string.set_key_pre_amp_with), 0f),
inner.getFloat(context.getString(R.string.set_key_pre_amp_without), 0f) inner.getFloat(context.getString(R.string.set_key_pre_amp_without), 0f))
)
set(value) { set(value) {
inner.edit { inner.edit {
putFloat(context.getString(R.string.set_key_pre_amp_with), value.with) putFloat(context.getString(R.string.set_key_pre_amp_with), value.with)
@ -258,10 +258,7 @@ class Settings(private val context: Context, private val callback: Callback? = n
get() = get() =
MusicMode.fromInt( MusicMode.fromInt(
inner.getInt( inner.getInt(
context.getString(R.string.set_key_library_song_playback_mode), context.getString(R.string.set_key_library_song_playback_mode), Int.MIN_VALUE))
Int.MIN_VALUE
)
)
?: MusicMode.SONGS ?: MusicMode.SONGS
/** /**
@ -272,10 +269,7 @@ class Settings(private val context: Context, private val callback: Callback? = n
get() = get() =
MusicMode.fromInt( MusicMode.fromInt(
inner.getInt( inner.getInt(
context.getString(R.string.set_key_detail_song_playback_mode), context.getString(R.string.set_key_detail_song_playback_mode), Int.MIN_VALUE))
Int.MIN_VALUE
)
)
/** Whether shuffle should stay on when a new song is selected. */ /** Whether shuffle should stay on when a new song is selected. */
val keepShuffle: Boolean val keepShuffle: Boolean
@ -298,7 +292,10 @@ class Settings(private val context: Context, private val callback: Callback? = n
/** The strategy used when loading images. */ /** The strategy used when loading images. */
val coverMode: CoverMode val coverMode: CoverMode
get() = CoverMode.fromIntCode(inner.getInt(context.getString(R.string.set_key_cover_mode), Int.MIN_VALUE)) ?: CoverMode.MEDIA_STORE get() =
CoverMode.fromIntCode(
inner.getInt(context.getString(R.string.set_key_cover_mode), Int.MIN_VALUE))
?: CoverMode.MEDIA_STORE
/** Whether to load all audio files, even ones not considered music. */ /** Whether to load all audio files, even ones not considered music. */
val excludeNonMusic: Boolean val excludeNonMusic: Boolean
@ -311,9 +308,7 @@ class Settings(private val context: Context, private val callback: Callback? = n
.mapNotNull { Directory.fromDocumentUri(storageManager, it) } .mapNotNull { Directory.fromDocumentUri(storageManager, it) }
return MusicDirs( return MusicDirs(
dirs, dirs, inner.getBoolean(context.getString(R.string.set_key_music_dirs_include), false))
inner.getBoolean(context.getString(R.string.set_key_music_dirs_include), false)
)
} }
/** Set the list of directories that music should be hidden/loaded from. */ /** Set the list of directories that music should be hidden/loaded from. */
@ -321,12 +316,9 @@ class Settings(private val context: Context, private val callback: Callback? = n
inner.edit { inner.edit {
putStringSet( putStringSet(
context.getString(R.string.set_key_music_dirs), context.getString(R.string.set_key_music_dirs),
musicDirs.dirs.map(Directory::toDocumentUri).toSet() musicDirs.dirs.map(Directory::toDocumentUri).toSet())
)
putBoolean( putBoolean(
context.getString(R.string.set_key_music_dirs_include), context.getString(R.string.set_key_music_dirs_include), musicDirs.shouldInclude)
musicDirs.shouldInclude
)
apply() apply()
} }
} }
@ -348,14 +340,12 @@ class Settings(private val context: Context, private val callback: Callback? = n
var searchFilterMode: MusicMode? var searchFilterMode: MusicMode?
get() = get() =
MusicMode.fromInt( MusicMode.fromInt(
inner.getInt(context.getString(R.string.set_key_search_filter), Int.MIN_VALUE) inner.getInt(context.getString(R.string.set_key_search_filter), Int.MIN_VALUE))
)
set(value) { set(value) {
inner.edit { inner.edit {
putInt( putInt(
context.getString(R.string.set_key_search_filter), context.getString(R.string.set_key_search_filter),
value?.intCode ?: Int.MIN_VALUE value?.intCode ?: Int.MIN_VALUE)
)
apply() apply()
} }
} }
@ -364,8 +354,7 @@ class Settings(private val context: Context, private val callback: Callback? = n
var libSongSort: Sort var libSongSort: Sort
get() = get() =
Sort.fromIntCode( Sort.fromIntCode(
inner.getInt(context.getString(R.string.set_key_lib_songs_sort), Int.MIN_VALUE) inner.getInt(context.getString(R.string.set_key_lib_songs_sort), Int.MIN_VALUE))
)
?: Sort(Sort.Mode.ByName, true) ?: Sort(Sort.Mode.ByName, true)
set(value) { set(value) {
inner.edit { inner.edit {
@ -378,8 +367,7 @@ class Settings(private val context: Context, private val callback: Callback? = n
var libAlbumSort: Sort var libAlbumSort: Sort
get() = get() =
Sort.fromIntCode( Sort.fromIntCode(
inner.getInt(context.getString(R.string.set_key_lib_albums_sort), Int.MIN_VALUE) inner.getInt(context.getString(R.string.set_key_lib_albums_sort), Int.MIN_VALUE))
)
?: Sort(Sort.Mode.ByName, true) ?: Sort(Sort.Mode.ByName, true)
set(value) { set(value) {
inner.edit { inner.edit {
@ -392,8 +380,7 @@ class Settings(private val context: Context, private val callback: Callback? = n
var libArtistSort: Sort var libArtistSort: Sort
get() = get() =
Sort.fromIntCode( Sort.fromIntCode(
inner.getInt(context.getString(R.string.set_key_lib_artists_sort), Int.MIN_VALUE) inner.getInt(context.getString(R.string.set_key_lib_artists_sort), Int.MIN_VALUE))
)
?: Sort(Sort.Mode.ByName, true) ?: Sort(Sort.Mode.ByName, true)
set(value) { set(value) {
inner.edit { inner.edit {
@ -406,8 +393,7 @@ class Settings(private val context: Context, private val callback: Callback? = n
var libGenreSort: Sort var libGenreSort: Sort
get() = get() =
Sort.fromIntCode( Sort.fromIntCode(
inner.getInt(context.getString(R.string.set_key_lib_genres_sort), Int.MIN_VALUE) inner.getInt(context.getString(R.string.set_key_lib_genres_sort), Int.MIN_VALUE))
)
?: Sort(Sort.Mode.ByName, true) ?: Sort(Sort.Mode.ByName, true)
set(value) { set(value) {
inner.edit { inner.edit {
@ -422,10 +408,7 @@ class Settings(private val context: Context, private val callback: Callback? = n
var sort = var sort =
Sort.fromIntCode( Sort.fromIntCode(
inner.getInt( inner.getInt(
context.getString(R.string.set_key_detail_album_sort), context.getString(R.string.set_key_detail_album_sort), Int.MIN_VALUE))
Int.MIN_VALUE
)
)
?: Sort(Sort.Mode.ByDisc, true) ?: Sort(Sort.Mode.ByDisc, true)
// Correct legacy album sort modes to Disc // Correct legacy album sort modes to Disc
@ -446,8 +429,7 @@ class Settings(private val context: Context, private val callback: Callback? = n
var detailArtistSort: Sort var detailArtistSort: Sort
get() = get() =
Sort.fromIntCode( Sort.fromIntCode(
inner.getInt(context.getString(R.string.set_key_detail_artist_sort), Int.MIN_VALUE) inner.getInt(context.getString(R.string.set_key_detail_artist_sort), Int.MIN_VALUE))
)
?: Sort(Sort.Mode.ByDate, false) ?: Sort(Sort.Mode.ByDate, false)
set(value) { set(value) {
inner.edit { inner.edit {
@ -460,8 +442,7 @@ class Settings(private val context: Context, private val callback: Callback? = n
var detailGenreSort: Sort var detailGenreSort: Sort
get() = get() =
Sort.fromIntCode( Sort.fromIntCode(
inner.getInt(context.getString(R.string.set_key_detail_genre_sort), Int.MIN_VALUE) inner.getInt(context.getString(R.string.set_key_detail_genre_sort), Int.MIN_VALUE))
)
?: Sort(Sort.Mode.ByName, true) ?: Sort(Sort.Mode.ByName, true)
set(value) { set(value) {
inner.edit { inner.edit {

View file

@ -26,10 +26,10 @@ import androidx.core.content.res.getTextArrayOrThrow
import androidx.preference.DialogPreference import androidx.preference.DialogPreference
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceViewHolder import androidx.preference.PreferenceViewHolder
import java.lang.reflect.Field
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.util.lazyReflectedField import org.oxycblt.auxio.util.lazyReflectedField
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import java.lang.reflect.Field
class IntListPreference class IntListPreference
@JvmOverloads @JvmOverloads
@ -57,18 +57,13 @@ constructor(
init { init {
val prefAttrs = val prefAttrs =
context.obtainStyledAttributes( context.obtainStyledAttributes(
attrs, attrs, R.styleable.IntListPreference, defStyleAttr, defStyleRes)
R.styleable.IntListPreference,
defStyleAttr,
defStyleRes
)
entries = prefAttrs.getTextArrayOrThrow(R.styleable.IntListPreference_entries) entries = prefAttrs.getTextArrayOrThrow(R.styleable.IntListPreference_entries)
values = values =
context.resources.getIntArray( context.resources.getIntArray(
prefAttrs.getResourceIdOrThrow(R.styleable.IntListPreference_entryValues) prefAttrs.getResourceIdOrThrow(R.styleable.IntListPreference_entryValues))
)
val offValueId = prefAttrs.getResourceId(R.styleable.IntListPreference_offValue, -1) val offValueId = prefAttrs.getResourceId(R.styleable.IntListPreference_offValue, -1)
if (offValueId > -1) { if (offValueId > -1) {
@ -152,6 +147,6 @@ constructor(
companion object { companion object {
private val PREFERENCE_DEFAULT_VALUE_FIELD: Field by private val PREFERENCE_DEFAULT_VALUE_FIELD: Field by
lazyReflectedField(Preference::class, "mDefaultValue") lazyReflectedField(Preference::class, "mDefaultValue")
} }
} }

View file

@ -17,8 +17,6 @@
package org.oxycblt.auxio.settings.prefs package org.oxycblt.auxio.settings.prefs
import android.app.Activity
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
@ -43,8 +41,6 @@ import org.oxycblt.auxio.util.isNight
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.showToast import org.oxycblt.auxio.util.showToast
import org.oxycblt.auxio.util.systemBarInsetsCompat import org.oxycblt.auxio.util.systemBarInsetsCompat
import java.security.Permission
import java.util.jar.Manifest
/** /**
* The actual fragment containing the settings menu. Inherits [PreferenceFragmentCompat]. * The actual fragment containing the settings menu. Inherits [PreferenceFragmentCompat].
@ -98,14 +94,20 @@ class PreferenceFragment : PreferenceFragmentCompat() {
} }
is WrappedDialogPreference -> { is WrappedDialogPreference -> {
val context = requireContext() val context = requireContext()
val directions = when (preference.key) { val directions =
context.getString(R.string.set_key_accent) -> SettingsFragmentDirections.goToAccentDialog() when (preference.key) {
context.getString(R.string.set_key_lib_tabs) -> SettingsFragmentDirections.goToTabDialog() context.getString(R.string.set_key_accent) ->
context.getString(R.string.set_key_pre_amp) -> SettingsFragmentDirections.goToPreAmpDialog() SettingsFragmentDirections.goToAccentDialog()
context.getString(R.string.set_key_music_dirs) -> SettingsFragmentDirections.goToMusicDirsDialog() context.getString(R.string.set_key_lib_tabs) ->
getString(R.string.set_key_separators) -> SettingsFragmentDirections.goToSeparatorsDialog() SettingsFragmentDirections.goToTabDialog()
else -> error("Unexpected dialog key ${preference.key}") context.getString(R.string.set_key_pre_amp) ->
} SettingsFragmentDirections.goToPreAmpDialog()
context.getString(R.string.set_key_music_dirs) ->
SettingsFragmentDirections.goToMusicDirsDialog()
getString(R.string.set_key_separators) ->
SettingsFragmentDirections.goToSeparatorsDialog()
else -> error("Unexpected dialog key ${preference.key}")
}
findNavController().navigate(directions) findNavController().navigate(directions)
} }
else -> super.onDisplayPreferenceDialog(preference) else -> super.onDisplayPreferenceDialog(preference)

View file

@ -49,14 +49,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
if (child != null) { if (child != null) {
val coordinator = parent as CoordinatorLayout val coordinator = parent as CoordinatorLayout
coordinatorLayoutBehavior?.onNestedPreScroll( coordinatorLayoutBehavior?.onNestedPreScroll(
coordinator, coordinator, this, coordinator, 0, 0, tConsumed, 0)
this,
coordinator,
0,
0,
tConsumed,
0
)
} }
true true

View file

@ -23,10 +23,10 @@ import android.view.View
import android.view.WindowInsets import android.view.WindowInsets
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
import com.google.android.material.bottomsheet.NeoBottomSheetBehavior import com.google.android.material.bottomsheet.NeoBottomSheetBehavior
import kotlin.math.abs
import org.oxycblt.auxio.util.coordinatorLayoutBehavior import org.oxycblt.auxio.util.coordinatorLayoutBehavior
import org.oxycblt.auxio.util.replaceSystemBarInsetsCompat import org.oxycblt.auxio.util.replaceSystemBarInsetsCompat
import org.oxycblt.auxio.util.systemBarInsetsCompat import org.oxycblt.auxio.util.systemBarInsetsCompat
import kotlin.math.abs
/** /**
* A behavior that automatically re-layouts and re-insets content to align with the parent layout's * A behavior that automatically re-layouts and re-insets content to align with the parent layout's
@ -127,11 +127,7 @@ class BottomSheetContentBehavior<V : View>(context: Context, attributeSet: Attri
val bars = insets.systemBarInsetsCompat val bars = insets.systemBarInsetsCompat
insets.replaceSystemBarInsetsCompat( insets.replaceSystemBarInsetsCompat(
bars.left, bars.left, bars.top, bars.right, (bars.bottom - consumed).coerceAtLeast(0))
bars.top,
bars.right,
(bars.bottom - consumed).coerceAtLeast(0)
)
} }
setup = true setup = true
@ -145,9 +141,7 @@ class BottomSheetContentBehavior<V : View>(context: Context, attributeSet: Attri
View.MeasureSpec.makeMeasureSpec(parent.measuredWidth, View.MeasureSpec.EXACTLY) View.MeasureSpec.makeMeasureSpec(parent.measuredWidth, View.MeasureSpec.EXACTLY)
val contentHeightSpec = val contentHeightSpec =
View.MeasureSpec.makeMeasureSpec( View.MeasureSpec.makeMeasureSpec(
parent.measuredHeight - consumed, parent.measuredHeight - consumed, View.MeasureSpec.EXACTLY)
View.MeasureSpec.EXACTLY
)
child.measure(contentWidthSpec, contentHeightSpec) child.measure(contentWidthSpec, contentHeightSpec)
} }

View file

@ -39,8 +39,7 @@ private val ACCENT_NAMES =
R.string.clr_orange, R.string.clr_orange,
R.string.clr_brown, R.string.clr_brown,
R.string.clr_grey, R.string.clr_grey,
R.string.clr_dynamic R.string.clr_dynamic)
)
private val ACCENT_THEMES = private val ACCENT_THEMES =
intArrayOf( intArrayOf(
@ -61,7 +60,7 @@ private val ACCENT_THEMES =
R.style.Theme_Auxio_Brown, R.style.Theme_Auxio_Brown,
R.style.Theme_Auxio_Grey, R.style.Theme_Auxio_Grey,
R.style.Theme_Auxio_App // Dynamic colors are on the base theme R.style.Theme_Auxio_App // Dynamic colors are on the base theme
) )
private val ACCENT_BLACK_THEMES = private val ACCENT_BLACK_THEMES =
intArrayOf( intArrayOf(
@ -82,7 +81,7 @@ private val ACCENT_BLACK_THEMES =
R.style.Theme_Auxio_Black_Brown, R.style.Theme_Auxio_Black_Brown,
R.style.Theme_Auxio_Black_Grey, R.style.Theme_Auxio_Black_Grey,
R.style.Theme_Auxio_Black // Dynamic colors are on the base theme R.style.Theme_Auxio_Black // Dynamic colors are on the base theme
) )
private val ACCENT_PRIMARY_COLORS = private val ACCENT_PRIMARY_COLORS =
intArrayOf( intArrayOf(
@ -102,8 +101,7 @@ private val ACCENT_PRIMARY_COLORS =
R.color.orange_primary, R.color.orange_primary,
R.color.brown_primary, R.color.brown_primary,
R.color.grey_primary, R.color.grey_primary,
R.color.dynamic_primary R.color.dynamic_primary)
)
/** /**
* The data object for an accent. In the UI this is known as a "Color Scheme." This can be nominally * The data object for an accent. In the UI this is known as a "Color Scheme." This can be nominally

View file

@ -62,8 +62,7 @@ class AccentCustomizeDialog :
Accent.from(savedInstanceState.getInt(KEY_PENDING_ACCENT)) Accent.from(savedInstanceState.getInt(KEY_PENDING_ACCENT))
} else { } else {
settings.accent settings.accent
} })
)
} }
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {

View file

@ -21,9 +21,9 @@ import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import kotlin.math.max
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.util.getDimenSize import org.oxycblt.auxio.util.getDimenSize
import kotlin.math.max
/** /**
* A sub-class of [GridLayoutManager] that automatically sets the spans so that they fit the width * A sub-class of [GridLayoutManager] that automatically sets the spans so that they fit the width

View file

@ -165,8 +165,7 @@ constructor(context: Context, attrs: AttributeSet? = null, defStyleRes: Int = 0)
centerY + radius, centerY + radius,
startAngle, startAngle,
sweepAngle, sweepAngle,
false false)
)
} }
} }

View file

@ -33,6 +33,7 @@ import androidx.core.view.isInvisible
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import kotlin.math.abs
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.ui.recycler.AuxioRecyclerView import org.oxycblt.auxio.ui.recycler.AuxioRecyclerView
import org.oxycblt.auxio.util.getDimenSize import org.oxycblt.auxio.util.getDimenSize
@ -40,7 +41,6 @@ import org.oxycblt.auxio.util.getDrawableCompat
import org.oxycblt.auxio.util.isRtl import org.oxycblt.auxio.util.isRtl
import org.oxycblt.auxio.util.isUnder import org.oxycblt.auxio.util.isUnder
import org.oxycblt.auxio.util.systemBarInsetsCompat import org.oxycblt.auxio.util.systemBarInsetsCompat
import kotlin.math.abs
/** /**
* A [RecyclerView] that enables better fast-scrolling. This is fundamentally a implementation of * A [RecyclerView] that enables better fast-scrolling. This is fundamentally a implementation of
@ -97,9 +97,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
FastScrollPopupView(context).apply { FastScrollPopupView(context).apply {
layoutParams = layoutParams =
FrameLayout.LayoutParams( FrameLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
ViewGroup.LayoutParams.WRAP_CONTENT
)
.apply { .apply {
gravity = Gravity.CENTER_HORIZONTAL or Gravity.TOP gravity = Gravity.CENTER_HORIZONTAL or Gravity.TOP
marginEnd = context.getDimenSize(R.dimen.spacing_small) marginEnd = context.getDimenSize(R.dimen.spacing_small)
@ -174,8 +172,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
override fun onDraw(canvas: Canvas, parent: RecyclerView, state: State) { override fun onDraw(canvas: Canvas, parent: RecyclerView, state: State) {
onPreDraw() onPreDraw()
} }
} })
)
// We use a listener instead of overriding onTouchEvent so that we don't conflict with // We use a listener instead of overriding onTouchEvent so that we don't conflict with
// RecyclerView touch events. // RecyclerView touch events.
@ -191,8 +188,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
): Boolean { ): Boolean {
return onItemTouch(event) return onItemTouch(event)
} }
} })
)
} }
// --- RECYCLERVIEW EVENT MANAGEMENT --- // --- RECYCLERVIEW EVENT MANAGEMENT ---
@ -247,8 +243,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
thumbWidth + thumbWidth +
popupLayoutParams.leftMargin + popupLayoutParams.leftMargin +
popupLayoutParams.rightMargin, popupLayoutParams.rightMargin,
popupLayoutParams.width popupLayoutParams.width)
)
val heightMeasureSpec = val heightMeasureSpec =
ViewGroup.getChildMeasureSpec( ViewGroup.getChildMeasureSpec(
@ -257,8 +252,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
thumbPadding.bottom + thumbPadding.bottom +
popupLayoutParams.topMargin + popupLayoutParams.topMargin +
popupLayoutParams.bottomMargin, popupLayoutParams.bottomMargin,
popupLayoutParams.height popupLayoutParams.height)
)
popupView.measure(widthMeasureSpec, heightMeasureSpec) popupView.measure(widthMeasureSpec, heightMeasureSpec)
} }
@ -279,8 +273,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
(thumbTop + thumbAnchorY - popupAnchorY) (thumbTop + thumbAnchorY - popupAnchorY)
.coerceAtLeast(thumbPadding.top + popupLayoutParams.topMargin) .coerceAtLeast(thumbPadding.top + popupLayoutParams.topMargin)
.coerceAtMost( .coerceAtMost(
height - thumbPadding.bottom - popupLayoutParams.bottomMargin - popupHeight height - thumbPadding.bottom - popupLayoutParams.bottomMargin - popupHeight)
)
popupView.layout(popupLeft, popupTop, popupLeft + popupWidth, popupTop + popupHeight) popupView.layout(popupLeft, popupTop, popupLeft + popupWidth, popupTop + popupHeight)
} }
@ -355,8 +348,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
MotionEvent.ACTION_MOVE -> { MotionEvent.ACTION_MOVE -> {
if (!dragging && if (!dragging &&
thumbView.isUnder(downX, thumbView.top.toFloat(), minTouchTargetSize) && thumbView.isUnder(downX, thumbView.top.toFloat(), minTouchTargetSize) &&
abs(eventY - downY) > touchSlop abs(eventY - downY) > touchSlop) {
) {
if (thumbView.isUnder(downX, downY, minTouchTargetSize)) { if (thumbView.isUnder(downX, downY, minTouchTargetSize)) {
dragStartY = lastY dragStartY = lastY
dragStartThumbOffset = thumbOffset dragStartThumbOffset = thumbOffset

View file

@ -49,8 +49,8 @@ abstract class MusicFragment<T : ViewBinding> : ViewBindingFragment<T>() {
protected val navModel: NavigationViewModel by activityViewModels() protected val navModel: NavigationViewModel by activityViewModels()
/** /**
* Run the UI flow to perform a specific [PickerMode] action with a particular * Run the UI flow to perform a specific [PickerMode] action with a particular artist from
* artist from [song]. * [song].
*/ */
fun doArtistDependentAction(song: Song, mode: PickerMode) { fun doArtistDependentAction(song: Song, mode: PickerMode) {
if (song.artists.size == 1) { if (song.artists.size == 1) {
@ -61,24 +61,18 @@ abstract class MusicFragment<T : ViewBinding> : ViewBindingFragment<T>() {
} else { } else {
navModel.mainNavigateTo( navModel.mainNavigateTo(
MainNavigationAction.Directions( MainNavigationAction.Directions(
MainFragmentDirections.actionPickArtist(song.uid, mode) MainFragmentDirections.actionPickArtist(song.uid, mode)))
)
)
} }
} }
/** /** Run the UI flow to navigate to a particular artist from [album]. */
* Run the UI flow to navigate to a particular artist from [album].
*/
fun navigateToArtist(album: Album) { fun navigateToArtist(album: Album) {
if (album.artists.size == 1) { if (album.artists.size == 1) {
navModel.exploreNavigateTo(album.artists[0]) navModel.exploreNavigateTo(album.artists[0])
} else { } else {
navModel.mainNavigateTo( navModel.mainNavigateTo(
MainNavigationAction.Directions( MainNavigationAction.Directions(
MainFragmentDirections.actionPickArtist(album.uid, PickerMode.SHOW) MainFragmentDirections.actionPickArtist(album.uid, PickerMode.SHOW)))
)
)
} }
} }
@ -108,9 +102,7 @@ abstract class MusicFragment<T : ViewBinding> : ViewBindingFragment<T>() {
R.id.action_song_detail -> { R.id.action_song_detail -> {
navModel.mainNavigateTo( navModel.mainNavigateTo(
MainNavigationAction.Directions( MainNavigationAction.Directions(
MainFragmentDirections.actionShowDetails(song.uid) MainFragmentDirections.actionShowDetails(song.uid)))
)
)
} }
else -> { else -> {
error("Unexpected menu item selected") error("Unexpected menu item selected")

View file

@ -26,10 +26,10 @@ import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.viewbinding.ViewBinding import androidx.viewbinding.ViewBinding
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.unlikelyToBeNull
import kotlin.properties.ReadOnlyProperty import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty import kotlin.reflect.KProperty
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.unlikelyToBeNull
/** /**
* A dialog fragment enabling ViewBinding inflation and usage across the dialog fragment lifecycle. * A dialog fragment enabling ViewBinding inflation and usage across the dialog fragment lifecycle.

View file

@ -23,10 +23,10 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.viewbinding.ViewBinding import androidx.viewbinding.ViewBinding
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.unlikelyToBeNull
import kotlin.properties.ReadOnlyProperty import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty import kotlin.reflect.KProperty
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.unlikelyToBeNull
/** /**
* A fragment enabling ViewBinding inflation and usage across the fragment lifecycle. * A fragment enabling ViewBinding inflation and usage across the fragment lifecycle.

View file

@ -106,8 +106,6 @@ abstract class DialogViewHolder(root: View) : RecyclerView.ViewHolder(root) {
// Actually make the item full-width, which it won't be in dialogs // Actually make the item full-width, which it won't be in dialogs
root.layoutParams = root.layoutParams =
RecyclerView.LayoutParams( RecyclerView.LayoutParams(
RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT)
RecyclerView.LayoutParams.WRAP_CONTENT
)
} }
} }

View file

@ -49,8 +49,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
initialPadding.left, initialPadding.left,
initialPadding.top, initialPadding.top,
initialPadding.right, initialPadding.right,
initialPadding.bottom + insets.systemBarInsetsCompat.bottom initialPadding.bottom + insets.systemBarInsetsCompat.bottom)
)
return insets return insets
} }

View file

@ -126,8 +126,7 @@ class SyncListDiffer<T>(
throw AssertionError() throw AssertionError()
} }
} }
} })
)
field = newList field = newList
result.dispatchUpdatesTo(updateCallback) result.dispatchUpdatesTo(updateCallback)

View file

@ -68,15 +68,13 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
ViewGroup.getChildMeasureSpec( ViewGroup.getChildMeasureSpec(
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
0, 0,
divider.layoutParams.width divider.layoutParams.width)
)
val heightMeasureSpec = val heightMeasureSpec =
ViewGroup.getChildMeasureSpec( ViewGroup.getChildMeasureSpec(
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY),
0, 0,
divider.layoutParams.height divider.layoutParams.height)
)
divider.measure(widthMeasureSpec, heightMeasureSpec) divider.measure(widthMeasureSpec, heightMeasureSpec)
} }

View file

@ -63,8 +63,7 @@ class SongViewHolder private constructor(private val binding: ItemSongBinding) :
val DIFFER = val DIFFER =
object : SimpleItemCallback<Song>() { object : SimpleItemCallback<Song>() {
override fun areContentsTheSame(oldItem: Song, newItem: Song) = override fun areContentsTheSame(oldItem: Song, newItem: Song) =
oldItem.rawName == newItem.rawName && oldItem.rawName == newItem.rawName && oldItem.areArtistContentsTheSame(newItem)
oldItem.areArtistContentsTheSame(newItem)
} }
} }
} }
@ -119,16 +118,16 @@ class ArtistViewHolder private constructor(private val binding: ItemParentBindin
binding.parentImage.bind(item) binding.parentImage.bind(item)
binding.parentName.text = item.resolveName(binding.context) binding.parentName.text = item.resolveName(binding.context)
binding.parentInfo.text = if (item.songs.isNotEmpty()) { binding.parentInfo.text =
binding.context.getString( if (item.songs.isNotEmpty()) {
R.string.fmt_two, binding.context.getString(
binding.context.getPlural(R.plurals.fmt_album_count, item.albums.size), R.string.fmt_two,
binding.context.getPlural(R.plurals.fmt_song_count, item.songs.size) binding.context.getPlural(R.plurals.fmt_album_count, item.albums.size),
) binding.context.getPlural(R.plurals.fmt_song_count, item.songs.size))
} else { } else {
// Artist has no songs, only display an album count. // Artist has no songs, only display an album count.
binding.context.getPlural(R.plurals.fmt_album_count, item.albums.size) binding.context.getPlural(R.plurals.fmt_album_count, item.albums.size)
} }
// binding.parentMenu.setOnClickListener { listener.onOpenMenu(item, it) } // binding.parentMenu.setOnClickListener { listener.onOpenMenu(item, it) }
binding.root.setOnLongClickListener { binding.root.setOnLongClickListener {
@ -172,8 +171,7 @@ class GenreViewHolder private constructor(private val binding: ItemParentBinding
binding.context.getString( binding.context.getString(
R.string.fmt_two, R.string.fmt_two,
binding.context.getPlural(R.plurals.fmt_artist_count, item.artists.size), binding.context.getPlural(R.plurals.fmt_artist_count, item.artists.size),
binding.context.getPlural(R.plurals.fmt_song_count, item.songs.size) binding.context.getPlural(R.plurals.fmt_song_count, item.songs.size))
)
// binding.parentMenu.setOnClickListener { listener.onOpenMenu(item, it) } // binding.parentMenu.setOnClickListener { listener.onOpenMenu(item, it) }
binding.root.setOnLongClickListener { binding.root.setOnLongClickListener {
listener.onOpenMenu(item, it) listener.onOpenMenu(item, it)

View file

@ -37,9 +37,9 @@ import androidx.annotation.PluralsRes
import androidx.annotation.Px import androidx.annotation.Px
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import kotlin.reflect.KClass
import org.oxycblt.auxio.IntegerTable import org.oxycblt.auxio.IntegerTable
import org.oxycblt.auxio.MainActivity import org.oxycblt.auxio.MainActivity
import kotlin.reflect.KClass
/** Shortcut to get a [LayoutInflater] from a [Context] */ /** Shortcut to get a [LayoutInflater] from a [Context] */
val Context.inflater: LayoutInflater val Context.inflater: LayoutInflater
@ -152,8 +152,7 @@ fun Context.newMainPendingIntent(): PendingIntent =
this, this,
IntegerTable.REQUEST_CODE, IntegerTable.REQUEST_CODE,
Intent(this, MainActivity::class.java), Intent(this, MainActivity::class.java),
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) PendingIntent.FLAG_IMMUTABLE else 0 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) PendingIntent.FLAG_IMMUTABLE else 0)
)
/** Create a broadcast [PendingIntent] */ /** Create a broadcast [PendingIntent] */
fun Context.newBroadcastPendingIntent(what: String): PendingIntent = fun Context.newBroadcastPendingIntent(what: String): PendingIntent =
@ -161,5 +160,4 @@ fun Context.newBroadcastPendingIntent(what: String): PendingIntent =
this, this,
IntegerTable.REQUEST_CODE, IntegerTable.REQUEST_CODE,
Intent(what).setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY), Intent(what).setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY),
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) PendingIntent.FLAG_IMMUTABLE else 0 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) PendingIntent.FLAG_IMMUTABLE else 0)
)

View file

@ -231,8 +231,7 @@ val WindowInsets.systemGestureInsetsCompat: Insets
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> { Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> {
Insets.max( Insets.max(
getCompatInsets(WindowInsets.Type.systemGestures()), getCompatInsets(WindowInsets.Type.systemGestures()),
getCompatInsets(WindowInsets.Type.systemBars()) getCompatInsets(WindowInsets.Type.systemBars()))
)
} }
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> { Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> {
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
@ -247,8 +246,7 @@ fun WindowInsets.getSystemWindowCompatInsets() =
systemWindowInsetLeft, systemWindowInsetLeft,
systemWindowInsetTop, systemWindowInsetTop,
systemWindowInsetRight, systemWindowInsetRight,
systemWindowInsetBottom systemWindowInsetBottom)
)
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
@RequiresApi(Build.VERSION_CODES.Q) @RequiresApi(Build.VERSION_CODES.Q)
@ -272,8 +270,7 @@ fun WindowInsets.replaceSystemBarInsetsCompat(
WindowInsets.Builder(this) WindowInsets.Builder(this)
.setInsets( .setInsets(
WindowInsets.Type.systemBars(), WindowInsets.Type.systemBars(),
Insets.of(left, top, right, bottom).toPlatformInsets() Insets.of(left, top, right, bottom).toPlatformInsets())
)
.build() .build()
} }
else -> { else -> {

View file

@ -50,14 +50,14 @@ private val Any.autoTag: String
get() = "Auxio.${this::class.simpleName ?: "Anonymous Object"}" get() = "Auxio.${this::class.simpleName ?: "Anonymous Object"}"
/** /**
* Note: If you are politely forking this project while keeping the source open, you can ignore * Note: If you are politely forking this project while keeping the source open, you can ignore the
* the following passage. If not, give me a moment of your time. * following passage. If not, give me a moment of your time.
* *
* Consider what you are doing with your life, plagiarizers. Do you want to live a fulfilling * Consider what you are doing with your life, plagiarizers. Do you want to live a fulfilling
* existence on this planet? Or do you want to spend your life taking work others did and making * existence on this planet? Or do you want to spend your life taking work others did and making it
* it objectively worse so you could arbitrage a fraction of a penny on every AdMob impression you * objectively worse so you could arbitrage a fraction of a penny on every AdMob impression you get?
* get? You could do so many great things if you simply had the courage to come up with an idea of * You could do so many great things if you simply had the courage to come up with an idea of your
* your own. * own.
* *
* If you still want to go on, I guess the only thing I can say is this: * If you still want to go on, I guess the only thing I can say is this:
* *
@ -82,13 +82,11 @@ private val Any.autoTag: String
@Suppress("KotlinConstantConditions") @Suppress("KotlinConstantConditions")
private fun basedCopyleftNotice(): Boolean { private fun basedCopyleftNotice(): Boolean {
if (BuildConfig.APPLICATION_ID != "org.oxycblt.auxio" && if (BuildConfig.APPLICATION_ID != "org.oxycblt.auxio" &&
BuildConfig.APPLICATION_ID != "org.oxycblt.auxio.debug" BuildConfig.APPLICATION_ID != "org.oxycblt.auxio.debug") {
) {
Log.d( Log.d(
"Auxio Project", "Auxio Project",
"Friendly reminder: Auxio is licensed under the " + "Friendly reminder: Auxio is licensed under the " +
"GPLv3 and all derivative apps must be made open source!" "GPLv3 and all derivative apps must be made open source!")
)
return true return true
} }

View file

@ -18,10 +18,10 @@
package org.oxycblt.auxio.util package org.oxycblt.auxio.util
import android.os.Looper import android.os.Looper
import org.oxycblt.auxio.BuildConfig
import java.lang.reflect.Field import java.lang.reflect.Field
import java.lang.reflect.Method import java.lang.reflect.Method
import kotlin.reflect.KClass import kotlin.reflect.KClass
import org.oxycblt.auxio.BuildConfig
/** Assert that we are on a background thread. */ /** Assert that we are on a background thread. */
fun requireBackgroundThread() { fun requireBackgroundThread() {

View file

@ -126,8 +126,7 @@ private fun RemoteViews.applyCover(
setImageViewBitmap(R.id.widget_cover, state.cover) setImageViewBitmap(R.id.widget_cover, state.cover)
setContentDescription( setContentDescription(
R.id.widget_cover, R.id.widget_cover,
context.getString(R.string.desc_album_cover, state.song.album.resolveName(context)) context.getString(R.string.desc_album_cover, state.song.album.resolveName(context)))
)
} else { } else {
setImageViewResource(R.id.widget_cover, R.drawable.ic_remote_default_cover_24) setImageViewResource(R.id.widget_cover, R.drawable.ic_remote_default_cover_24)
setContentDescription(R.id.widget_cover, context.getString(R.string.desc_no_cover)) setContentDescription(R.id.widget_cover, context.getString(R.string.desc_no_cover))
@ -145,8 +144,7 @@ private fun RemoteViews.applyPlayPauseControls(
setOnClickPendingIntent( setOnClickPendingIntent(
R.id.widget_play_pause, R.id.widget_play_pause,
context.newBroadcastPendingIntent(PlaybackService.ACTION_PLAY_PAUSE) context.newBroadcastPendingIntent(PlaybackService.ACTION_PLAY_PAUSE))
)
// Like the Android 13 media controls, use a circular fab when paused, and a squircle fab // Like the Android 13 media controls, use a circular fab when paused, and a squircle fab
// when playing. // when playing.
@ -175,14 +173,10 @@ private fun RemoteViews.applyBasicControls(
applyPlayPauseControls(context, state) applyPlayPauseControls(context, state)
setOnClickPendingIntent( setOnClickPendingIntent(
R.id.widget_skip_prev, R.id.widget_skip_prev, context.newBroadcastPendingIntent(PlaybackService.ACTION_SKIP_PREV))
context.newBroadcastPendingIntent(PlaybackService.ACTION_SKIP_PREV)
)
setOnClickPendingIntent( setOnClickPendingIntent(
R.id.widget_skip_next, R.id.widget_skip_next, context.newBroadcastPendingIntent(PlaybackService.ACTION_SKIP_NEXT))
context.newBroadcastPendingIntent(PlaybackService.ACTION_SKIP_NEXT)
)
return this return this
} }
@ -195,13 +189,11 @@ private fun RemoteViews.applyFullControls(
setOnClickPendingIntent( setOnClickPendingIntent(
R.id.widget_repeat, R.id.widget_repeat,
context.newBroadcastPendingIntent(PlaybackService.ACTION_INC_REPEAT_MODE) context.newBroadcastPendingIntent(PlaybackService.ACTION_INC_REPEAT_MODE))
)
setOnClickPendingIntent( setOnClickPendingIntent(
R.id.widget_shuffle, R.id.widget_shuffle,
context.newBroadcastPendingIntent(PlaybackService.ACTION_INVERT_SHUFFLE) context.newBroadcastPendingIntent(PlaybackService.ACTION_INVERT_SHUFFLE))
)
val shuffleRes = val shuffleRes =
when { when {

View file

@ -22,6 +22,7 @@ import android.graphics.Bitmap
import android.os.Build import android.os.Build
import coil.request.ImageRequest import coil.request.ImageRequest
import coil.transform.RoundedCornersTransformation import coil.transform.RoundedCornersTransformation
import kotlin.math.sqrt
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.image.BitmapProvider import org.oxycblt.auxio.image.BitmapProvider
import org.oxycblt.auxio.image.extractor.SquareFrameTransform import org.oxycblt.auxio.image.extractor.SquareFrameTransform
@ -33,7 +34,6 @@ import org.oxycblt.auxio.playback.state.RepeatMode
import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.util.getDimenSize import org.oxycblt.auxio.util.getDimenSize
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import kotlin.math.sqrt
/** /**
* A wrapper around each [WidgetProvider] that plugs into the main Auxio process and updates the * A wrapper around each [WidgetProvider] that plugs into the main Auxio process and updates the
@ -109,8 +109,7 @@ class WidgetComponent(private val context: Context) :
.size(computeSize(sw, sh, 10f)) .size(computeSize(sw, sh, 10f))
.transformations( .transformations(
SquareFrameTransform.INSTANCE, SquareFrameTransform.INSTANCE,
RoundedCornersTransformation(cornerRadius.toFloat()) RoundedCornersTransformation(cornerRadius.toFloat()))
)
} else { } else {
// Divide by two to really make sure we aren't hitting the memory limit. // Divide by two to really make sure we aren't hitting the memory limit.
builder.size(computeSize(sw, sh, 2f)) builder.size(computeSize(sw, sh, 2f))
@ -121,8 +120,7 @@ class WidgetComponent(private val context: Context) :
val state = WidgetState(song, bitmap, isPlaying, repeatMode, isShuffled) val state = WidgetState(song, bitmap, isPlaying, repeatMode, isShuffled)
widget.update(context, state) widget.update(context, state)
} }
} })
)
} }
private fun computeSize(sw: Int, sh: Int, modifier: Float) = private fun computeSize(sw: Int, sh: Int, modifier: Float) =
@ -147,8 +145,7 @@ class WidgetComponent(private val context: Context) :
override fun onRepeatChanged(repeatMode: RepeatMode) = update() override fun onRepeatChanged(repeatMode: RepeatMode) = update()
override fun onSettingChanged(key: String) { override fun onSettingChanged(key: String) {
if (key == context.getString(R.string.set_key_cover_mode) || if (key == context.getString(R.string.set_key_cover_mode) ||
key == context.getString(R.string.set_key_round_mode) key == context.getString(R.string.set_key_round_mode)) {
) {
update() update()
} }
} }

View file

@ -60,8 +60,7 @@ class WidgetProvider : AppWidgetProvider() {
SizeF(180f, 152f) to createSmallWidget(context, state), SizeF(180f, 152f) to createSmallWidget(context, state),
SizeF(272f, 152f) to createWideWidget(context, state), SizeF(272f, 152f) to createWideWidget(context, state),
SizeF(180f, 272f) to createMediumWidget(context, state), SizeF(180f, 272f) to createMediumWidget(context, state),
SizeF(272f, 272f) to createLargeWidget(context, state) SizeF(272f, 272f) to createLargeWidget(context, state))
)
val awm = AppWidgetManager.getInstance(context) val awm = AppWidgetManager.getInstance(context)
@ -70,9 +69,7 @@ class WidgetProvider : AppWidgetProvider() {
} catch (e: Exception) { } catch (e: Exception) {
logW("Unable to update widget: $e") logW("Unable to update widget: $e")
awm.updateAppWidget( awm.updateAppWidget(
ComponentName(context, this::class.java), ComponentName(context, this::class.java), createDefaultWidget(context))
createDefaultWidget(context)
)
} }
} }

View file

@ -170,13 +170,13 @@
<string name="set_lib_tabs">Library tabs</string> <string name="set_lib_tabs">Library tabs</string>
<string name="set_lib_tabs_desc">Change visibility and order of library tabs</string> <string name="set_lib_tabs_desc">Change visibility and order of library tabs</string>
<string name="set_hide_collaborators">Hide collaborators</string> <string name="set_hide_collaborators">Hide collaborators</string>
<string name="set_hide_collaborators_desc">Only show artists that are directly credited on an album in the library</string> <string name="set_hide_collaborators_desc">Only show artists that are directly credited on an album (works best on well-tagged libraries)</string>
<string name="set_cover_mode">Album covers</string> <string name="set_cover_mode">Album covers</string>
<string name="set_cover_mode_off">Off</string> <string name="set_cover_mode_off">Off</string>
<string name="set_cover_mode_media_store">Fast</string> <string name="set_cover_mode_media_store">Fast</string>
<string name="set_cover_mode_quality">High quality</string> <string name="set_cover_mode_quality">High quality</string>
<string name="set_round_mode">Round mode</string> <string name="set_round_mode">Round mode</string>
<string name="set_round_mode_desc">Enable rounded corners on additional UI elements (Requires album covers to be rounded)</string> <string name="set_round_mode_desc">Enable rounded corners on additional UI elements (requires album covers to be rounded)</string>
<string name="set_bar_action">Custom playback bar action</string> <string name="set_bar_action">Custom playback bar action</string>
<!-- Skip to next (song) --> <!-- Skip to next (song) -->
<string name="set_bar_action_next">Skip to next</string> <string name="set_bar_action_next">Skip to next</string>
@ -217,13 +217,13 @@
<string name="set_restore_desc">Restore the previously saved playback state (if any)</string> <string name="set_restore_desc">Restore the previously saved playback state (if any)</string>
<string name="set_content">Content</string> <string name="set_content">Content</string>
<string name="set_reindex">Reload music</string> <string name="set_reindex">Refresh music</string>
<string name="set_reindex_desc">Reload the music library, using the cache whenever possible</string> <string name="set_reindex_desc">Reload the music library, using cached tags when possible</string>
<!-- Different from "Reload music" --> <!-- Different from "Reload music" -->
<string name="set_rescan">Rescan music</string> <string name="set_rescan">Rescan music</string>
<string name="set_rescan_desc">Reload the music library and re-create the cache (Slower, but more complete)</string> <string name="set_rescan_desc">Clear the tag cache and fully reload the music library (slower, but more complete)</string>
<string name="set_observing">Automatic reloading</string> <string name="set_observing">Automatic reloading</string>
<string name="set_observing_desc">Reload the music library whenever it changes (Requires persistent notification)</string> <string name="set_observing_desc">Reload the music library whenever it changes (requires persistent notification)</string>
<string name="set_dirs">Music folders</string> <string name="set_dirs">Music folders</string>
<string name="set_dirs_desc">Manage where music should be loaded from</string> <string name="set_dirs_desc">Manage where music should be loaded from</string>
<!-- As in the mode to be used with the music folders setting --> <!-- As in the mode to be used with the music folders setting -->