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 {
kotlin {
target "src/**/*.kt"
ktlint()
ktfmt().dropboxStyle()
licenseHeaderFile("NOTICE")
}
}

View file

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

View file

@ -22,7 +22,6 @@ import android.os.Bundle
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.app.ActivityCompat
import androidx.core.view.WindowCompat
import androidx.core.view.updatePadding
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.shape.MaterialShapeDrawable
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.music.Music
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.systemBarInsetsCompat
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
@ -89,13 +89,9 @@ class MainFragment :
// Send meaningful accessibility events for bottom sheets
ViewCompat.setAccessibilityPaneTitle(
binding.playbackSheet,
context.getString(R.string.lbl_playback)
)
binding.playbackSheet, context.getString(R.string.lbl_playback))
ViewCompat.setAccessibilityPaneTitle(
binding.queueSheet,
context.getString(R.string.lbl_queue)
)
binding.queueSheet, context.getString(R.string.lbl_queue))
val queueSheetBehavior = binding.queueSheet.coordinatorLayoutBehavior as QueueSheetBehavior?
if (queueSheetBehavior != null) {
@ -104,8 +100,7 @@ class MainFragment :
unlikelyToBeNull(binding.handleWrapper).setOnClickListener {
if (playbackSheetBehavior.state == NeoBottomSheetBehavior.STATE_EXPANDED &&
queueSheetBehavior.state == NeoBottomSheetBehavior.STATE_COLLAPSED
) {
queueSheetBehavior.state == NeoBottomSheetBehavior.STATE_COLLAPSED) {
queueSheetBehavior.state = NeoBottomSheetBehavior.STATE_EXPANDED
}
}
@ -232,8 +227,7 @@ class MainFragment :
when (action) {
is MainNavigationAction.Expand -> tryExpandAll()
is MainNavigationAction.Collapse -> tryCollapseAll()
is MainNavigationAction.Directions ->
findNavController().navigate(action.directions)
is MainNavigationAction.Directions -> findNavController().navigate(action.directions)
}
navModel.finishMainNavigation()
@ -328,16 +322,14 @@ class MainFragment :
if (queueSheetBehavior != null &&
queueSheetBehavior.state != NeoBottomSheetBehavior.STATE_COLLAPSED &&
playbackSheetBehavior.state == NeoBottomSheetBehavior.STATE_EXPANDED
) {
playbackSheetBehavior.state == NeoBottomSheetBehavior.STATE_EXPANDED) {
// Collapse the queue first if it is expanded.
queueSheetBehavior.state = NeoBottomSheetBehavior.STATE_COLLAPSED
return
}
if (playbackSheetBehavior.state != NeoBottomSheetBehavior.STATE_COLLAPSED &&
playbackSheetBehavior.state != NeoBottomSheetBehavior.STATE_HIDDEN
) {
playbackSheetBehavior.state != NeoBottomSheetBehavior.STATE_HIDDEN) {
// Then collapse the playback sheet.
playbackSheetBehavior.state = NeoBottomSheetBehavior.STATE_COLLAPSED
return

View file

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

View file

@ -26,7 +26,6 @@ import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import com.google.android.material.transition.MaterialSharedAxis
import org.oxycblt.auxio.MainFragmentDirections
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentDetailBinding
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.picker.PickerMode
import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.ui.MainNavigationAction
import org.oxycblt.auxio.ui.fragment.MusicFragment
import org.oxycblt.auxio.ui.recycler.Item
import org.oxycblt.auxio.util.collect
@ -55,7 +53,9 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
* @author OxygenCobalt
*/
class ArtistDetailFragment :
MusicFragment<FragmentDetailBinding>(), Toolbar.OnMenuItemClickListener, DetailAdapter.Listener {
MusicFragment<FragmentDetailBinding>(),
Toolbar.OnMenuItemClickListener,
DetailAdapter.Listener {
private val detailModel: DetailViewModel by activityViewModels()
private val args: ArtistDetailFragmentArgs by navArgs()
@ -88,11 +88,7 @@ class ArtistDetailFragment :
collectImmediately(detailModel.currentArtist, ::handleItemChange)
collectImmediately(detailModel.artistData, detailAdapter::submitList)
collectImmediately(
playbackModel.song,
playbackModel.parent,
playbackModel.isPlaying,
::updatePlayback
)
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
collect(navModel.exploreNavigationItem, ::handleNavigation)
}
@ -122,7 +118,9 @@ class ArtistDetailFragment :
when (item) {
is Song -> {
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.ALBUMS -> playbackModel.playFromAlbum(item)
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.RecyclerView
import com.google.android.material.appbar.AppBarLayout
import java.lang.reflect.Field
import org.oxycblt.auxio.R
import org.oxycblt.auxio.ui.AuxioAppBarLayout
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

View file

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

View file

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

View file

@ -74,16 +74,14 @@ class SongDetailDialog : ViewBindingDialogFragment<DialogSongDetailBinding>() {
if (song.info.bitrateKbps != null) {
binding.detailBitrate.setText(
getString(R.string.fmt_bitrate, song.info.bitrateKbps)
)
getString(R.string.fmt_bitrate, song.info.bitrateKbps))
} else {
binding.detailBitrate.setText(R.string.def_bitrate)
}
if (song.info.sampleRate != null) {
binding.detailSampleRate.setText(
getString(R.string.fmt_sample_rate, song.info.sampleRate)
)
getString(R.string.fmt_sample_rate, song.info.sampleRate))
} else {
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 {
val date =
item.date?.resolveDate(context)
?: context.getString(R.string.def_date)
val date = item.date?.resolveDate(context) ?: context.getString(R.string.def_date)
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(
R.string.fmt_two,
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.detailShuffleButton.isVisible = true
@ -143,7 +142,8 @@ private class ArtistDetailViewHolder private constructor(private val binding: It
fun new(parent: View) =
ArtistDetailViewHolder(ItemDetailBinding.inflate(parent.context.inflater))
val DIFFER = object : SimpleItemCallback<Artist>() {
val DIFFER =
object : SimpleItemCallback<Artist>() {
override fun areContentsTheSame(oldItem: Artist, newItem: Artist) =
oldItem.rawName == newItem.rawName &&
oldItem.areGenreContentsTheSame(newItem) &&
@ -159,8 +159,7 @@ private class ArtistAlbumViewHolder private constructor(private val binding: Ite
binding.parentImage.bind(item)
binding.parentName.text = item.resolveName(binding.context)
binding.parentInfo.text =
item.date?.resolveDate(binding.context)
?: binding.context.getString(R.string.def_date)
item.date?.resolveDate(binding.context) ?: binding.context.getString(R.string.def_date)
// binding.parentMenu.setOnClickListener { listener.onOpenMenu(item, it) }
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.util.context
import org.oxycblt.auxio.util.inflater
import org.oxycblt.auxio.util.logD
abstract class DetailAdapter<L : DetailAdapter.Listener>(
private val listener: L,
@ -43,8 +42,7 @@ abstract class DetailAdapter<L : DetailAdapter.Listener>(
) : IndicatorAdapter<RecyclerView.ViewHolder>(), AuxioRecyclerView.SpanSizeLookup {
private var isPlaying = false
@Suppress("LeakingThis")
override fun getItemCount() = differ.currentList.size
@Suppress("LeakingThis") override fun getItemCount() = differ.currentList.size
override fun getItemViewType(position: Int) =
when (differ.currentList[position]) {
@ -85,8 +83,7 @@ abstract class DetailAdapter<L : DetailAdapter.Listener>(
return item is Header || item is SortHeader
}
@Suppress("LeakingThis")
protected val differ = AsyncListDiffer(this, diffCallback)
@Suppress("LeakingThis") protected val differ = AsyncListDiffer(this, diffCallback)
override val currentList: List<Item>
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.Genre
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.Header
import org.oxycblt.auxio.ui.recycler.Item
import org.oxycblt.auxio.ui.recycler.SimpleItemCallback
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.detailName.text = item.resolveName(binding.context)
binding.detailSubhead.isVisible = false
binding.detailInfo.text = binding.context.getString(
binding.detailInfo.text =
binding.context.getString(
R.string.fmt_two,
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.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.tabs.TabLayoutMediator
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.MainFragmentDirections
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.lazyReflectedField
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
@ -112,8 +112,7 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
binding.homeToolbar.alpha = 1f - (abs(offset.toFloat()) / (range.toFloat() / 2))
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() {
override fun onPageSelected(position: Int) =
homeModel.updateCurrentTab(position)
}
)
})
TabLayoutMediator(binding.homeTabs, this, AdaptiveTabStrategy(context, homeModel))
.attach()
@ -186,11 +184,13 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
}
R.id.action_settings -> {
logD("Navigating to settings")
navModel.mainNavigateTo(MainNavigationAction.Directions(MainFragmentDirections.actionShowSettings()))
navModel.mainNavigateTo(
MainNavigationAction.Directions(MainFragmentDirections.actionShowSettings()))
}
R.id.action_about -> {
logD("Navigating to about")
navModel.mainNavigateTo(MainNavigationAction.Directions(MainFragmentDirections.actionShowAbout()))
navModel.mainNavigateTo(
MainNavigationAction.Directions(MainFragmentDirections.actionShowAbout()))
}
R.id.submenu_sorting -> {
// Junk click event when opening the menu
@ -200,8 +200,7 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
homeModel.updateCurrentSort(
homeModel
.getSortForTab(homeModel.currentTab.value)
.withAscending(item.isChecked)
)
.withAscending(item.isChecked))
}
else -> {
// 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
.getSortForTab(homeModel.currentTab.value)
.withMode(requireNotNull(Sort.Mode.fromItemId(item.itemId)))
)
.withMode(requireNotNull(Sort.Mode.fromItemId(item.itemId))))
}
}

View file

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

View file

@ -21,6 +21,7 @@ import android.os.Bundle
import android.text.format.DateUtils
import android.view.View
import android.view.ViewGroup
import java.util.Formatter
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentHomeListBinding
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.SyncListDiffer
import org.oxycblt.auxio.util.collectImmediately
import java.util.Formatter
/**
* 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() }
// 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
is Sort.Mode.ByDate -> album.date?.resolveDate(requireContext())
@ -87,8 +88,7 @@ class AlbumListFragment : HomeListFragment<Album>() {
formatter,
dateAddedMillis,
dateAddedMillis,
DateUtils.FORMAT_ABBREV_ALL
)
DateUtils.FORMAT_ABBREV_ALL)
.toString()
}

View file

@ -21,7 +21,7 @@ import android.os.Bundle
import android.text.format.DateUtils
import android.view.View
import android.view.ViewGroup
import org.oxycblt.auxio.MainFragmentDirections
import java.util.Formatter
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentHomeListBinding
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.secsToMs
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.Item
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.util.collectImmediately
import org.oxycblt.auxio.util.context
import java.util.Formatter
/**
* A [HomeListFragment] for showing a list of [Song]s.
@ -62,11 +60,7 @@ class SongListFragment : HomeListFragment<Song>() {
collectImmediately(homeModel.songs, homeAdapter::replaceList)
collectImmediately(
playbackModel.song,
playbackModel.parent,
playbackModel.isPlaying,
::handlePlayback
)
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::handlePlayback)
}
override fun getPopup(pos: Int): String? {
@ -80,10 +74,12 @@ class SongListFragment : HomeListFragment<Song>() {
is Sort.Mode.ByName -> song.collationKey?.run { sourceString.first().uppercase() }
// 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
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
is Sort.Mode.ByDate -> song.album.date?.resolveDate(requireContext())
@ -100,8 +96,7 @@ class SongListFragment : HomeListFragment<Song>() {
formatter,
dateAddedMillis,
dateAddedMillis,
DateUtils.FORMAT_ABBREV_ALL
)
DateUtils.FORMAT_ABBREV_ALL)
.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.
*/
private val MODE_TABLE =
arrayOf(
MusicMode.SONGS,
MusicMode.ALBUMS,
MusicMode.ARTISTS,
MusicMode.GENRES
)
arrayOf(MusicMode.SONGS, MusicMode.ALBUMS, MusicMode.ARTISTS, MusicMode.GENRES)
/** Convert an array [tabs] into a sequence of tabs. */
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.ARTISTS -> R.string.lbl_artists
MusicMode.GENRES -> R.string.lbl_genres
}
)
})
isChecked = item is Tab.Visible
}

View file

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

View file

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

View file

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

View file

@ -26,11 +26,11 @@ import androidx.annotation.AttrRes
import androidx.appcompat.widget.AppCompatImageView
import androidx.core.widget.ImageViewCompat
import com.google.android.material.shape.MaterialShapeDrawable
import kotlin.math.max
import org.oxycblt.auxio.R
import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.util.getColorCompat
import org.oxycblt.auxio.util.getDrawableCompat
import kotlin.math.max
/**
* 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,
drawable.intrinsicWidth.toFloat(),
drawable.intrinsicHeight.toFloat()
)
drawable.intrinsicHeight.toFloat())
indicatorMatrixDst.set(0f, 0f, iconSize.toFloat(), iconSize.toFloat())
indicatorMatrix.setRectToRect(
indicatorMatrixSrc,
indicatorMatrixDst,
Matrix.ScaleToFit.CENTER
)
indicatorMatrixSrc, indicatorMatrixDst, Matrix.ScaleToFit.CENTER)
// Then actually center it into the icon, which the previous call does not
// actually do.
indicatorMatrix.postTranslate(
(measuredWidth - iconSize) / 2f,
(measuredHeight - iconSize) / 2f
)
(measuredWidth - 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 staticIcon =
styledAttrs.getResourceId(
R.styleable.StyledImageView_staticIcon,
ResourcesCompat.ID_NULL
)
R.styleable.StyledImageView_staticIcon, ResourcesCompat.ID_NULL)
if (staticIcon != ResourcesCompat.ID_NULL) {
this.staticIcon = context.getDrawableCompat(staticIcon)
}
@ -146,8 +144,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
adjustWidth,
adjustHeight,
bounds.width() - adjustWidth,
bounds.height() - adjustHeight
)
bounds.height() - adjustHeight)
src.draw(canvas)
}

View file

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

View file

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

View file

@ -21,6 +21,11 @@ package org.oxycblt.auxio.music
import android.content.Context
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.Parcelize
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.util.nonZeroOrNull
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 ---
@ -55,14 +55,14 @@ sealed class Music : Item {
abstract val rawSortName: String?
/**
* A key used by the sorting system that takes into account the sort tags of this item,
* any (english) articles that prefix the names, and collation rules.
* A key used by the sorting system that takes into account the sort tags of this item, any
* (english) articles that prefix the names, and collation rules.
*/
abstract val collationKey: CollationKey?
/**
* Resolve a name from it's raw form to a form suitable to be shown in a UI.
* Null values will be resolved into their string form with this function.
* Resolve a name from it's raw form to a form suitable to be shown in a UI. Null values will be
* resolved into their string form with this function.
*/
abstract fun resolveName(context: Context): String
@ -75,11 +75,12 @@ sealed class Music : Item {
other is Music && javaClass == other.javaClass && uid == other.uid
/**
* Workaround to allow for easy collation key generation in the initializer without
* base-class initialization issues or slow lazy initialization.
* Workaround to allow for easy collation key generation in the initializer without base-class
* initialization issues or slow lazy initialization.
*/
protected fun makeCollationKeyImpl(): CollationKey? {
val sortName = (rawSortName ?: rawName)?.run {
val sortName =
(rawSortName ?: rawName)?.run {
when {
length > 5 && startsWith("the ", ignoreCase = true) -> substring(4)
length > 4 && startsWith("an ", ignoreCase = true) -> substring(3)
@ -92,9 +93,9 @@ sealed class Music : Item {
}
/**
* Called when the library has been linked and validation/construction steps dependent
* on linked items should run. It's also used to do last-step initialization of fields
* that require any parent values that would not be present during startup.
* Called when the library has been linked and validation/construction steps dependent on linked
* items should run. It's also used to do last-step initialization of fields that require any
* parent values that would not be present during startup.
*/
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
* information about the relationships between items.
*
* Note: While the core of a UID is a UUID. The whole is not technically a UUID, with
* string representation in particular having multiple extensions to increase uniqueness.
* Please don't try to do anything interesting with this and just assume it's a black box
* that can only be compared, serialized, and deserialized.
* Note: While the core of a UID is a UUID. The whole is not technically a UUID, with string
* representation in particular having multiple extensions to increase uniqueness. Please don't
* try to do anything interesting with this and just assume it's a black box that can only be
* compared, serialized, and deserialized.
*
* @author OxygenCobalt
*/
@Parcelize
class UID private constructor(
class UID
private constructor(
private val format: Format,
private val mode: MusicMode,
private val uuid: UUID
@ -130,9 +132,8 @@ sealed class Music : Item {
override fun hashCode() = hashCode
override fun equals(other: Any?) = other is UID &&
format == other.format &&
mode == other.mode && uuid == other.uuid
override fun equals(other: Any?) =
other is UID && format == other.format && mode == other.mode && uuid == other.uuid
// UID string format is roughly:
// format_namespace:music_mode_int-uuid
@ -151,7 +152,8 @@ sealed class Music : Item {
return null
}
val format = when (split[0]) {
val format =
when (split[0]) {
Format.AUXIO.namespace -> Format.AUXIO
Format.MUSICBRAINZ.namespace -> Format.MUSICBRAINZ
else -> return null
@ -168,9 +170,7 @@ sealed class Music : Item {
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 {
// 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
@ -181,18 +181,13 @@ sealed class Music : Item {
return UID(Format.AUXIO, mode, uuid)
}
/**
* Make a UUID derived from a MusicBrainz ID.
*/
fun musicBrainz(mode: MusicMode, uuid: UUID): UID =
UID(Format.MUSICBRAINZ, mode, uuid)
/** Make a UUID derived from a MusicBrainz ID. */
fun musicBrainz(mode: MusicMode, uuid: UUID): UID = UID(Format.MUSICBRAINZ, mode, uuid)
}
}
companion object {
private val COLLATOR = Collator.getInstance().apply {
strength = Collator.PRIMARY
}
private val COLLATOR = Collator.getInstance().apply { strength = Collator.PRIMARY }
}
}
@ -207,7 +202,10 @@ sealed class MusicParent : Music() {
override fun hashCode() = 31 * uid.hashCode() + songs.hashCode()
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,7 +213,8 @@ sealed class MusicParent : Music() {
* @author OxygenCobalt
*/
class Song constructor(raw: Raw, settings: Settings) : Music() {
override val uid = raw.musicBrainzId?.toUuidOrNull()?.let { UID.musicBrainz(MusicMode.SONGS, it) }
override val uid =
raw.musicBrainzId?.toUuidOrNull()?.let { UID.musicBrainz(MusicMode.SONGS, it) }
?: UID.auxio(MusicMode.SONGS) {
// Song UIDs are based on the raw data without parsing so that they remain
// consistent across music setting changes. Parents are not held up to the
@ -258,15 +257,13 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
val path =
Path(
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. */
val mimeType =
MimeType(
fromExtension = requireNotNull(raw.extensionMimeType) { "Invalid raw: No mime type" },
fromFormat = raw.formatMimeType
)
fromFormat = raw.formatMimeType)
/** The size of this audio file. */
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
/**
* The album of this song. Every song is guaranteed to have one and only one album,
* with a "directory" album being used if no album tag can be found.
* The album of this song. Every song is guaranteed to have one and only one album, with a
* "directory" album being used if no album tag can be found.
*/
val album: 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 rawArtists = artistNames.mapIndexed { i, name ->
Artist.Raw(artistMusicBrainzIds.getOrNull(i)?.toUuidOrNull(), name, artistSortNames.getOrNull(i))
private val rawArtists =
artistNames.mapIndexed { i, name ->
Artist.Raw(
artistMusicBrainzIds.getOrNull(i)?.toUuidOrNull(),
name,
artistSortNames.getOrNull(i))
}
private val rawAlbumArtists = albumArtistNames.mapIndexed { i, name ->
Artist.Raw(albumArtistMusicBrainzIds.getOrNull(i)?.toUuidOrNull(), name, albumArtistSortNames.getOrNull(i))
private val rawAlbumArtists =
albumArtistNames.mapIndexed { i, name ->
Artist.Raw(
albumArtistMusicBrainzIds.getOrNull(i)?.toUuidOrNull(),
name,
albumArtistSortNames.getOrNull(i))
}
private val _artists = mutableListOf<Artist>()
/**
* The artists of this song. Most often one, but there could be multiple. These artists
* are derived from the artists tag and not the album artists tag, so they may differ from
* the artists of the album.
* The artists of this song. Most often one, but there could be multiple. These artists are
* derived from the artists tag and not the album artists tag, so they may differ from the
* artists of the album.
*/
val artists: List<Artist>
get() = _artists
/**
* Resolve the artists of this song into a human-readable name. First tries to use artist
* tags, then falls back to album artist tags.
* Resolve the artists of this song into a human-readable name. First tries to use artist tags,
* then falls back to album artist tags.
*/
fun resolveArtistContents(context: Context) =
artists.joinToString { it.resolveName(context) }
fun resolveArtistContents(context: Context) = artists.joinToString { it.resolveName(context) }
/**
* Utility method for recyclerview diffing that checks if resolveArtistContents is the
* same without a context.
* Utility method for recyclerview diffing that checks if resolveArtistContents is the same
* without a context.
*/
fun areArtistContentsTheSame(other: Song): Boolean {
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>
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) }
// --- INTERNAL FIELDS ---
val _rawGenres = raw.genreNames.parseId3GenreNames(settings)
.map { Genre.Raw(it) }.ifEmpty { listOf(Genre.Raw()) }
val _rawGenres =
raw.genreNames
.parseId3GenreNames(settings)
.map { Genre.Raw(it) }
.ifEmpty { listOf(Genre.Raw()) }
val _rawArtists = rawArtists.ifEmpty { rawAlbumArtists }.ifEmpty {
listOf(Artist.Raw())
}
val _rawArtists = rawArtists.ifEmpty { rawAlbumArtists }.ifEmpty { listOf(Artist.Raw()) }
val _rawAlbum =
Album.Raw(
@ -369,8 +372,8 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
name = requireNotNull(raw.albumName) { "Invalid raw: No album name" },
sortName = raw.albumSortName,
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) {
_album = album
@ -445,7 +448,8 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
* @author OxygenCobalt
*/
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 =
raw.musicBrainzId?.let { UID.musicBrainz(MusicMode.ALBUMS, it) }
?: UID.auxio(MusicMode.ALBUMS) {
// Hash based on only names despite the presence of a date to increase stability.
// I don't know if there is any situation where an artist will have two albums with
@ -481,21 +485,19 @@ class Album constructor(raw: Raw, override val songs: List<Song>) : MusicParent(
val dateAdded: Long
/**
* The artists of this album. Usually one, but there may be more. These are derived from
* the album artist first, so they may differ from the song artists.
* The artists of this album. Usually one, but there may be more. These are derived from the
* album artist first, so they may differ from the song artists.
*/
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.
*/
fun resolveArtistContents(context: Context) =
artists.joinToString { it.resolveName(context) }
/**
* Utility for RecyclerView differs to check if resolveArtistContents is the same without
* a context.
* Utility for RecyclerView differs to check if resolveArtistContents is the same without a
* context.
*/
fun areArtistContentsTheSame(other: Album): Boolean {
for (i in 0 until max(artists.size, other.artists.size)) {
@ -572,9 +574,9 @@ class Album constructor(raw: Raw, override val songs: List<Song>) : MusicParent(
override fun equals(other: Any?): Boolean {
if (other !is Raw) return false
if (musicBrainzId != null && other.musicBrainzId != null &&
musicBrainzId == other.musicBrainzId
) {
if (musicBrainzId != null &&
other.musicBrainzId != null &&
musicBrainzId == other.musicBrainzId) {
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
* albums and songs respectively.
* An abstract artist. This is derived from both album artist values and artist values in albums and
* songs respectively.
* @author OxygenCobalt
*/
class Artist
constructor(private val raw: Raw, songAlbums: List<Music>) : MusicParent() {
override val uid = raw.musicBrainzId?.let { UID.musicBrainz(MusicMode.ARTISTS, it) } ?: UID.auxio(
MusicMode.ARTISTS
) { update(raw.name) }
class Artist constructor(private val raw: Raw, songAlbums: List<Music>) : MusicParent() {
override val uid =
raw.musicBrainzId?.let { UID.musicBrainz(MusicMode.ARTISTS, it) }
?: UID.auxio(MusicMode.ARTISTS) { update(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)
/**
* The songs of this artist. This might be empty.
*/
/** The songs of this artist. This might be empty. */
override val songs: List<Song>
/** 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>
/**
* 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) }
/**
* Utility for RecyclerView differs to check if resolveGenreContents is the same without
* a context.
* Utility for RecyclerView differs to check if resolveGenreContents is the same without a
* context.
*/
fun areGenreContentsTheSame(other: Artist): Boolean {
for (i in 0 until max(genres.size, other.genres.size)) {
@ -639,7 +636,6 @@ constructor(private val raw: Raw, songAlbums: List<Music>) : MusicParent() {
return true
}
init {
val distinctSongs = mutableSetOf<Song>()
val distinctAlbums = mutableSetOf<Album>()
@ -653,13 +649,11 @@ constructor(private val raw: Raw, songAlbums: List<Music>) : MusicParent() {
distinctSongs.add(music)
distinctAlbums.add(music.album)
}
is Album -> {
music._link(this)
distinctAlbums.add(music)
noAlbums = false
}
else -> error("Unexpected input music ${music::class.simpleName}")
}
}
@ -677,11 +671,17 @@ constructor(private val raw: Raw, songAlbums: List<Music>) : MusicParent() {
override fun _finalize() {
check(songs.isNotEmpty() || albums.isNotEmpty()) { "Malformed artist: Empty" }
genres = Sort(Sort.Mode.ByName, true).genres(songs.flatMapTo(mutableSetOf()) { it.genres })
genres =
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()
override fun hashCode() = hashCode
@ -689,9 +689,9 @@ constructor(private val raw: Raw, songAlbums: List<Music>) : MusicParent() {
override fun equals(other: Any?): Boolean {
if (other !is Raw) return false
if (musicBrainzId != null && other.musicBrainzId != null &&
musicBrainzId == other.musicBrainzId
) {
if (musicBrainzId != null &&
other.musicBrainzId != null &&
musicBrainzId == other.musicBrainzId) {
return true
}
@ -743,8 +743,8 @@ class Genre constructor(private val raw: Raw, override val songs: List<Song>) :
durationMs = totalDuration
albums = Sort(Sort.Mode.ByName, true).albums(distinctAlbums)
.sortedByDescending { album ->
albums =
Sort(Sort.Mode.ByName, true).albums(distinctAlbums).sortedByDescending { album ->
album.songs.count { it.genres.contains(this) }
}
@ -819,9 +819,7 @@ fun MessageDigest.update(n: Long?) {
n.shr(32).toByte(),
n.shr(40).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(13).toLong().and(0xFF).shl(16))
.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
* not [T], null will be returned.
* Find a music [T] by its [uid]. If the music does not exist, or if the music is not [T],
* null will be returned.
*/
@Suppress("UNCHECKED_CAST")
fun <T : Music> find(uid: Music.UID) = uidMap[uid] as? T
@Suppress("UNCHECKED_CAST") fun <T : Music> find(uid: Music.UID) = uidMap[uid] as? T
/** Sanitize an old item to find the corresponding item in a new library. */
fun sanitize(song: Song) = find<Song>(song.uid)
@ -122,8 +121,8 @@ class MusicStore private constructor() {
/** Find a song for a [uri]. */
fun findSongForUri(context: Context, uri: Uri) =
context.contentResolverSafe.useQuery(uri, arrayOf(OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE)) {
cursor ->
context.contentResolverSafe.useQuery(
uri, arrayOf(OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE)) { cursor ->
cursor.moveToFirst()
// We are weirdly limited to DISPLAY_NAME and SIZE when trying to locate a
@ -132,9 +131,7 @@ class MusicStore private constructor() {
val displayName =
cursor.getString(cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME))
val size = cursor.getLong(
cursor.getColumnIndexOrThrow(OpenableColumns.SIZE)
)
val size = cursor.getLong(cursor.getColumnIndexOrThrow(OpenableColumns.SIZE))
songs.find { it.path.name == displayName && it.size == size }
}

View file

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

View file

@ -18,10 +18,10 @@
package org.oxycblt.auxio.music
import androidx.annotation.IdRes
import kotlin.math.max
import org.oxycblt.auxio.IntegerTable
import org.oxycblt.auxio.R
import org.oxycblt.auxio.music.Sort.Mode
import kotlin.math.max
/**
* 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))
}
fun artistsInPlace(artists: MutableList<Artist>) {
private fun artistsInPlace(artists: MutableList<Artist>) {
artists.sortWith(mode.getArtistComparator(isAscending))
}
fun genresInPlace(genres: MutableList<Genre>) {
private fun genresInPlace(genres: MutableList<Genre>) {
genres.sortWith(mode.getGenreComparator(isAscending))
}
@ -141,8 +141,7 @@ data class Sort(val mode: Mode, val isAscending: Boolean) {
compareByDynamic(ascending, BasicComparator.ALBUM) { it.album },
compareBy(NullableComparator.INT) { it.disc },
compareBy(NullableComparator.INT) { it.track },
compareBy(BasicComparator.SONG)
)
compareBy(BasicComparator.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 },
compareBy(NullableComparator.INT) { it.disc },
compareBy(NullableComparator.INT) { it.track },
compareBy(BasicComparator.SONG)
)
compareBy(BasicComparator.SONG))
override fun getAlbumComparator(ascending: Boolean): Comparator<Album> =
MultiComparator(
compareByDynamic(ascending, ListComparator.ARTISTS) { it.artists },
compareByDescending(NullableComparator.DATE) { it.date },
compareBy(BasicComparator.ALBUM)
)
compareBy(BasicComparator.ALBUM))
}
/** 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 },
compareBy(NullableComparator.INT) { it.disc },
compareBy(NullableComparator.INT) { it.track },
compareBy(BasicComparator.SONG)
)
compareBy(BasicComparator.SONG))
override fun getAlbumComparator(ascending: Boolean): Comparator<Album> =
MultiComparator(
compareByDynamic(ascending, NullableComparator.DATE) { it.date },
compareBy(BasicComparator.ALBUM)
)
compareBy(BasicComparator.ALBUM))
}
/** 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> =
MultiComparator(
compareByDynamic(ascending) { it.durationMs },
compareBy(BasicComparator.SONG)
)
compareByDynamic(ascending) { it.durationMs }, compareBy(BasicComparator.SONG))
override fun getAlbumComparator(ascending: Boolean): Comparator<Album> =
MultiComparator(
compareByDynamic(ascending) { it.durationMs },
compareBy(BasicComparator.ALBUM)
)
compareByDynamic(ascending) { it.durationMs }, compareBy(BasicComparator.ALBUM))
override fun getArtistComparator(ascending: Boolean): Comparator<Artist> =
MultiComparator(
compareByDynamic(ascending, NullableComparator.LONG) { it.durationMs },
compareBy(BasicComparator.ARTIST)
)
compareBy(BasicComparator.ARTIST))
override fun getGenreComparator(ascending: Boolean): Comparator<Genre> =
MultiComparator(
compareByDynamic(ascending) { it.durationMs },
compareBy(BasicComparator.GENRE)
)
compareByDynamic(ascending) { it.durationMs }, compareBy(BasicComparator.GENRE))
}
/** 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> =
MultiComparator(
compareByDynamic(ascending) { it.songs.size },
compareBy(BasicComparator.ALBUM)
)
compareByDynamic(ascending) { it.songs.size }, compareBy(BasicComparator.ALBUM))
override fun getArtistComparator(ascending: Boolean): Comparator<Artist> =
MultiComparator(
compareByDynamic(ascending, NullableComparator.INT) { it.songs.size },
compareBy(BasicComparator.ARTIST)
)
compareBy(BasicComparator.ARTIST))
override fun getGenreComparator(ascending: Boolean): Comparator<Genre> =
MultiComparator(
compareByDynamic(ascending) { it.songs.size },
compareBy(BasicComparator.GENRE)
)
compareByDynamic(ascending) { it.songs.size }, compareBy(BasicComparator.GENRE))
}
/** 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(
compareByDynamic(ascending, NullableComparator.INT) { it.disc },
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(
compareBy(NullableComparator.INT) { it.disc },
compareByDynamic(ascending, NullableComparator.INT) { it.track },
compareBy(BasicComparator.SONG)
)
compareBy(BasicComparator.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> =
MultiComparator(
compareByDynamic(ascending) { it.dateAdded },
compareBy(BasicComparator.SONG)
)
compareByDynamic(ascending) { it.dateAdded }, compareBy(BasicComparator.SONG))
override fun getAlbumComparator(ascending: Boolean): Comparator<Album> =
MultiComparator(
compareByDynamic(ascending) { album -> album.songs.minOf { it.dateAdded } },
compareBy(BasicComparator.ALBUM)
)
compareBy(BasicComparator.ALBUM))
}
protected inline fun <T : Music, K> compareByDynamic(

View file

@ -19,22 +19,18 @@ package org.oxycblt.auxio.music
import android.content.Context
import android.os.Build
import android.text.format.DateUtils
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.format.DateTimeFormatter
import java.time.temporal.TemporalQueries
import java.util.Formatter
import java.util.Locale
import kotlin.math.max
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.
@ -81,9 +77,8 @@ class Date private constructor(private val tokens: List<Int>) : Comparable<Date>
private val second = tokens.getOrNull(5)
/**
* Resolve this date into a string. This could result in a year string formatted
* as "YYYY", or a month and year string formatted as "MMM YYYY" depending on the
* situation.
* Resolve this date into a string. This could result in a year string formatted as "YYYY", or a
* month and year string formatted as "MMM YYYY" depending on the situation.
*/
fun resolveDate(context: Context): String {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@ -102,16 +97,16 @@ class Date private constructor(private val tokens: List<Int>) : Comparable<Date>
@RequiresApi(Build.VERSION_CODES.O)
private fun resolveFullDate(context: Context) =
if (month != null) {
val temporal = DateTimeFormatter.ISO_DATE.parse(
"$year-$month-${day ?: 1}",
TemporalQueries.localDate()
)
val temporal =
DateTimeFormatter.ISO_DATE.parse(
"$year-$month-${day ?: 1}", TemporalQueries.localDate())
// 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
// 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.
temporal.atStartOfDay(ZoneId.systemDefault())
temporal
.atStartOfDay(ZoneId.systemDefault())
.format(DateTimeFormatter.ofPattern("MMM yyyy", Locale.getDefault()))
} else {
resolveYear(context)
@ -161,8 +156,7 @@ class Date private constructor(private val tokens: List<Int>) : Comparable<Date>
companion object {
private val ISO8601_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))
@ -246,7 +240,8 @@ sealed class ReleaseType {
data class Compilation(override val refinement: Refinement?) : ReleaseType() {
override val stringRes: Int
get() = when (refinement) {
get() =
when (refinement) {
null -> R.string.lbl_compilation
Refinement.LIVE -> R.string.lbl_compilation_live
Refinement.REMIX -> R.string.lbl_compilation_remix
@ -325,7 +320,8 @@ sealed class ReleaseType {
private inline fun parseSecondaryTypeImpl(
type: String?,
convertRefinement: (Refinement?) -> ReleaseType
) = when {
) =
when {
// Parse all the types that have no children
type.equals("soundtrack", true) -> Soundtrack
type.equals("mixtape/street", true) -> Mixtape

View file

@ -24,18 +24,18 @@ import android.database.sqlite.SQLiteOpenHelper
import androidx.core.database.getIntOrNull
import androidx.core.database.getStringOrNull
import androidx.core.database.sqlite.transaction
import java.io.File
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logE
import org.oxycblt.auxio.util.queryAll
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
* storing "intrinsic" data, as in information derived from the file format and not
* information from the media database or file system. The exceptions are the database ID and
* modification times for files, as these are required for the cache to function well.
* storing "intrinsic" data, as in information derived from the file format and not information from
* the media database or file system. The exceptions are the database ID and modification times for
* files, as these are required for the cache to function well.
* @author OxygenCobalt
*/
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>) {
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
* since it was last saved. Returns true if a song was loaded.
* Maybe copy a cached raw song into this instance, assuming that it has not changed since it
* was last saved. Returns true if a song was loaded.
*/
fun populateFromCache(rawSong: Song.Raw): Boolean {
val map = cacheMap ?: return false
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.name = cachedRawSong.name
rawSong.sortName = cachedRawSong.sortName
@ -118,9 +118,11 @@ 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) {
val command = StringBuilder()
val command =
StringBuilder()
.append("CREATE TABLE IF NOT EXISTS $TABLE_RAW_SONGS(")
.append("${Columns.MEDIA_STORE_ID} LONG PRIMARY KEY,")
.append("${Columns.DATE_ADDED} LONG NOT NULL,")
@ -185,18 +187,22 @@ private class CacheDatabase(context: Context) : SQLiteOpenHelper(context, File(c
val discIndex = cursor.getColumnIndexOrThrow(Columns.DISC)
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 albumSortNameIndex = cursor.getColumnIndexOrThrow(Columns.ALBUM_SORT_NAME)
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 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 albumArtistSortNamesIndex = cursor.getColumnIndexOrThrow(Columns.ALBUM_ARTIST_SORT_NAMES)
val albumArtistSortNamesIndex =
cursor.getColumnIndexOrThrow(Columns.ALBUM_ARTIST_SORT_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.albumName = cursor.getString(albumNameIndex)
raw.albumSortName = cursor.getStringOrNull(albumSortNameIndex)
cursor.getStringOrNull(albumReleaseTypesIndex)?.parseMultiValue()
?.let { raw.albumReleaseTypes = it }
cursor.getStringOrNull(albumReleaseTypesIndex)?.parseMultiValue()?.let {
raw.albumReleaseTypes = it
}
cursor.getStringOrNull(artistMusicBrainzIdsIndex)?.let {
raw.artistMusicBrainzIds = it.parseMultiValue()
}
cursor.getStringOrNull(artistNamesIndex)
?.let { raw.artistNames = it.parseMultiValue() }
cursor.getStringOrNull(artistSortNamesIndex)
?.let { raw.artistSortNames = it.parseMultiValue() }
cursor.getStringOrNull(artistNamesIndex)?.let {
raw.artistNames = it.parseMultiValue()
}
cursor.getStringOrNull(artistSortNamesIndex)?.let {
raw.artistSortNames = it.parseMultiValue()
}
cursor.getStringOrNull(albumArtistMusicBrainzIdsIndex)
?.let { raw.albumArtistMusicBrainzIds = it.parseMultiValue() }
cursor.getStringOrNull(albumArtistNamesIndex)
?.let { raw.albumArtistNames = it.parseMultiValue() }
cursor.getStringOrNull(albumArtistSortNamesIndex)
?.let { raw.albumArtistSortNames = it.parseMultiValue() }
cursor.getStringOrNull(albumArtistMusicBrainzIdsIndex)?.let {
raw.albumArtistMusicBrainzIds = it.parseMultiValue()
}
cursor.getStringOrNull(albumArtistNamesIndex)?.let {
raw.albumArtistNames = it.parseMultiValue()
}
cursor.getStringOrNull(albumArtistSortNamesIndex)?.let {
raw.albumArtistSortNames = it.parseMultiValue()
}
cursor.getStringOrNull(genresIndex)
?.let { raw.genreNames = it.parseMultiValue() }
cursor.getStringOrNull(genresIndex)?.let { raw.genreNames = it.parseMultiValue() }
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_NAME, rawSong.albumName)
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_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_SORT_NAMES, rawSong.albumArtistSortNames.toMultiValue())
put(
Columns.ALBUM_ARTIST_SORT_NAMES,
rawSong.albumArtistSortNames.toMultiValue())
put(Columns.GENRE_NAMES, rawSong.genreNames.toMultiValue())
}

View file

@ -26,6 +26,7 @@ import android.provider.MediaStore
import androidx.annotation.RequiresApi
import androidx.core.database.getIntOrNull
import androidx.core.database.getStringOrNull
import java.io.File
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.storage.Directory
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.getSystemServiceCompat
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
@ -100,7 +100,10 @@ import java.io.File
* music loading process.
* @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 idIndex = -1
@ -177,9 +180,7 @@ abstract class MediaStoreExtractor(private val context: Context, private val cac
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
projection,
selector,
args.toTypedArray()
)
) { "Content resolver failure: No Cursor returned" }
args.toTypedArray())) { "Content resolver failure: No Cursor returned" }
.also { cursor = it }
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.
context.contentResolverSafe.useQuery(
MediaStore.Audio.Genres.EXTERNAL_CONTENT_URI,
arrayOf(MediaStore.Audio.Genres._ID, MediaStore.Audio.Genres.NAME)
) { genreCursor ->
arrayOf(MediaStore.Audio.Genres._ID, MediaStore.Audio.Genres.NAME)) { genreCursor ->
val idIndex = genreCursor.getColumnIndexOrThrow(MediaStore.Audio.Genres._ID)
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(
MediaStore.Audio.Genres.Members.getContentUri(VOLUME_EXTERNAL, id),
arrayOf(MediaStore.Audio.Genres.Members._ID)
) { cursor ->
arrayOf(MediaStore.Audio.Genres.Members._ID)) { cursor ->
val songIdIndex =
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_ID,
MediaStore.Audio.AudioColumns.ARTIST,
AUDIO_COLUMN_ALBUM_ARTIST
)
AUDIO_COLUMN_ALBUM_ARTIST)
protected abstract val dirSelector: String
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
* or makes no sense to cache. This includes database IDs, modification dates,
* Populate the "file data" of the cursor, or data that is required to access a cache entry or
* makes no sense to cache. This includes database IDs, modification dates,
*/
protected open fun populateFileData(cursor: Cursor, raw: Song.Raw) {
raw.mediaStoreId = cursor.getLong(idIndex)
@ -315,9 +313,7 @@ abstract class MediaStoreExtractor(private val context: Context, private val cac
raw.albumMediaStoreId = cursor.getLong(albumIdIndex)
}
/**
* Extract cursor metadata into [raw].
*/
/** Extract cursor metadata into [raw]. */
protected open fun populateMetadata(cursor: Cursor, raw: Song.Raw) {
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.
* This constant is safe to use.
*/
@Suppress("InlinedApi")
private const val VOLUME_EXTERNAL = MediaStore.VOLUME_EXTERNAL
@Suppress("InlinedApi") private const val VOLUME_EXTERNAL = MediaStore.VOLUME_EXTERNAL
/**
* 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.
/**
* A [MediaStoreExtractor] that completes the music loading process in a way compatible from
* API 21 onwards to API 29.
* A [MediaStoreExtractor] that completes the music loading process in a way compatible from API 21
* onwards to API 29.
* @author OxygenCobalt
*/
class Api21MediaStoreExtractor(context: Context, cacheDatabase: CacheExtractor) :
@ -471,8 +466,7 @@ open class BaseApi29MediaStoreExtractor(context: Context, cacheDatabase: CacheEx
super.projection +
arrayOf(
MediaStore.Audio.AudioColumns.VOLUME_NAME,
MediaStore.Audio.AudioColumns.RELATIVE_PATH
)
MediaStore.Audio.AudioColumns.RELATIVE_PATH)
override val dirSelector: String
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
* API 29.
* A [MediaStoreExtractor] that completes the music loading process in a way compatible with at
* least API 29.
* @author OxygenCobalt
*/
@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
* API 30.
* A [MediaStoreExtractor] that completes the music loading process in a way compatible with at
* least API 30.
* @author OxygenCobalt
*/
@RequiresApi(Build.VERSION_CODES.R)
@ -556,8 +550,7 @@ class Api30MediaStoreExtractor(context: Context, cacheDatabase: CacheExtractor)
super.projection +
arrayOf(
MediaStore.Audio.AudioColumns.CD_TRACK_NUMBER,
MediaStore.Audio.AudioColumns.DISC_NUMBER
)
MediaStore.Audio.AudioColumns.DISC_NUMBER)
override fun populateMetadata(cursor: Cursor, raw: Song.Raw) {
super.populateMetadata(cursor, raw)

View file

@ -45,7 +45,10 @@ import org.oxycblt.auxio.util.logW
*
* @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)
/** 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 =
MetadataRetriever.retrieveMetadata(
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
@ -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
// 4. ID3v2.3 Original Date, as it is like #1
// 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["TDRL"]?.run { get(0).parseTimestamp() } ?: parseId3v23Date(tags)
)
?: tags["TDRL"]?.run { get(0).parseTimestamp() } ?: parseId3v23Date(tags))
?.let { raw.date = it }
// Album
tags["TXXX:MusicBrainz Album Id"]?.let { raw.albumMusicBrainzId = it[0] }
tags["TALB"]?.let { raw.albumName = it[0] }
tags["TSOA"]?.let { raw.albumSortName = it[0] }
(tags["TXXX:MusicBrainz Album Type"] ?: tags["GRP1"])?.let {
raw.albumReleaseTypes = it
}
(tags["TXXX:MusicBrainz Album Type"] ?: tags["GRP1"])?.let { raw.albumReleaseTypes = it }
// Artist
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
// 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!)
(
tags["ORIGINALDATE"]?.run { get(0).parseTimestamp() }
(tags["ORIGINALDATE"]?.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 }
// Album

View file

@ -18,10 +18,10 @@
package org.oxycblt.auxio.music.extractor
import androidx.core.text.isDigitsOnly
import java.util.UUID
import org.oxycblt.auxio.music.Date
import org.oxycblt.auxio.settings.Settings
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
@ -51,9 +51,7 @@ fun String.parseYear() = toIntOrNull()?.toDate()
/** Parse an ISO-8601 time-stamp from this field into a [Date]. */
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> {
val split = mutableListOf<String>()
var currentString = ""
@ -110,7 +108,8 @@ fun String.maybeParseSeparators(settings: Settings): List<String> {
return splitEscaped { separators.contains(it) }
}
fun String.toUuidOrNull(): UUID? = try {
fun String.toUuidOrNull(): UUID? =
try {
UUID.fromString(this)
} catch (e: IllegalArgumentException) {
null
@ -392,5 +391,4 @@ private val GENRE_TABLE =
"Psybient",
// 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.inflater
/**
* The adapter that displays a list of artist choices in the picker UI.
*/
class ArtistChoiceAdapter(private val listener: ItemClickListener) : RecyclerView.Adapter<ArtistChoiceViewHolder>() {
/** The adapter that displays a list of artist choices in the picker UI. */
class ArtistChoiceAdapter(private val listener: ItemClickListener) :
RecyclerView.Adapter<ArtistChoiceViewHolder>() {
private var artists = listOf<Artist>()
override fun getItemCount() = artists.size
@ -45,8 +44,7 @@ class ArtistChoiceAdapter(private val listener: ItemClickListener) : RecyclerVie
if (newArtists != artists) {
artists = newArtists
@Suppress("NotifyDataSetChanged")
notifyDataSetChanged()
@Suppress("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
* 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) {
binding.pickerImage.bind(artist)
binding.pickerName.text = artist.resolveName(binding.context)
binding.root.setOnClickListener {
listener.onItemClick(artist)
}
binding.root.setOnClickListener { listener.onItemClick(artist) }
}
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.
*/
class ArtistPickerDialog : ViewBindingDialogFragment<DialogMusicPickerBinding>(), ItemClickListener {
class ArtistPickerDialog :
ViewBindingDialogFragment<DialogMusicPickerBinding>(), ItemClickListener {
private val pickerModel: PickerViewModel by viewModels()
private val playbackModel: PlaybackViewModel by androidActivityViewModels()
private val navModel: NavigationViewModel by activityViewModels()
@ -56,9 +57,7 @@ class ArtistPickerDialog : ViewBindingDialogFragment<DialogMusicPickerBinding>()
DialogMusicPickerBinding.inflate(inflater)
override fun onConfigDialog(builder: AlertDialog.Builder) {
builder
.setTitle(R.string.lbl_artists)
.setNegativeButton(R.string.lbl_cancel, null)
builder.setTitle(R.string.lbl_artists).setNegativeButton(R.string.lbl_cancel, null)
}
override fun onBindingCreated(binding: DialogMusicPickerBinding, savedInstanceState: Bundle?) {

View file

@ -17,9 +17,7 @@
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 {
PLAY,
SHOW

View file

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

View file

@ -30,10 +30,10 @@ import android.os.storage.StorageVolume
import android.provider.MediaStore
import android.webkit.MimeTypeMap
import com.google.android.exoplayer2.util.MimeTypes
import org.oxycblt.auxio.R
import org.oxycblt.auxio.util.lazyReflectedMethod
import java.io.File
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. */
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) =
Directory(
volume,
relativePath.removePrefix(File.separator).removeSuffix(File.separator)
)
volume, relativePath.removePrefix(File.separator).removeSuffix(File.separator))
/**
* Converts an opaque document uri in the form of VOLUME:PATH into a [Directory]. This is a
@ -205,8 +203,7 @@ private val SV_API21_GET_PATH_METHOD: Method by lazyReflectedMethod(StorageVolum
/** The "primary" storage volume containing the OS. May be an SD Card. */
val StorageManager.primaryStorageVolumeCompat: StorageVolume
@Suppress("NewApi")
get() = primaryStorageVolume
@Suppress("NewApi") get() = primaryStorageVolume
/**
* 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. */
val StorageVolume.isPrimaryCompat: Boolean
@SuppressLint("NewApi")
get() = isPrimary
@SuppressLint("NewApi") get() = isPrimary
/** If this volume is emulated. */
val StorageVolume.isEmulatedCompat: Boolean
@SuppressLint("NewApi")
get() = isEmulated
@SuppressLint("NewApi") get() = isEmulated
/**
* 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. */
val StorageVolume.uuidCompat: String?
@SuppressLint("NewApi")
get() = uuid
@SuppressLint("NewApi") get() = uuid
/** Returns the state of the volume in a compatible manner. */
val StorageVolume.stateCompat: String
@SuppressLint("NewApi")
get() = state
@SuppressLint("NewApi") get() = state
/**
* 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
* 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 =
ContextCompat.checkSelfPermission(context, PERMISSION_READ_AUDIO) ==
PackageManager.PERMISSION_DENIED
@ -148,12 +148,11 @@ class Indexer {
val response =
try {
val start = System.currentTimeMillis()
val library = indexImpl(context, fresh)
val library = indexImpl(context, withCache)
if (library != null) {
logD(
"Music indexing completed successfully in " +
"${System.currentTimeMillis() - start}ms"
)
"${System.currentTimeMillis() - start}ms")
Response.Ok(library)
} else {
logE("No music found")
@ -194,9 +193,7 @@ class Indexer {
emitIndexing(null)
}
/**
* Run the proper music loading process.
*/
/** Run the proper music loading process. */
private suspend fun indexImpl(context: Context, withCache: Boolean): MusicStore.Library? {
// Create the chain of extractors. Each extractor builds on the previous and
// 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
* this function are **not** well-formed. The companion [buildAlbums], [buildArtists], and
* Does the initial query over the song database using [metadataExtractor]. The songs returned
* 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
* 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")
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
* the purpose is that the actual artist information of albums and songs often differs,
* and so they are linked in different ways.
* Group up songs AND albums into artists. This process seems weird (because it is), but the
* purpose is that the actual artist information of albums and songs often differs, and so they
* are linked in different ways.
*/
private fun buildArtists(songs: List<Song>, albums: List<Album>): List<Artist> {
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.
logD("Updating state to $indexing")
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)
return true
}
@ -95,6 +94,4 @@ class ObservingNotification(context: Context) : ServiceNotification(context, IND
private val INDEXER_CHANNEL =
ServiceNotification.ChannelInfo(
id = BuildConfig.APPLICATION_ID + ".channel.INDEXER",
nameRes = R.string.lbl_indexer
)
id = BuildConfig.APPLICATION_ID + ".channel.INDEXER", nameRes = R.string.lbl_indexer)

View file

@ -79,9 +79,7 @@ class IndexerService : Service(), Indexer.Controller, Settings.Callback {
wakeLock =
getSystemServiceCompat(PowerManager::class)
.newWakeLock(
PowerManager.PARTIAL_WAKE_LOCK,
BuildConfig.APPLICATION_ID + ":IndexerService"
)
PowerManager.PARTIAL_WAKE_LOCK, BuildConfig.APPLICATION_ID + ":IndexerService")
settings = Settings(this, this)
indexerContentObserver = SystemContentObserver()
@ -130,8 +128,7 @@ class IndexerService : Service(), Indexer.Controller, Settings.Callback {
when (state) {
is Indexer.State.Complete -> {
if (state.response is Indexer.Response.Ok &&
state.response.library != musicStore.library
) {
state.response.library != musicStore.library) {
logD("Applying new library")
val newLibrary = state.response.library
@ -243,10 +240,7 @@ class IndexerService : Service(), Indexer.Controller, Settings.Callback {
) : ContentObserver(handler), Runnable {
init {
contentResolverSafe.registerContentObserver(
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
true,
this
)
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, true, this)
}
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
// registering and de-registering this component as this setting changes.
if (settings.shouldBeObserving) {
onSt
artIndexing(true)
onStartIndexing(true)
}
}
}

View file

@ -26,7 +26,8 @@ enum class ActionMode {
SHUFFLE;
val intCode: Int
get() = when (this) {
get() =
when (this) {
NEXT -> IntegerTable.ACTION_MODE_NEXT
REPEAT -> IntegerTable.ACTION_MODE_REPEAT
SHUFFLE -> IntegerTable.ACTION_MODE_SHUFFLE

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -61,18 +61,13 @@ class QueueFragment : ViewBindingFragment<FragmentQueueBinding>(), QueueItemList
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
invalidateDivider()
}
}
)
})
}
// --- VIEWMODEL SETUP ----
collectImmediately(
queueModel.queue,
queueModel.index,
playbackModel.isPlaying,
::updateQueue
)
queueModel.queue, queueModel.index, playbackModel.isPlaying, ::updateQueue)
}
override fun onDestroyBinding(binding: FragmentQueueBinding) {

View file

@ -70,10 +70,6 @@ class QueueSheetBehavior<V : View>(context: Context, attributeSet: AttributeSet?
val bars = insets.systemBarInsetsCompat
expandedOffset = bars.top + barHeight + barSpacing
return insets.replaceSystemBarInsetsCompat(
bars.left,
bars.top,
bars.right,
expandedOffset + bars.bottom
)
bars.left, 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. */
fun removeQueueDataItem(adapterIndex: Int) {
if (adapterIndex <= playbackManager.index ||
adapterIndex !in playbackManager.queue.indices
) {
adapterIndex !in playbackManager.queue.indices) {
return
}

View file

@ -21,12 +21,12 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import kotlin.math.abs
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.DialogPreAmpBinding
import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.ui.fragment.ViewBindingDialogFragment
import org.oxycblt.auxio.util.context
import kotlin.math.abs
/**
* 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))
}
}
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.id3.TextInformationFrame
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.playback.state.PlaybackStateManager
import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.util.logD
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.

View file

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

View file

@ -110,8 +110,7 @@ class PlaybackStateDatabase private constructor(context: Context) :
queue = queue,
positionMs = rawState.positionMs,
repeatMode = rawState.repeatMode,
isShuffled = rawState.isShuffled
)
isShuffled = rawState.isShuffled)
}
private fun readRawState(): RawState? {
@ -138,8 +137,7 @@ class PlaybackStateDatabase private constructor(context: Context) :
isShuffled = cursor.getInt(shuffleIndex) == 1,
songUid = Music.UID.fromString(cursor.getString(songUidIndex))
?: 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,
isShuffled = state.isShuffled,
songUid = state.queue[state.index].uid,
parentUid = state.parent?.uid
)
parentUid = state.parent?.uid)
writeRawState(rawState)
writeQueue(state.queue)

View file

@ -17,6 +17,7 @@
package org.oxycblt.auxio.playback.state
import kotlin.math.max
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
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.logE
import org.oxycblt.auxio.util.logW
import kotlin.math.max
/**
* Master class (and possible god object) for the playback state.
@ -268,11 +268,7 @@ class PlaybackStateManager private constructor() {
notifyShuffledChanged()
}
private fun orderQueue(
settings: Settings,
shuffled: Boolean,
keep: Song?
) {
private fun orderQueue(settings: Settings, shuffled: Boolean, keep: Song?) {
val newIndex: Int
if (shuffled) {
@ -369,7 +365,8 @@ class PlaybackStateManager private constructor() {
val library = musicStore.library ?: return false
val internalPlayer = internalPlayer ?: return false
val state = try {
val state =
try {
withContext(Dispatchers.IO) { database.read(library) }
} catch (e: Exception) {
logE("Unable to restore playback state.")
@ -484,8 +481,7 @@ class PlaybackStateManager private constructor() {
queue = _queue,
positionMs = playerState.calculateElapsedPosition(),
isShuffled = isShuffled,
repeatMode = repeatMode
)
repeatMode = repeatMode)
// --- 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
import android.bluetooth.BluetoothProfile
@ -6,14 +23,16 @@ import android.content.Context
import android.content.Intent
/**
* A [BroadcastReceiver] that handles connections from bluetooth headsets, starting playback if
* they occur.
* A [BroadcastReceiver] that handles connections from bluetooth headsets, starting playback if they
* occur.
* @author seijikun, OxygenCobalt
*/
class BluetoothHeadsetReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
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) {
// TODO: Initialize the service (Permission workflow must be figured out)
// 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_ALBUM_ARTIST,
song.album.resolveArtistContents(context)
)
song.album.resolveArtistContents(context))
.putText(MediaMetadataCompat.METADATA_KEY_AUTHOR, artist)
.putText(MediaMetadataCompat.METADATA_KEY_COMPOSER, artist)
.putText(MediaMetadataCompat.METADATA_KEY_WRITER, artist)
.putText(
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_DISPLAY_TITLE, title)
.putText(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, artist)
.putText(
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)
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())
}
song.date?.let {
builder.putString(MediaMetadataCompat.METADATA_KEY_DATE, it.toString())
}
song.date?.let { 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
// 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)
callback.onPostNotification(notification, PostingReason.METADATA)
}
}
)
})
}
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.TRACK -> PlaybackStateCompat.REPEAT_MODE_ONE
RepeatMode.ALL -> PlaybackStateCompat.REPEAT_MODE_ALL
}
)
})
invalidateSecondaryAction()
}
@ -242,8 +235,7 @@ class MediaSessionComponent(private val context: Context, private val callback:
PlaybackStateCompat.SHUFFLE_MODE_ALL
} else {
PlaybackStateCompat.SHUFFLE_MODE_NONE
}
)
})
invalidateSecondaryAction()
}
@ -328,8 +320,7 @@ class MediaSessionComponent(private val context: Context, private val callback:
playbackManager.reshuffle(
shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_ALL ||
shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_GROUP,
settings
)
settings)
}
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.
val extraAction = when (settings.notifAction) {
ActionMode.SHUFFLE -> PlaybackStateCompat.CustomAction.Builder(
val extraAction =
when (settings.notifAction) {
ActionMode.SHUFFLE ->
PlaybackStateCompat.CustomAction.Builder(
PlaybackService.ACTION_INVERT_SHUFFLE,
context.getString(R.string.desc_shuffle),
if (playbackManager.isShuffled) {
R.drawable.ic_shuffle_on_24
} else {
R.drawable.ic_shuffle_off_24
}
)
else -> PlaybackStateCompat.CustomAction.Builder(
})
else ->
PlaybackStateCompat.CustomAction.Builder(
PlaybackService.ACTION_INC_REPEAT_MODE,
context.getString(R.string.desc_change_repeat),
playbackManager.repeatMode.icon
)
playbackManager.repeatMode.icon)
}
val exitAction =
PlaybackStateCompat.CustomAction.Builder(
PlaybackService.ACTION_EXIT,
context.getString(R.string.desc_exit),
R.drawable.ic_close_24
)
R.drawable.ic_close_24)
.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(
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(
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))
setStyle(MediaStyle().setMediaSession(sessionToken).setShowActionsInCompactView(1, 2, 3))
@ -134,10 +132,7 @@ class NotificationComponent(private val context: Context, sessionToken: MediaSes
): NotificationCompat.Action {
val action =
NotificationCompat.Action.Builder(
iconRes,
actionName,
context.newBroadcastPendingIntent(actionName)
)
iconRes, actionName, context.newBroadcastPendingIntent(actionName))
return action.build()
}
@ -146,7 +141,6 @@ class NotificationComponent(private val context: Context, sessionToken: MediaSes
val CHANNEL_INFO =
ChannelInfo(
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.mediacodec.MediaCodecSelector
import com.google.android.exoplayer2.source.DefaultMediaSourceFactory
import kotlin.math.max
import kotlin.math.min
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
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.widgets.WidgetComponent
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:
@ -123,10 +123,8 @@ class PlaybackService :
handler,
audioListener,
AudioCapabilities.DEFAULT_AUDIO_CAPABILITIES,
replayGainProcessor
),
LibflacAudioRenderer(handler, audioListener, replayGainProcessor)
)
replayGainProcessor),
LibflacAudioRenderer(handler, audioListener, replayGainProcessor))
}
// Enable constant bitrate seeking so that certain MP3s/AACs are seekable
@ -141,8 +139,7 @@ class PlaybackService :
.setUsage(C.USAGE_MEDIA)
.setContentType(C.AUDIO_CONTENT_TYPE_MUSIC)
.build(),
true
)
true)
.build()
player.addListener(this)
@ -227,10 +224,7 @@ class PlaybackService :
override fun makeState(durationMs: Long) =
InternalPlayer.State.new(
player.playWhenReady,
player.isPlaying,
max(min(player.currentPosition, durationMs), 0)
)
player.playWhenReady, player.isPlaying, max(min(player.currentPosition, durationMs), 0))
override fun loadSong(song: Song?, play: Boolean) {
if (song == null) {
@ -340,8 +334,7 @@ class PlaybackService :
override fun onSettingChanged(key: String) {
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_without)
) {
key == getString(R.string.set_key_pre_amp_without)) {
onTracksChanged(player.currentTracks)
}
}
@ -353,8 +346,7 @@ class PlaybackService :
Intent(event)
.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, packageName)
.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 */
@ -378,9 +370,7 @@ class PlaybackService :
is InternalPlayer.Action.RestoreState -> {
restoreScope.launch {
playbackManager.restoreState(
PlaybackStateDatabase.getInstance(this@PlaybackService),
false
)
PlaybackStateDatabase.getInstance(this@PlaybackService), false)
}
}
is InternalPlayer.Action.ShuffleAll -> {
@ -475,8 +465,7 @@ class PlaybackService :
private fun maybeResumeFromPlug() {
if (playbackManager.song != null &&
settings.headsetAutoplay &&
initialHeadsetPlugEventHandled
) {
initialHeadsetPlugEventHandled) {
logD("Device connected, resuming")
playbackManager.changePlaying(true)
}

View file

@ -20,11 +20,11 @@ package org.oxycblt.auxio.playback.ui
import android.content.Context
import android.util.AttributeSet
import com.google.android.material.slider.Slider
import kotlin.math.max
import org.oxycblt.auxio.databinding.ViewSeekBarBinding
import org.oxycblt.auxio.playback.formatDurationDs
import org.oxycblt.auxio.util.inflater
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

View file

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

View file

@ -23,6 +23,7 @@ import androidx.annotation.IdRes
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import java.text.Normalizer
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
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.util.application
import org.oxycblt.auxio.util.logD
import java.text.Normalizer
/**
* The [ViewModel] for search functionality.
@ -87,7 +87,8 @@ class SearchViewModel(application: Application) :
logD("Performing search for $query")
// Searching can be quite expensive, so get on a co-routine
currentSearchJob = viewModelScope.launch {
currentSearchJob =
viewModelScope.launch {
val sort = Sort(Sort.Mode.ByName, true)
val results = mutableListOf<Item>()

View file

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

View file

@ -84,7 +84,8 @@ class Settings(private val context: Context, private val callback: Callback? = n
if (inner.contains(OldKeys.KEY_SHOW_COVERS) || inner.contains(OldKeys.KEY_QUALITY_COVERS)) {
logD("Migrating cover settings")
val mode = when {
val mode =
when {
!inner.getBoolean(OldKeys.KEY_SHOW_COVERS, true) -> CoverMode.OFF
!inner.getBoolean(OldKeys.KEY_QUALITY_COVERS, true) -> CoverMode.MEDIA_STORE
else -> CoverMode.QUALITY
@ -100,7 +101,8 @@ class Settings(private val context: Context, private val callback: Callback? = n
if (inner.contains(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 =
if (inner.getBoolean(OldKeys.KEY_ALT_NOTIF_ACTION, false)) {
ActionMode.SHUFFLE
} else {
ActionMode.REPEAT
@ -125,10 +127,11 @@ class Settings(private val context: Context, private val callback: Callback? = n
if (inner.contains(OldKeys.KEY_LIB_PLAYBACK_MODE)) {
logD("Migrating ${OldKeys.KEY_LIB_PLAYBACK_MODE}")
val mode = inner.getInt(
OldKeys.KEY_LIB_PLAYBACK_MODE,
IntegerTable.PLAYBACK_MODE_ALL_SONGS
).migratePlaybackMode() ?: MusicMode.SONGS
val mode =
inner
.getInt(OldKeys.KEY_LIB_PLAYBACK_MODE, IntegerTable.PLAYBACK_MODE_ALL_SONGS)
.migratePlaybackMode()
?: MusicMode.SONGS
inner.edit {
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)) {
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 {
putInt(
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)
apply()
}
@ -173,8 +176,7 @@ class Settings(private val context: Context, private val callback: Callback? = n
get() =
inner.getInt(
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 */
val useBlackTheme: Boolean
@ -182,7 +184,8 @@ class Settings(private val context: Context, private val callback: Callback? = n
/** The current 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) {
inner.edit {
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>
get() =
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))
set(value) {
inner.edit {
@ -216,15 +218,15 @@ class Settings(private val context: Context, private val callback: Callback? = n
val actionMode: ActionMode
get() =
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
/**
* The custom action to display in the notification.
*/
/** The custom action to display in the notification. */
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) */
val headsetAutoplay: Boolean
@ -234,8 +236,7 @@ class Settings(private val context: Context, private val callback: Callback? = n
val replayGainMode: ReplayGainMode
get() =
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
/** The current ReplayGain pre-amp configuration */
@ -243,8 +244,7 @@ class Settings(private val context: Context, private val callback: Callback? = n
get() =
ReplayGainPreAmp(
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) {
inner.edit {
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() =
MusicMode.fromInt(
inner.getInt(
context.getString(R.string.set_key_library_song_playback_mode),
Int.MIN_VALUE
)
)
context.getString(R.string.set_key_library_song_playback_mode), Int.MIN_VALUE))
?: MusicMode.SONGS
/**
@ -272,10 +269,7 @@ class Settings(private val context: Context, private val callback: Callback? = n
get() =
MusicMode.fromInt(
inner.getInt(
context.getString(R.string.set_key_detail_song_playback_mode),
Int.MIN_VALUE
)
)
context.getString(R.string.set_key_detail_song_playback_mode), Int.MIN_VALUE))
/** Whether shuffle should stay on when a new song is selected. */
val keepShuffle: Boolean
@ -298,7 +292,10 @@ class Settings(private val context: Context, private val callback: Callback? = n
/** The strategy used when loading images. */
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. */
val excludeNonMusic: Boolean
@ -311,9 +308,7 @@ class Settings(private val context: Context, private val callback: Callback? = n
.mapNotNull { Directory.fromDocumentUri(storageManager, it) }
return MusicDirs(
dirs,
inner.getBoolean(context.getString(R.string.set_key_music_dirs_include), false)
)
dirs, inner.getBoolean(context.getString(R.string.set_key_music_dirs_include), false))
}
/** 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 {
putStringSet(
context.getString(R.string.set_key_music_dirs),
musicDirs.dirs.map(Directory::toDocumentUri).toSet()
)
musicDirs.dirs.map(Directory::toDocumentUri).toSet())
putBoolean(
context.getString(R.string.set_key_music_dirs_include),
musicDirs.shouldInclude
)
context.getString(R.string.set_key_music_dirs_include), musicDirs.shouldInclude)
apply()
}
}
@ -348,14 +340,12 @@ class Settings(private val context: Context, private val callback: Callback? = n
var searchFilterMode: MusicMode?
get() =
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) {
inner.edit {
putInt(
context.getString(R.string.set_key_search_filter),
value?.intCode ?: Int.MIN_VALUE
)
value?.intCode ?: Int.MIN_VALUE)
apply()
}
}
@ -364,8 +354,7 @@ class Settings(private val context: Context, private val callback: Callback? = n
var libSongSort: Sort
get() =
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)
set(value) {
inner.edit {
@ -378,8 +367,7 @@ class Settings(private val context: Context, private val callback: Callback? = n
var libAlbumSort: Sort
get() =
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)
set(value) {
inner.edit {
@ -392,8 +380,7 @@ class Settings(private val context: Context, private val callback: Callback? = n
var libArtistSort: Sort
get() =
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)
set(value) {
inner.edit {
@ -406,8 +393,7 @@ class Settings(private val context: Context, private val callback: Callback? = n
var libGenreSort: Sort
get() =
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)
set(value) {
inner.edit {
@ -422,10 +408,7 @@ class Settings(private val context: Context, private val callback: Callback? = n
var sort =
Sort.fromIntCode(
inner.getInt(
context.getString(R.string.set_key_detail_album_sort),
Int.MIN_VALUE
)
)
context.getString(R.string.set_key_detail_album_sort), Int.MIN_VALUE))
?: Sort(Sort.Mode.ByDisc, true)
// 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
get() =
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)
set(value) {
inner.edit {
@ -460,8 +442,7 @@ class Settings(private val context: Context, private val callback: Callback? = n
var detailGenreSort: Sort
get() =
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)
set(value) {
inner.edit {

View file

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

View file

@ -17,8 +17,6 @@
package org.oxycblt.auxio.settings.prefs
import android.app.Activity
import android.os.Build
import android.os.Bundle
import android.view.View
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.showToast
import org.oxycblt.auxio.util.systemBarInsetsCompat
import java.security.Permission
import java.util.jar.Manifest
/**
* The actual fragment containing the settings menu. Inherits [PreferenceFragmentCompat].
@ -98,12 +94,18 @@ class PreferenceFragment : PreferenceFragmentCompat() {
}
is WrappedDialogPreference -> {
val context = requireContext()
val directions = when (preference.key) {
context.getString(R.string.set_key_accent) -> SettingsFragmentDirections.goToAccentDialog()
context.getString(R.string.set_key_lib_tabs) -> SettingsFragmentDirections.goToTabDialog()
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()
val directions =
when (preference.key) {
context.getString(R.string.set_key_accent) ->
SettingsFragmentDirections.goToAccentDialog()
context.getString(R.string.set_key_lib_tabs) ->
SettingsFragmentDirections.goToTabDialog()
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)

View file

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

View file

@ -23,10 +23,10 @@ import android.view.View
import android.view.WindowInsets
import androidx.coordinatorlayout.widget.CoordinatorLayout
import com.google.android.material.bottomsheet.NeoBottomSheetBehavior
import kotlin.math.abs
import org.oxycblt.auxio.util.coordinatorLayoutBehavior
import org.oxycblt.auxio.util.replaceSystemBarInsetsCompat
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
@ -127,11 +127,7 @@ class BottomSheetContentBehavior<V : View>(context: Context, attributeSet: Attri
val bars = insets.systemBarInsetsCompat
insets.replaceSystemBarInsetsCompat(
bars.left,
bars.top,
bars.right,
(bars.bottom - consumed).coerceAtLeast(0)
)
bars.left, bars.top, bars.right, (bars.bottom - consumed).coerceAtLeast(0))
}
setup = true
@ -145,9 +141,7 @@ class BottomSheetContentBehavior<V : View>(context: Context, attributeSet: Attri
View.MeasureSpec.makeMeasureSpec(parent.measuredWidth, View.MeasureSpec.EXACTLY)
val contentHeightSpec =
View.MeasureSpec.makeMeasureSpec(
parent.measuredHeight - consumed,
View.MeasureSpec.EXACTLY
)
parent.measuredHeight - consumed, View.MeasureSpec.EXACTLY)
child.measure(contentWidthSpec, contentHeightSpec)
}

View file

@ -39,8 +39,7 @@ private val ACCENT_NAMES =
R.string.clr_orange,
R.string.clr_brown,
R.string.clr_grey,
R.string.clr_dynamic
)
R.string.clr_dynamic)
private val ACCENT_THEMES =
intArrayOf(
@ -102,8 +101,7 @@ private val ACCENT_PRIMARY_COLORS =
R.color.orange_primary,
R.color.brown_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

View file

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

View file

@ -21,9 +21,9 @@ import android.content.Context
import android.util.AttributeSet
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kotlin.math.max
import org.oxycblt.auxio.R
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

View file

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

View file

@ -33,6 +33,7 @@ import androidx.core.view.isInvisible
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kotlin.math.abs
import org.oxycblt.auxio.R
import org.oxycblt.auxio.ui.recycler.AuxioRecyclerView
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.isUnder
import org.oxycblt.auxio.util.systemBarInsetsCompat
import kotlin.math.abs
/**
* 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 {
layoutParams =
FrameLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
.apply {
gravity = Gravity.CENTER_HORIZONTAL or Gravity.TOP
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) {
onPreDraw()
}
}
)
})
// We use a listener instead of overriding onTouchEvent so that we don't conflict with
// RecyclerView touch events.
@ -191,8 +188,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
): Boolean {
return onItemTouch(event)
}
}
)
})
}
// --- RECYCLERVIEW EVENT MANAGEMENT ---
@ -247,8 +243,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
thumbWidth +
popupLayoutParams.leftMargin +
popupLayoutParams.rightMargin,
popupLayoutParams.width
)
popupLayoutParams.width)
val heightMeasureSpec =
ViewGroup.getChildMeasureSpec(
@ -257,8 +252,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
thumbPadding.bottom +
popupLayoutParams.topMargin +
popupLayoutParams.bottomMargin,
popupLayoutParams.height
)
popupLayoutParams.height)
popupView.measure(widthMeasureSpec, heightMeasureSpec)
}
@ -279,8 +273,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
(thumbTop + thumbAnchorY - popupAnchorY)
.coerceAtLeast(thumbPadding.top + popupLayoutParams.topMargin)
.coerceAtMost(
height - thumbPadding.bottom - popupLayoutParams.bottomMargin - popupHeight
)
height - thumbPadding.bottom - popupLayoutParams.bottomMargin - popupHeight)
popupView.layout(popupLeft, popupTop, popupLeft + popupWidth, popupTop + popupHeight)
}
@ -355,8 +348,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
MotionEvent.ACTION_MOVE -> {
if (!dragging &&
thumbView.isUnder(downX, thumbView.top.toFloat(), minTouchTargetSize) &&
abs(eventY - downY) > touchSlop
) {
abs(eventY - downY) > touchSlop) {
if (thumbView.isUnder(downX, downY, minTouchTargetSize)) {
dragStartY = lastY
dragStartThumbOffset = thumbOffset

View file

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

View file

@ -26,10 +26,10 @@ import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment
import androidx.viewbinding.ViewBinding
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.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.

View file

@ -23,10 +23,10 @@ import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.viewbinding.ViewBinding
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.unlikelyToBeNull
import kotlin.properties.ReadOnlyProperty
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.

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
root.layoutParams =
RecyclerView.LayoutParams(
RecyclerView.LayoutParams.MATCH_PARENT,
RecyclerView.LayoutParams.WRAP_CONTENT
)
RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT)
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -37,9 +37,9 @@ import androidx.annotation.PluralsRes
import androidx.annotation.Px
import androidx.annotation.StringRes
import androidx.core.content.ContextCompat
import kotlin.reflect.KClass
import org.oxycblt.auxio.IntegerTable
import org.oxycblt.auxio.MainActivity
import kotlin.reflect.KClass
/** Shortcut to get a [LayoutInflater] from a [Context] */
val Context.inflater: LayoutInflater
@ -152,8 +152,7 @@ fun Context.newMainPendingIntent(): PendingIntent =
this,
IntegerTable.REQUEST_CODE,
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] */
fun Context.newBroadcastPendingIntent(what: String): PendingIntent =
@ -161,5 +160,4 @@ fun Context.newBroadcastPendingIntent(what: String): PendingIntent =
this,
IntegerTable.REQUEST_CODE,
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 -> {
Insets.max(
getCompatInsets(WindowInsets.Type.systemGestures()),
getCompatInsets(WindowInsets.Type.systemBars())
)
getCompatInsets(WindowInsets.Type.systemBars()))
}
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> {
@Suppress("DEPRECATION")
@ -247,8 +246,7 @@ fun WindowInsets.getSystemWindowCompatInsets() =
systemWindowInsetLeft,
systemWindowInsetTop,
systemWindowInsetRight,
systemWindowInsetBottom
)
systemWindowInsetBottom)
@Suppress("DEPRECATION")
@RequiresApi(Build.VERSION_CODES.Q)
@ -272,8 +270,7 @@ fun WindowInsets.replaceSystemBarInsetsCompat(
WindowInsets.Builder(this)
.setInsets(
WindowInsets.Type.systemBars(),
Insets.of(left, top, right, bottom).toPlatformInsets()
)
Insets.of(left, top, right, bottom).toPlatformInsets())
.build()
}
else -> {

View file

@ -50,14 +50,14 @@ private val Any.autoTag: String
get() = "Auxio.${this::class.simpleName ?: "Anonymous Object"}"
/**
* Note: If you are politely forking this project while keeping the source open, you can ignore
* the following passage. If not, give me a moment of your time.
* Note: If you are politely forking this project while keeping the source open, you can ignore the
* 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
* existence on this planet? Or do you want to spend your life taking work others did and making
* it objectively worse so you could arbitrage a fraction of a penny on every AdMob impression you
* get? You could do so many great things if you simply had the courage to come up with an idea of
* your own.
* existence on this planet? Or do you want to spend your life taking work others did and making it
* objectively worse so you could arbitrage a fraction of a penny on every AdMob impression you get?
* You could do so many great things if you simply had the courage to come up with an idea of your
* own.
*
* 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")
private fun basedCopyleftNotice(): Boolean {
if (BuildConfig.APPLICATION_ID != "org.oxycblt.auxio" &&
BuildConfig.APPLICATION_ID != "org.oxycblt.auxio.debug"
) {
BuildConfig.APPLICATION_ID != "org.oxycblt.auxio.debug") {
Log.d(
"Auxio Project",
"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
}

View file

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

View file

@ -126,8 +126,7 @@ private fun RemoteViews.applyCover(
setImageViewBitmap(R.id.widget_cover, state.cover)
setContentDescription(
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 {
setImageViewResource(R.id.widget_cover, R.drawable.ic_remote_default_cover_24)
setContentDescription(R.id.widget_cover, context.getString(R.string.desc_no_cover))
@ -145,8 +144,7 @@ private fun RemoteViews.applyPlayPauseControls(
setOnClickPendingIntent(
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
// when playing.
@ -175,14 +173,10 @@ private fun RemoteViews.applyBasicControls(
applyPlayPauseControls(context, state)
setOnClickPendingIntent(
R.id.widget_skip_prev,
context.newBroadcastPendingIntent(PlaybackService.ACTION_SKIP_PREV)
)
R.id.widget_skip_prev, context.newBroadcastPendingIntent(PlaybackService.ACTION_SKIP_PREV))
setOnClickPendingIntent(
R.id.widget_skip_next,
context.newBroadcastPendingIntent(PlaybackService.ACTION_SKIP_NEXT)
)
R.id.widget_skip_next, context.newBroadcastPendingIntent(PlaybackService.ACTION_SKIP_NEXT))
return this
}
@ -195,13 +189,11 @@ private fun RemoteViews.applyFullControls(
setOnClickPendingIntent(
R.id.widget_repeat,
context.newBroadcastPendingIntent(PlaybackService.ACTION_INC_REPEAT_MODE)
)
context.newBroadcastPendingIntent(PlaybackService.ACTION_INC_REPEAT_MODE))
setOnClickPendingIntent(
R.id.widget_shuffle,
context.newBroadcastPendingIntent(PlaybackService.ACTION_INVERT_SHUFFLE)
)
context.newBroadcastPendingIntent(PlaybackService.ACTION_INVERT_SHUFFLE))
val shuffleRes =
when {

View file

@ -22,6 +22,7 @@ import android.graphics.Bitmap
import android.os.Build
import coil.request.ImageRequest
import coil.transform.RoundedCornersTransformation
import kotlin.math.sqrt
import org.oxycblt.auxio.R
import org.oxycblt.auxio.image.BitmapProvider
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.util.getDimenSize
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
@ -109,8 +109,7 @@ class WidgetComponent(private val context: Context) :
.size(computeSize(sw, sh, 10f))
.transformations(
SquareFrameTransform.INSTANCE,
RoundedCornersTransformation(cornerRadius.toFloat())
)
RoundedCornersTransformation(cornerRadius.toFloat()))
} else {
// Divide by two to really make sure we aren't hitting the memory limit.
builder.size(computeSize(sw, sh, 2f))
@ -121,8 +120,7 @@ class WidgetComponent(private val context: Context) :
val state = WidgetState(song, bitmap, isPlaying, repeatMode, isShuffled)
widget.update(context, state)
}
}
)
})
}
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 onSettingChanged(key: String) {
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()
}
}

View file

@ -60,8 +60,7 @@ class WidgetProvider : AppWidgetProvider() {
SizeF(180f, 152f) to createSmallWidget(context, state),
SizeF(272f, 152f) to createWideWidget(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)
@ -70,9 +69,7 @@ class WidgetProvider : AppWidgetProvider() {
} catch (e: Exception) {
logW("Unable to update widget: $e")
awm.updateAppWidget(
ComponentName(context, this::class.java),
createDefaultWidget(context)
)
ComponentName(context, this::class.java), createDefaultWidget(context))
}
}

View file

@ -170,13 +170,13 @@
<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_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_off">Off</string>
<string name="set_cover_mode_media_store">Fast</string>
<string name="set_cover_mode_quality">High quality</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>
<!-- Skip to next (song) -->
<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_content">Content</string>
<string name="set_reindex">Reload music</string>
<string name="set_reindex_desc">Reload the music library, using the cache whenever possible</string>
<string name="set_reindex">Refresh music</string>
<string name="set_reindex_desc">Reload the music library, using cached tags when possible</string>
<!-- Different from "Reload music" -->
<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_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_desc">Manage where music should be loaded from</string>
<!-- As in the mode to be used with the music folders setting -->