all: lint with ktlint

Just lint with ktlint. It has better import checking functionality.
This commit is contained in:
Alexander Capehart 2022-09-08 21:54:38 -06:00
parent 09823d7829
commit 06d6495dcd
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
77 changed files with 537 additions and 274 deletions

View file

@ -120,10 +120,7 @@ dependencies {
spotless { spotless {
kotlin { kotlin {
target "src/**/*.kt" target "src/**/*.kt"
// ktlint does checking, while ktfmt actually does formatting
ktlint() ktlint()
ktfmt().dropboxStyle()
licenseHeaderFile("NOTICE") licenseHeaderFile("NOTICE")
} }
} }

View file

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

View file

@ -98,9 +98,7 @@ class MainActivity : AppCompatActivity() {
val action = val action =
when (intent.action) { when (intent.action) {
Intent.ACTION_VIEW -> InternalPlayer.Action.Open(intent.data ?: return false) Intent.ACTION_VIEW -> InternalPlayer.Action.Open(intent.data ?: return false)
AuxioApp.INTENT_KEY_SHORTCUT_SHUFFLE -> { AuxioApp.INTENT_KEY_SHORTCUT_SHUFFLE -> InternalPlayer.Action.ShuffleAll
InternalPlayer.Action.ShuffleAll
}
else -> return false else -> return false
} }

View file

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

View file

@ -89,7 +89,11 @@ class AlbumDetailFragment :
collectImmediately(detailModel.currentAlbum, ::handleItemChange) collectImmediately(detailModel.currentAlbum, ::handleItemChange)
collectImmediately(detailModel.albumData, detailAdapter::submitList) collectImmediately(detailModel.albumData, detailAdapter::submitList)
collectImmediately( collectImmediately(
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback) playbackModel.song,
playbackModel.parent,
playbackModel.isPlaying,
::updatePlayback
)
collect(navModel.exploreNavigationItem, ::handleNavigation) collect(navModel.exploreNavigationItem, ::handleNavigation)
} }
@ -164,7 +168,9 @@ class AlbumDetailFragment :
findNavController() findNavController()
.navigate( .navigate(
AlbumDetailFragmentDirections.actionShowArtist( AlbumDetailFragmentDirections.actionShowArtist(
unlikelyToBeNull(detailModel.currentAlbum.value).artist.uid)) unlikelyToBeNull(detailModel.currentAlbum.value).artist.uid
)
)
} }
private fun handleItemChange(album: Album?) { private fun handleItemChange(album: Album?) {
@ -228,7 +234,8 @@ class AlbumDetailFragment :
binding.detailRecycler.post { binding.detailRecycler.post {
// Make sure to increment the position to make up for the detail header // Make sure to increment the position to make up for the detail header
binding.detailRecycler.layoutManager?.startSmoothScroll( binding.detailRecycler.layoutManager?.startSmoothScroll(
CenterSmoothScroller(requireContext(), pos)) CenterSmoothScroller(requireContext(), pos)
)
// If the recyclerview can scroll, its certain that it will have to scroll to // If the recyclerview can scroll, its certain that it will have to scroll to
// correctly center the playing item, so make sure that the Toolbar is lifted in // correctly center the playing item, so make sure that the Toolbar is lifted in

View file

@ -84,7 +84,11 @@ class ArtistDetailFragment :
collectImmediately(detailModel.currentArtist, ::handleItemChange) collectImmediately(detailModel.currentArtist, ::handleItemChange)
collectImmediately(detailModel.artistData, detailAdapter::submitList) collectImmediately(detailModel.artistData, detailAdapter::submitList)
collectImmediately( collectImmediately(
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback) playbackModel.song,
playbackModel.parent,
playbackModel.isPlaying,
::updatePlayback
)
collect(navModel.exploreNavigationItem, ::handleNavigation) collect(navModel.exploreNavigationItem, ::handleNavigation)
} }

View file

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

View file

@ -85,7 +85,11 @@ class GenreDetailFragment :
collectImmediately(detailModel.currentGenre, ::handleItemChange) collectImmediately(detailModel.currentGenre, ::handleItemChange)
collectImmediately(detailModel.genreData, detailAdapter::submitList) collectImmediately(detailModel.genreData, detailAdapter::submitList)
collectImmediately( collectImmediately(
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback) playbackModel.song,
playbackModel.parent,
playbackModel.isPlaying,
::updatePlayback
)
collect(navModel.exploreNavigationItem, ::handleNavigation) collect(navModel.exploreNavigationItem, ::handleNavigation)
} }

View file

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

View file

@ -127,7 +127,8 @@ private class ArtistDetailViewHolder private constructor(private val binding: It
binding.context.getString( binding.context.getString(
R.string.fmt_two, R.string.fmt_two,
binding.context.getPlural(R.plurals.fmt_album_count, item.albums.size), binding.context.getPlural(R.plurals.fmt_album_count, item.albums.size),
binding.context.getPlural(R.plurals.fmt_song_count, item.songs.size)) binding.context.getPlural(R.plurals.fmt_song_count, item.songs.size)
)
binding.detailPlayButton.setOnClickListener { listener.onPlayParent() } binding.detailPlayButton.setOnClickListener { listener.onPlayParent() }
binding.detailShuffleButton.setOnClickListener { listener.onShuffleParent() } binding.detailShuffleButton.setOnClickListener { listener.onShuffleParent() }

View file

@ -42,7 +42,8 @@ abstract class DetailAdapter<L : DetailAdapter.Listener>(
) : IndicatorAdapter<RecyclerView.ViewHolder>(), AuxioRecyclerView.SpanSizeLookup { ) : IndicatorAdapter<RecyclerView.ViewHolder>(), AuxioRecyclerView.SpanSizeLookup {
private var isPlaying = false private var isPlaying = false
@Suppress("LeakingThis") override fun getItemCount() = differ.currentList.size @Suppress("LeakingThis")
override fun getItemCount() = differ.currentList.size
override fun getItemViewType(position: Int) = override fun getItemViewType(position: Int) =
when (differ.currentList[position]) { when (differ.currentList[position]) {
@ -83,7 +84,8 @@ abstract class DetailAdapter<L : DetailAdapter.Listener>(
return item is Header || item is SortHeader 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> override val currentList: List<Item>
get() = differ.currentList get() = differ.currentList

View file

@ -36,8 +36,6 @@ import androidx.viewpager2.widget.ViewPager2
import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.tabs.TabLayoutMediator import com.google.android.material.tabs.TabLayoutMediator
import com.google.android.material.transition.MaterialSharedAxis import com.google.android.material.transition.MaterialSharedAxis
import java.lang.reflect.Field
import kotlin.math.abs
import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentHomeBinding import org.oxycblt.auxio.databinding.FragmentHomeBinding
@ -64,6 +62,8 @@ import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.getColorCompat import org.oxycblt.auxio.util.getColorCompat
import org.oxycblt.auxio.util.lazyReflectedField import org.oxycblt.auxio.util.lazyReflectedField
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import java.lang.reflect.Field
import kotlin.math.abs
/** /**
* The main "Launching Point" fragment of Auxio, allowing navigation to the detail views for each * The main "Launching Point" fragment of Auxio, allowing navigation to the detail views for each
@ -111,7 +111,8 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
binding.homeToolbar.alpha = 1f - (abs(offset.toFloat()) / (range.toFloat() / 2)) binding.homeToolbar.alpha = 1f - (abs(offset.toFloat()) / (range.toFloat() / 2))
binding.homeContent.updatePadding( binding.homeContent.updatePadding(
bottom = binding.homeAppbar.totalScrollRange + offset) bottom = binding.homeAppbar.totalScrollRange + offset
)
} }
} }
@ -138,7 +139,8 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
object : ViewPager2.OnPageChangeCallback() { object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) = override fun onPageSelected(position: Int) =
homeModel.updateCurrentTab(position) homeModel.updateCurrentTab(position)
}) }
)
TabLayoutMediator(binding.homeTabs, this, AdaptiveTabStrategy(context, homeModel)) TabLayoutMediator(binding.homeTabs, this, AdaptiveTabStrategy(context, homeModel))
.attach() .attach()
@ -197,7 +199,8 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
homeModel.updateCurrentSort( homeModel.updateCurrentSort(
homeModel homeModel
.getSortForDisplay(homeModel.currentTab.value) .getSortForDisplay(homeModel.currentTab.value)
.withAscending(item.isChecked)) .withAscending(item.isChecked)
)
} }
else -> { else -> {
// Sorting option was selected, mark it as selected and update the mode // Sorting option was selected, mark it as selected and update the mode
@ -205,7 +208,8 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
homeModel.updateCurrentSort( homeModel.updateCurrentSort(
homeModel homeModel
.getSortForDisplay(homeModel.currentTab.value) .getSortForDisplay(homeModel.currentTab.value)
.withMode(requireNotNull(Sort.Mode.fromItemId(item.itemId)))) .withMode(requireNotNull(Sort.Mode.fromItemId(item.itemId)))
)
} }
} }
@ -284,7 +288,7 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
binding.homeTabs.isVisible = true binding.homeTabs.isVisible = true
toolbarParams.scrollFlags = toolbarParams.scrollFlags =
AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL or AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL or
AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS
} }
} }
@ -440,9 +444,9 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
companion object { companion object {
private val VIEW_PAGER_RECYCLER_FIELD: Field by private val VIEW_PAGER_RECYCLER_FIELD: Field by
lazyReflectedField(ViewPager2::class, "mRecyclerView") lazyReflectedField(ViewPager2::class, "mRecyclerView")
private val VIEW_PAGER_TOUCH_SLOP_FIELD: Field by private val VIEW_PAGER_TOUCH_SLOP_FIELD: Field by
lazyReflectedField(RecyclerView::class, "mTouchSlop") lazyReflectedField(RecyclerView::class, "mTouchSlop")
private const val KEY_LAST_TRANSITION_AXIS = private const val KEY_LAST_TRANSITION_AXIS =
BuildConfig.APPLICATION_ID + ".key.LAST_TRANSITION_AXIS" BuildConfig.APPLICATION_ID + ".key.LAST_TRANSITION_AXIS"
} }

View file

@ -21,7 +21,6 @@ import android.os.Bundle
import android.text.format.DateUtils import android.text.format.DateUtils
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import java.util.Formatter
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentHomeListBinding import org.oxycblt.auxio.databinding.FragmentHomeListBinding
import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Album
@ -36,6 +35,7 @@ import org.oxycblt.auxio.ui.recycler.Item
import org.oxycblt.auxio.ui.recycler.MenuItemListener import org.oxycblt.auxio.ui.recycler.MenuItemListener
import org.oxycblt.auxio.ui.recycler.SyncListDiffer import org.oxycblt.auxio.ui.recycler.SyncListDiffer
import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.collectImmediately
import java.util.Formatter
/** /**
* A [HomeListFragment] for showing a list of [Album]s. * A [HomeListFragment] for showing a list of [Album]s.
@ -83,11 +83,12 @@ class AlbumListFragment : HomeListFragment<Album>() {
val dateAddedMillis = album.dateAdded.secsToMs() val dateAddedMillis = album.dateAdded.secsToMs()
formatterSb.setLength(0) formatterSb.setLength(0)
DateUtils.formatDateRange( DateUtils.formatDateRange(
context, context,
formatter, formatter,
dateAddedMillis, dateAddedMillis,
dateAddedMillis, dateAddedMillis,
DateUtils.FORMAT_ABBREV_ALL) DateUtils.FORMAT_ABBREV_ALL
)
.toString() .toString()
} }

View file

@ -21,7 +21,6 @@ import android.os.Bundle
import android.text.format.DateUtils import android.text.format.DateUtils
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import java.util.Formatter
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentHomeListBinding import org.oxycblt.auxio.databinding.FragmentHomeListBinding
import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.MusicParent
@ -38,6 +37,7 @@ import org.oxycblt.auxio.ui.recycler.SongViewHolder
import org.oxycblt.auxio.ui.recycler.SyncListDiffer import org.oxycblt.auxio.ui.recycler.SyncListDiffer
import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.context
import java.util.Formatter
/** /**
* A [HomeListFragment] for showing a list of [Song]s. * A [HomeListFragment] for showing a list of [Song]s.
@ -59,7 +59,11 @@ class SongListFragment : HomeListFragment<Song>() {
collectImmediately(homeModel.songs, homeAdapter::replaceList) collectImmediately(homeModel.songs, homeAdapter::replaceList)
collectImmediately( collectImmediately(
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::handlePlayback) playbackModel.song,
playbackModel.parent,
playbackModel.isPlaying,
::handlePlayback
)
} }
override fun getPopup(pos: Int): String? { override fun getPopup(pos: Int): String? {
@ -89,11 +93,12 @@ class SongListFragment : HomeListFragment<Song>() {
val dateAddedMillis = song.dateAdded.secsToMs() val dateAddedMillis = song.dateAdded.secsToMs()
formatterSb.setLength(0) formatterSb.setLength(0)
DateUtils.formatDateRange( DateUtils.formatDateRange(
context, context,
formatter, formatter,
dateAddedMillis, dateAddedMillis,
dateAddedMillis, dateAddedMillis,
DateUtils.FORMAT_ABBREV_ALL) DateUtils.FORMAT_ABBREV_ALL
)
.toString() .toString()
} }

View file

@ -66,7 +66,8 @@ sealed class Tab(open val mode: DisplayMode) {
DisplayMode.SHOW_SONGS, DisplayMode.SHOW_SONGS,
DisplayMode.SHOW_ALBUMS, DisplayMode.SHOW_ALBUMS,
DisplayMode.SHOW_ARTISTS, DisplayMode.SHOW_ARTISTS,
DisplayMode.SHOW_GENRES) DisplayMode.SHOW_GENRES
)
/** Convert an array [tabs] into a sequence of tabs. */ /** Convert an array [tabs] into a sequence of tabs. */
fun toSequence(tabs: Array<Tab>): Int { fun toSequence(tabs: Array<Tab>): Int {

View file

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

View file

@ -22,7 +22,6 @@ import android.graphics.Bitmap
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.graphics.Canvas import android.graphics.Canvas
import android.media.MediaMetadataRetriever import android.media.MediaMetadataRetriever
import android.util.Size as AndroidSize
import androidx.core.graphics.drawable.toDrawable import androidx.core.graphics.drawable.toDrawable
import coil.decode.DataSource import coil.decode.DataSource
import coil.decode.ImageSource import coil.decode.ImageSource
@ -38,8 +37,6 @@ import com.google.android.exoplayer2.MediaMetadata
import com.google.android.exoplayer2.MetadataRetriever import com.google.android.exoplayer2.MetadataRetriever
import com.google.android.exoplayer2.metadata.flac.PictureFrame import com.google.android.exoplayer2.metadata.flac.PictureFrame
import com.google.android.exoplayer2.metadata.id3.ApicFrame import com.google.android.exoplayer2.metadata.id3.ApicFrame
import java.io.ByteArrayInputStream
import java.io.InputStream
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import okio.buffer import okio.buffer
@ -48,6 +45,9 @@ import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logW import org.oxycblt.auxio.util.logW
import java.io.ByteArrayInputStream
import java.io.InputStream
import android.util.Size as AndroidSize
/** /**
* The base implementation for all image fetchers in Auxio. * The base implementation for all image fetchers in Auxio.
@ -194,7 +194,8 @@ abstract class BaseFetcher : Fetcher {
return SourceResult( return SourceResult(
source = ImageSource(stream.source().buffer(), context), source = ImageSource(stream.source().buffer(), context),
mimeType = null, mimeType = null,
dataSource = DataSource.DISK) dataSource = DataSource.DISK
)
} }
} }
@ -223,7 +224,9 @@ abstract class BaseFetcher : Fetcher {
// resolution. // resolution.
val bitmap = val bitmap =
SquareFrameTransform.INSTANCE.transform( SquareFrameTransform.INSTANCE.transform(
BitmapFactory.decodeStream(stream), mosaicFrameSize) BitmapFactory.decodeStream(stream),
mosaicFrameSize
)
canvas.drawBitmap(bitmap, x.toFloat(), y.toFloat(), null) canvas.drawBitmap(bitmap, x.toFloat(), y.toFloat(), null)
@ -241,7 +244,8 @@ abstract class BaseFetcher : Fetcher {
return DrawableResult( return DrawableResult(
drawable = mosaicBitmap.toDrawable(context.resources), drawable = mosaicBitmap.toDrawable(context.resources),
isSampled = true, isSampled = true,
dataSource = DataSource.DISK) dataSource = DataSource.DISK
)
} }
private fun Dimension.mosaicSize(): Int { private fun Dimension.mosaicSize(): Int {

View file

@ -70,8 +70,10 @@ class BitmapProvider(private val context: Context) {
if (guard.check(handle)) { if (guard.check(handle)) {
target.onCompleted(null) target.onCompleted(null)
} }
}) }
.transformations(SquareFrameTransform.INSTANCE)) )
.transformations(SquareFrameTransform.INSTANCE)
)
currentRequest = Request(context.imageLoader.enqueue(request.build()), target) currentRequest = Request(context.imageLoader.enqueue(request.build()), target)
} }

View file

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

View file

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

View file

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

View file

@ -21,11 +21,6 @@ package org.oxycblt.auxio.music
import android.content.Context import android.content.Context
import android.os.Parcelable import android.os.Parcelable
import java.security.MessageDigest
import java.util.UUID
import kotlin.math.max
import kotlin.math.min
import kotlin.reflect.KClass
import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.BuildConfig
@ -36,6 +31,11 @@ import org.oxycblt.auxio.ui.recycler.Item
import org.oxycblt.auxio.util.inRangeOrNull import org.oxycblt.auxio.util.inRangeOrNull
import org.oxycblt.auxio.util.nonZeroOrNull import org.oxycblt.auxio.util.nonZeroOrNull
import org.oxycblt.auxio.util.unlikelyToBeNull import org.oxycblt.auxio.util.unlikelyToBeNull
import java.security.MessageDigest
import java.util.UUID
import kotlin.math.max
import kotlin.math.min
import kotlin.reflect.KClass
// --- MUSIC MODELS --- // --- MUSIC MODELS ---
@ -164,13 +164,15 @@ class Song constructor(raw: Raw) : Music() {
val path = val path =
Path( Path(
name = requireNotNull(raw.displayName) { "Invalid raw: No display name" }, name = requireNotNull(raw.displayName) { "Invalid raw: No display name" },
parent = requireNotNull(raw.directory) { "Invalid raw: No parent directory" }) parent = requireNotNull(raw.directory) { "Invalid raw: No parent directory" }
)
/** The mime type of the audio file. Only intended for display. */ /** The mime type of the audio file. Only intended for display. */
val mimeType = val mimeType =
MimeType( MimeType(
fromExtension = requireNotNull(raw.extensionMimeType) { "Invalid raw: No mime type" }, fromExtension = requireNotNull(raw.extensionMimeType) { "Invalid raw: No mime type" },
fromFormat = raw.formatMimeType) fromFormat = raw.formatMimeType
)
/** The size of this audio file. */ /** The size of this audio file. */
val size = requireNotNull(raw.size) { "Invalid raw: No size" } val size = requireNotNull(raw.size) { "Invalid raw: No size" }
@ -234,11 +236,12 @@ class Song constructor(raw: Raw) : Music() {
date = raw.date, date = raw.date,
releaseType = raw.albumReleaseType, releaseType = raw.albumReleaseType,
rawArtist = rawArtist =
if (albumArtistName != null) { if (albumArtistName != null) {
Artist.Raw(albumArtistName, albumArtistSortName) Artist.Raw(albumArtistName, albumArtistSortName)
} else { } else {
Artist.Raw(artistName, artistSortName) Artist.Raw(artistName, artistSortName)
}) }
)
val _rawGenres = raw.genreNames?.map { Genre.Raw(it) } ?: listOf(Genre.Raw(null)) val _rawGenres = raw.genreNames?.map { Genre.Raw(it) } ?: listOf(Genre.Raw(null))
@ -505,7 +508,9 @@ fun MessageDigest.update(n: Long?) {
n.shr(32).toByte(), n.shr(32).toByte(),
n.shr(40).toByte(), n.shr(40).toByte(),
n.shl(48).toByte(), n.shl(48).toByte(),
n.shr(56).toByte())) n.shr(56).toByte()
)
)
} }
/** /**
@ -535,7 +540,8 @@ fun ByteArray.toUuid(): UUID {
.or(get(12).toLong().and(0xFF).shl(24)) .or(get(12).toLong().and(0xFF).shl(24))
.or(get(13).toLong().and(0xFF).shl(16)) .or(get(13).toLong().and(0xFF).shl(16))
.or(get(14).toLong().and(0xFF).shl(8)) .or(get(14).toLong().and(0xFF).shl(8))
.or(get(15).toLong().and(0xFF))) .or(get(15).toLong().and(0xFF))
)
} }
/** /**
@ -628,7 +634,8 @@ class Date private constructor(private val tokens: List<Int>) : Comparable<Date>
companion object { companion object {
private val ISO8601_REGEX = private val ISO8601_REGEX =
Regex( Regex(
"""^(\d{4,})([-.](\d{2})([-.](\d{2})([T ](\d{2})([:.](\d{2})([:.](\d{2}))?)?)?)?)?$""") """^(\d{4,})([-.](\d{2})([-.](\d{2})([T ](\d{2})([:.](\d{2})([:.](\d{2}))?)?)?)?)?$"""
)
fun from(year: Int) = fromTokens(listOf(year)) fun from(year: Int) = fromTokens(listOf(year))

View file

@ -97,7 +97,8 @@ class MusicStore private constructor() {
} }
} }
@Suppress("UNCHECKED_CAST") fun <T : Music> find(uid: Music.UID): T? = uidMap[uid] as? T @Suppress("UNCHECKED_CAST")
fun <T : Music> find(uid: Music.UID): T? = uidMap[uid] as? T
/** Sanitize an old item to find the corresponding item in a new library. */ /** Sanitize an old item to find the corresponding item in a new library. */
fun sanitize(song: Song) = find<Song>(song.uid) fun sanitize(song: Song) = find<Song>(song.uid)
@ -117,7 +118,7 @@ class MusicStore private constructor() {
/** Find a song for a [uri]. */ /** Find a song for a [uri]. */
fun findSongForUri(context: Context, uri: Uri) = fun findSongForUri(context: Context, uri: Uri) =
context.contentResolverSafe.useQuery(uri, arrayOf(OpenableColumns.DISPLAY_NAME)) { context.contentResolverSafe.useQuery(uri, arrayOf(OpenableColumns.DISPLAY_NAME)) {
cursor -> cursor ->
cursor.moveToFirst() cursor.moveToFirst()
val displayName = val displayName =

View file

@ -24,9 +24,9 @@ import android.database.Cursor
import android.net.Uri import android.net.Uri
import android.provider.MediaStore import android.provider.MediaStore
import android.text.format.DateUtils import android.text.format.DateUtils
import java.util.UUID
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import java.util.UUID
/** Shortcut for making a [ContentResolver] query with less superfluous arguments. */ /** Shortcut for making a [ContentResolver] query with less superfluous arguments. */
fun ContentResolver.queryCursor( fun ContentResolver.queryCursor(

View file

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

View file

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

View file

@ -31,3 +31,5 @@ class CacheLayer {
fun maybePopulateCachedRaw(raw: Song.Raw) = false fun maybePopulateCachedRaw(raw: Song.Raw) = false
} }
// TODO: Make raw naming consistent (always rawSong(s), not raw)

View file

@ -26,7 +26,6 @@ import android.provider.MediaStore
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.core.database.getIntOrNull import androidx.core.database.getIntOrNull
import androidx.core.database.getStringOrNull import androidx.core.database.getStringOrNull
import java.io.File
import org.oxycblt.auxio.music.Date import org.oxycblt.auxio.music.Date
import org.oxycblt.auxio.music.Directory import org.oxycblt.auxio.music.Directory
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
@ -38,6 +37,7 @@ import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.util.contentResolverSafe import org.oxycblt.auxio.util.contentResolverSafe
import org.oxycblt.auxio.util.getSystemServiceCompat import org.oxycblt.auxio.util.getSystemServiceCompat
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import java.io.File
/* /*
* This file acts as the base for most the black magic required to get a remotely sensible music * This file acts as the base for most the black magic required to get a remotely sensible music
@ -167,11 +167,13 @@ abstract class MediaStoreLayer(private val context: Context, private val cacheLa
val cursor = val cursor =
requireNotNull( requireNotNull(
context.contentResolverSafe.queryCursor( context.contentResolverSafe.queryCursor(
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
projection, projection,
selector, selector,
args.toTypedArray())) { "Content resolver failure: No Cursor returned" } args.toTypedArray()
)
) { "Content resolver failure: No Cursor returned" }
.also { cursor = it } .also { cursor = it }
idIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns._ID) idIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns._ID)
@ -250,7 +252,8 @@ abstract class MediaStoreLayer(private val context: Context, private val cacheLa
MediaStore.Audio.AudioColumns.ALBUM, MediaStore.Audio.AudioColumns.ALBUM,
MediaStore.Audio.AudioColumns.ALBUM_ID, MediaStore.Audio.AudioColumns.ALBUM_ID,
MediaStore.Audio.AudioColumns.ARTIST, MediaStore.Audio.AudioColumns.ARTIST,
AUDIO_COLUMN_ALBUM_ARTIST) AUDIO_COLUMN_ALBUM_ARTIST
)
abstract val dirSelector: String abstract val dirSelector: String
abstract fun addDirToSelectorArgs(dir: Directory, args: MutableList<String>): Boolean abstract fun addDirToSelectorArgs(dir: Directory, args: MutableList<String>): Boolean
@ -287,8 +290,8 @@ abstract class MediaStoreLayer(private val context: Context, private val cacheLa
raw.artistNames = raw.artistNames =
cursor.getString(artistIndex).run { cursor.getString(artistIndex).run {
if (this != MediaStore.UNKNOWN_STRING) { if (this != MediaStore.UNKNOWN_STRING) {
// While we can't natively parse multi-value tags, // While we can't natively parse multi-value tags from MediaStore itself, we
// from MediaStore itself, we can still parse by user-defined separators. // can still parse by user-defined separators.
maybeParseSeparators(settings) maybeParseSeparators(settings)
} else { } else {
null null
@ -412,7 +415,8 @@ open class BaseApi29MediaStoreLayer(context: Context, cacheLayer: CacheLayer) :
super.projection + super.projection +
arrayOf( arrayOf(
MediaStore.Audio.AudioColumns.VOLUME_NAME, MediaStore.Audio.AudioColumns.VOLUME_NAME,
MediaStore.Audio.AudioColumns.RELATIVE_PATH) MediaStore.Audio.AudioColumns.RELATIVE_PATH
)
override val dirSelector: String override val dirSelector: String
get() = get() =
@ -496,7 +500,8 @@ class Api30MediaStoreLayer(context: Context, cacheLayer: CacheLayer) :
super.projection + super.projection +
arrayOf( arrayOf(
MediaStore.Audio.AudioColumns.CD_TRACK_NUMBER, MediaStore.Audio.AudioColumns.CD_TRACK_NUMBER,
MediaStore.Audio.AudioColumns.DISC_NUMBER) MediaStore.Audio.AudioColumns.DISC_NUMBER
)
override fun buildRaw(cursor: Cursor, raw: Song.Raw) { override fun buildRaw(cursor: Cursor, raw: Song.Raw) {
super.buildRaw(cursor, raw) super.buildRaw(cursor, raw)

View file

@ -116,7 +116,8 @@ class Task(context: Context, private val settings: Settings, private val raw: So
private val future = private val future =
MetadataRetriever.retrieveMetadata( MetadataRetriever.retrieveMetadata(
context, context,
MediaItem.fromUri(requireNotNull(raw.mediaStoreId) { "Invalid raw: No id" }.audioUri)) MediaItem.fromUri(requireNotNull(raw.mediaStoreId) { "Invalid raw: No id" }.audioUri)
)
/** /**
* Get the song that this task is trying to complete. If the task is still busy, this will * Get the song that this task is trying to complete. If the task is still busy, this will
@ -213,9 +214,11 @@ class Task(context: Context, private val settings: Settings, private val raw: So
// 3. ID3v2.4 Release Date, as it is the second most common date type // 3. ID3v2.4 Release Date, as it is the second most common date type
// 4. ID3v2.3 Original Date, as it is like #1 // 4. ID3v2.3 Original Date, as it is like #1
// 5. ID3v2.3 Release Year, as it is the most common date type // 5. ID3v2.3 Release Year, as it is the most common date type
(tags["TDOR"]?.run { get(0).parseTimestamp() } (
tags["TDOR"]?.run { get(0).parseTimestamp() }
?: tags["TDRC"]?.run { get(0).parseTimestamp() } ?: tags["TDRC"]?.run { get(0).parseTimestamp() }
?: tags["TDRL"]?.run { get(0).parseTimestamp() } ?: parseId3v23Date(tags)) ?: tags["TDRL"]?.run { get(0).parseTimestamp() } ?: parseId3v23Date(tags)
)
?.let { raw.date = it } ?.let { raw.date = it }
// (Sort) Album // (Sort) Album
@ -279,9 +282,11 @@ class Task(context: Context, private val settings: Settings, private val raw: So
// 2. Date, as it is the most common date type // 2. Date, as it is the most common date type
// 3. Year, as old vorbis tags tended to use this (I know this because it's the only // 3. Year, as old vorbis tags tended to use this (I know this because it's the only
// tag that android supports, so it must be 15 years old or more!) // tag that android supports, so it must be 15 years old or more!)
(tags["ORIGINALDATE"]?.run { get(0).parseTimestamp() } (
tags["ORIGINALDATE"]?.run { get(0).parseTimestamp() }
?: tags["DATE"]?.run { get(0).parseTimestamp() } ?: tags["DATE"]?.run { get(0).parseTimestamp() }
?: tags["YEAR"]?.run { get(0).parseYear() }) ?: tags["YEAR"]?.run { get(0).parseYear() }
)
?.let { raw.date = it } ?.let { raw.date = it }
// (Sort) Album // (Sort) Album

View file

@ -370,4 +370,5 @@ private val GENRE_TABLE =
"Psybient", "Psybient",
// Auxio's extensions (Future garage is also based and deserves a slot) // Auxio's extensions (Future garage is also based and deserves a slot)
"Future Garage") "Future Garage"
)

View file

@ -152,7 +152,8 @@ class Indexer {
if (library != null) { if (library != null) {
logD( logD(
"Music indexing completed successfully in " + "Music indexing completed successfully in " +
"${System.currentTimeMillis() - start}ms") "${System.currentTimeMillis() - start}ms"
)
Response.Ok(library) Response.Ok(library)
} else { } else {
logE("No music found") logE("No music found")

View file

@ -58,7 +58,8 @@ class IndexingNotification(private val context: Context) :
if (indexing.current % 50 == 0) { if (indexing.current % 50 == 0) {
logD("Updating state to $indexing") logD("Updating state to $indexing")
setContentText( setContentText(
context.getString(R.string.fmt_indexing, indexing.current, indexing.total)) context.getString(R.string.fmt_indexing, indexing.current, indexing.total)
)
setProgress(indexing.total, indexing.current, false) setProgress(indexing.total, indexing.current, false)
return true return true
} }
@ -88,4 +89,6 @@ class ObservingNotification(context: Context) : ServiceNotification(context, IND
private val INDEXER_CHANNEL = private val INDEXER_CHANNEL =
ServiceNotification.ChannelInfo( ServiceNotification.ChannelInfo(
id = BuildConfig.APPLICATION_ID + ".channel.INDEXER", nameRes = R.string.lbl_indexer) id = BuildConfig.APPLICATION_ID + ".channel.INDEXER",
nameRes = R.string.lbl_indexer
)

View file

@ -78,7 +78,9 @@ class IndexerService : Service(), Indexer.Controller, Settings.Callback {
wakeLock = wakeLock =
getSystemServiceCompat(PowerManager::class) getSystemServiceCompat(PowerManager::class)
.newWakeLock( .newWakeLock(
PowerManager.PARTIAL_WAKE_LOCK, BuildConfig.APPLICATION_ID + ":IndexerService") PowerManager.PARTIAL_WAKE_LOCK,
BuildConfig.APPLICATION_ID + ":IndexerService"
)
settings = Settings(this, this) settings = Settings(this, this)
indexerContentObserver = SystemContentObserver() indexerContentObserver = SystemContentObserver()
@ -126,7 +128,8 @@ class IndexerService : Service(), Indexer.Controller, Settings.Callback {
when (state) { when (state) {
is Indexer.State.Complete -> { is Indexer.State.Complete -> {
if (state.response is Indexer.Response.Ok && if (state.response is Indexer.Response.Ok &&
state.response.library != musicStore.library) { state.response.library != musicStore.library
) {
logD("Applying new library") logD("Applying new library")
val newLibrary = state.response.library val newLibrary = state.response.library
@ -236,7 +239,10 @@ class IndexerService : Service(), Indexer.Controller, Settings.Callback {
) : ContentObserver(handler), Runnable { ) : ContentObserver(handler), Runnable {
init { init {
contentResolverSafe.registerContentObserver( contentResolverSafe.registerContentObserver(
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, true, this) MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
true,
this
)
} }
fun release() { fun release() {

View file

@ -122,7 +122,9 @@ class PlaybackPanelFragment :
val equalizerIntent = val equalizerIntent =
Intent(AudioEffect.ACTION_DISPLAY_AUDIO_EFFECT_CONTROL_PANEL) Intent(AudioEffect.ACTION_DISPLAY_AUDIO_EFFECT_CONTROL_PANEL)
.putExtra( .putExtra(
AudioEffect.EXTRA_AUDIO_SESSION, playbackModel.currentAudioSessionId) AudioEffect.EXTRA_AUDIO_SESSION,
playbackModel.currentAudioSessionId
)
.putExtra(AudioEffect.EXTRA_CONTENT_TYPE, AudioEffect.CONTENT_TYPE_MUSIC) .putExtra(AudioEffect.EXTRA_CONTENT_TYPE, AudioEffect.CONTENT_TYPE_MUSIC)
try { try {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -21,13 +21,13 @@ import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.widget.TextView import android.widget.TextView
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import kotlin.math.abs
import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.DialogPreAmpBinding import org.oxycblt.auxio.databinding.DialogPreAmpBinding
import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.ui.fragment.ViewBindingDialogFragment import org.oxycblt.auxio.ui.fragment.ViewBindingDialogFragment
import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.context
import kotlin.math.abs
/** /**
* The dialog for customizing the ReplayGain pre-amp values. * The dialog for customizing the ReplayGain pre-amp values.

View file

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

View file

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

View file

@ -116,7 +116,8 @@ class PlaybackStateDatabase private constructor(context: Context) :
queue = queue, queue = queue,
positionMs = rawState.positionMs, positionMs = rawState.positionMs,
repeatMode = rawState.repeatMode, repeatMode = rawState.repeatMode,
isShuffled = rawState.isShuffled) isShuffled = rawState.isShuffled
)
} }
private fun readRawState(): RawState? { private fun readRawState(): RawState? {
@ -139,11 +140,12 @@ class PlaybackStateDatabase private constructor(context: Context) :
index = cursor.getInt(indexIndex), index = cursor.getInt(indexIndex),
positionMs = cursor.getLong(posIndex), positionMs = cursor.getLong(posIndex),
repeatMode = RepeatMode.fromIntCode(cursor.getInt(repeatModeIndex)) repeatMode = RepeatMode.fromIntCode(cursor.getInt(repeatModeIndex))
?: RepeatMode.NONE, ?: RepeatMode.NONE,
isShuffled = cursor.getInt(shuffleIndex) == 1, isShuffled = cursor.getInt(shuffleIndex) == 1,
songUid = Music.UID.fromString(cursor.getString(songUidIndex)) songUid = Music.UID.fromString(cursor.getString(songUidIndex))
?: return@queryAll null, ?: return@queryAll null,
parentUid = cursor.getString(parentUidIndex)?.let(Music.UID::fromString)) parentUid = cursor.getString(parentUidIndex)?.let(Music.UID::fromString)
)
} }
} }
@ -179,7 +181,8 @@ class PlaybackStateDatabase private constructor(context: Context) :
repeatMode = state.repeatMode, repeatMode = state.repeatMode,
isShuffled = state.isShuffled, isShuffled = state.isShuffled,
songUid = state.queue[state.index].uid, songUid = state.queue[state.index].uid,
parentUid = state.parent?.uid) parentUid = state.parent?.uid
)
writeRawState(rawState) writeRawState(rawState)
writeQueue(state.queue) writeQueue(state.queue)

View file

@ -17,7 +17,6 @@
package org.oxycblt.auxio.playback.state package org.oxycblt.auxio.playback.state
import kotlin.math.max
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.BuildConfig
@ -31,6 +30,7 @@ import org.oxycblt.auxio.playback.state.PlaybackStateManager.Callback
import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logW import org.oxycblt.auxio.util.logW
import kotlin.math.max
/** /**
* Master class (and possible god object) for the playback state. * Master class (and possible god object) for the playback state.
@ -491,7 +491,8 @@ class PlaybackStateManager private constructor() {
queue = _queue, queue = _queue,
positionMs = playerState.calculateElapsedPosition(), positionMs = playerState.calculateElapsedPosition(),
isShuffled = isShuffled, isShuffled = isShuffled,
repeatMode = repeatMode) repeatMode = repeatMode
)
// --- CALLBACKS --- // --- CALLBACKS ---

View file

@ -140,16 +140,19 @@ class MediaSessionComponent(private val context: Context, private val callback:
.putText(MediaMetadataCompat.METADATA_KEY_ARTIST, artist) .putText(MediaMetadataCompat.METADATA_KEY_ARTIST, artist)
.putText( .putText(
MediaMetadataCompat.METADATA_KEY_ALBUM_ARTIST, MediaMetadataCompat.METADATA_KEY_ALBUM_ARTIST,
song.album.artist.resolveName(context)) song.album.artist.resolveName(context)
)
.putText(MediaMetadataCompat.METADATA_KEY_AUTHOR, artist) .putText(MediaMetadataCompat.METADATA_KEY_AUTHOR, artist)
.putText(MediaMetadataCompat.METADATA_KEY_COMPOSER, artist) .putText(MediaMetadataCompat.METADATA_KEY_COMPOSER, artist)
.putText(MediaMetadataCompat.METADATA_KEY_WRITER, artist) .putText(MediaMetadataCompat.METADATA_KEY_WRITER, artist)
.putText( .putText(
MediaMetadataCompat.METADATA_KEY_GENRE, MediaMetadataCompat.METADATA_KEY_GENRE,
song.genres.joinToString { it.resolveName(context) }) song.genres.joinToString { it.resolveName(context) }
)
.putText( .putText(
METADATA_KEY_PARENT, METADATA_KEY_PARENT,
parent?.resolveName(context) ?: context.getString(R.string.lbl_all_songs)) parent?.resolveName(context) ?: context.getString(R.string.lbl_all_songs)
)
.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, song.durationMs) .putLong(MediaMetadataCompat.METADATA_KEY_DURATION, song.durationMs)
song.track?.let { song.track?.let {
@ -186,7 +189,8 @@ class MediaSessionComponent(private val context: Context, private val callback:
notification.updateMetadata(metadata) notification.updateMetadata(metadata)
callback.onPostNotification(notification, PostingReason.METADATA) callback.onPostNotification(notification, PostingReason.METADATA)
} }
}) }
)
} }
private fun updateQueue(queue: List<Song>) { private fun updateQueue(queue: List<Song>) {
@ -223,7 +227,8 @@ class MediaSessionComponent(private val context: Context, private val callback:
RepeatMode.NONE -> PlaybackStateCompat.REPEAT_MODE_NONE RepeatMode.NONE -> PlaybackStateCompat.REPEAT_MODE_NONE
RepeatMode.TRACK -> PlaybackStateCompat.REPEAT_MODE_ONE RepeatMode.TRACK -> PlaybackStateCompat.REPEAT_MODE_ONE
RepeatMode.ALL -> PlaybackStateCompat.REPEAT_MODE_ALL RepeatMode.ALL -> PlaybackStateCompat.REPEAT_MODE_ALL
}) }
)
invalidateSecondaryAction() invalidateSecondaryAction()
} }
@ -234,7 +239,8 @@ class MediaSessionComponent(private val context: Context, private val callback:
PlaybackStateCompat.SHUFFLE_MODE_ALL PlaybackStateCompat.SHUFFLE_MODE_ALL
} else { } else {
PlaybackStateCompat.SHUFFLE_MODE_NONE PlaybackStateCompat.SHUFFLE_MODE_NONE
}) }
)
invalidateSecondaryAction() invalidateSecondaryAction()
} }
@ -320,7 +326,8 @@ class MediaSessionComponent(private val context: Context, private val callback:
playbackManager.reshuffle( playbackManager.reshuffle(
shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_ALL || shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_ALL ||
shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_GROUP, shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_GROUP,
settings) settings
)
} }
override fun onSkipToQueueItem(id: Long) { override fun onSkipToQueueItem(id: Long) {
@ -368,19 +375,22 @@ class MediaSessionComponent(private val context: Context, private val callback:
R.drawable.ic_shuffle_on_24 R.drawable.ic_shuffle_on_24
} else { } else {
R.drawable.ic_shuffle_off_24 R.drawable.ic_shuffle_off_24
}) }
)
} else { } else {
PlaybackStateCompat.CustomAction.Builder( PlaybackStateCompat.CustomAction.Builder(
PlaybackService.ACTION_INC_REPEAT_MODE, PlaybackService.ACTION_INC_REPEAT_MODE,
context.getString(R.string.desc_change_repeat), context.getString(R.string.desc_change_repeat),
playbackManager.repeatMode.icon) playbackManager.repeatMode.icon
)
} }
val exitAction = val exitAction =
PlaybackStateCompat.CustomAction.Builder( PlaybackStateCompat.CustomAction.Builder(
PlaybackService.ACTION_EXIT, PlaybackService.ACTION_EXIT,
context.getString(R.string.desc_exit), context.getString(R.string.desc_exit),
R.drawable.ic_close_24) R.drawable.ic_close_24
)
.build() .build()
state.addCustomAction(extraAction.build()) state.addCustomAction(extraAction.build())

View file

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

View file

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

View file

@ -109,7 +109,11 @@ class SearchFragment :
collectImmediately(searchModel.searchResults, ::handleResults) collectImmediately(searchModel.searchResults, ::handleResults)
collectImmediately( collectImmediately(
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::handlePlayback) playbackModel.song,
playbackModel.parent,
playbackModel.isPlaying,
::handlePlayback
)
collect(navModel.exploreNavigationItem, ::handleNavigation) collect(navModel.exploreNavigationItem, ::handleNavigation)
} }
@ -175,7 +179,8 @@ class SearchFragment :
is Artist -> SearchFragmentDirections.actionShowArtist(item.uid) is Artist -> SearchFragmentDirections.actionShowArtist(item.uid)
is Genre -> SearchFragmentDirections.actionShowGenre(item.uid) is Genre -> SearchFragmentDirections.actionShowGenre(item.uid)
else -> return else -> return
}) }
)
imm.hide() imm.hide()
} }

View file

@ -23,7 +23,6 @@ import androidx.annotation.IdRes
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import java.text.Normalizer
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -42,6 +41,7 @@ import org.oxycblt.auxio.ui.recycler.Item
import org.oxycblt.auxio.util.TaskGuard import org.oxycblt.auxio.util.TaskGuard
import org.oxycblt.auxio.util.application import org.oxycblt.auxio.util.application
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import java.text.Normalizer
/** /**
* The [ViewModel] for search functionality. * The [ViewModel] for search functionality.
@ -157,12 +157,12 @@ class SearchViewModel(application: Application) :
private inline fun <T : Music> List<T>.baseFilterBy(value: String, additional: (T) -> Boolean) = private inline fun <T : Music> List<T>.baseFilterBy(value: String, additional: (T) -> Boolean) =
filter { filter {
// The basic comparison is first by the *normalized* name, as that allows a // The basic comparison is first by the *normalized* name, as that allows a
// non-unicode search to match with some unicode characters. If that fails, // non-unicode search to match with some unicode characters. If that fails,
// filter impls have fallback values, primarily around sort tags or file names. // filter impls have fallback values, primarily around sort tags or file names.
it.resolveNameNormalized(application).contains(value, ignoreCase = true) || it.resolveNameNormalized(application).contains(value, ignoreCase = true) ||
additional(it) additional(it)
} }
.ifEmpty { null } .ifEmpty { null }
private fun Music.resolveNameNormalized(context: Context): String { private fun Music.resolveNameNormalized(context: Context): String {

View file

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

View file

@ -77,7 +77,8 @@ class Settings(private val context: Context, private val callback: Callback? = n
get() = get() =
inner.getInt( inner.getInt(
context.getString(R.string.set_key_theme), context.getString(R.string.set_key_theme),
AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM) AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
)
/** Whether the dark theme should be black or not */ /** Whether the dark theme should be black or not */
val useBlackTheme: Boolean val useBlackTheme: Boolean
@ -97,7 +98,8 @@ class Settings(private val context: Context, private val callback: Callback? = n
var libTabs: Array<Tab> var libTabs: Array<Tab>
get() = get() =
Tab.fromSequence( Tab.fromSequence(
inner.getInt(context.getString(R.string.set_key_lib_tabs), Tab.SEQUENCE_DEFAULT)) inner.getInt(context.getString(R.string.set_key_lib_tabs), Tab.SEQUENCE_DEFAULT)
)
?: unlikelyToBeNull(Tab.fromSequence(Tab.SEQUENCE_DEFAULT)) ?: unlikelyToBeNull(Tab.fromSequence(Tab.SEQUENCE_DEFAULT))
set(value) { set(value) {
inner.edit { inner.edit {
@ -122,7 +124,8 @@ class Settings(private val context: Context, private val callback: Callback? = n
val barAction: BarAction val barAction: BarAction
get() = get() =
BarAction.fromIntCode( BarAction.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)
)
?: BarAction.NEXT ?: BarAction.NEXT
/** /**
@ -140,7 +143,8 @@ class Settings(private val context: Context, private val callback: Callback? = n
val replayGainMode: ReplayGainMode val replayGainMode: ReplayGainMode
get() = get() =
ReplayGainMode.fromIntCode( ReplayGainMode.fromIntCode(
inner.getInt(context.getString(R.string.set_key_replay_gain), Int.MIN_VALUE)) inner.getInt(context.getString(R.string.set_key_replay_gain), Int.MIN_VALUE)
)
?: ReplayGainMode.DYNAMIC ?: ReplayGainMode.DYNAMIC
/** The current ReplayGain pre-amp configuration */ /** The current ReplayGain pre-amp configuration */
@ -148,7 +152,8 @@ class Settings(private val context: Context, private val callback: Callback? = n
get() = get() =
ReplayGainPreAmp( ReplayGainPreAmp(
inner.getFloat(context.getString(R.string.set_key_pre_amp_with), 0f), inner.getFloat(context.getString(R.string.set_key_pre_amp_with), 0f),
inner.getFloat(context.getString(R.string.set_key_pre_amp_without), 0f)) inner.getFloat(context.getString(R.string.set_key_pre_amp_without), 0f)
)
set(value) { set(value) {
inner.edit { inner.edit {
putFloat(context.getString(R.string.set_key_pre_amp_with), value.with) putFloat(context.getString(R.string.set_key_pre_amp_with), value.with)
@ -162,7 +167,10 @@ class Settings(private val context: Context, private val callback: Callback? = n
get() = get() =
PlaybackMode.fromInt( PlaybackMode.fromInt(
inner.getInt( 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
)
)
?: PlaybackMode.ALL_SONGS ?: PlaybackMode.ALL_SONGS
/** /**
@ -173,7 +181,10 @@ class Settings(private val context: Context, private val callback: Callback? = n
get() = get() =
PlaybackMode.fromInt( PlaybackMode.fromInt(
inner.getInt( 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. */ /** Whether shuffle should stay on when a new song is selected. */
val keepShuffle: Boolean val keepShuffle: Boolean
@ -201,7 +212,9 @@ class Settings(private val context: Context, private val callback: Callback? = n
.mapNotNull { Directory.fromDocumentUri(storageManager, it) } .mapNotNull { Directory.fromDocumentUri(storageManager, it) }
return MusicDirs( return MusicDirs(
dirs, 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. */ /** Set the list of directories that music should be hidden/loaded from. */
@ -209,9 +222,12 @@ class Settings(private val context: Context, private val callback: Callback? = n
inner.edit { inner.edit {
putStringSet( putStringSet(
context.getString(R.string.set_key_music_dirs), context.getString(R.string.set_key_music_dirs),
musicDirs.dirs.map(Directory::toDocumentUri).toSet()) musicDirs.dirs.map(Directory::toDocumentUri).toSet()
)
putBoolean( putBoolean(
context.getString(R.string.set_key_music_dirs_include), musicDirs.shouldInclude) context.getString(R.string.set_key_music_dirs_include),
musicDirs.shouldInclude
)
apply() apply()
} }
} }
@ -233,12 +249,14 @@ class Settings(private val context: Context, private val callback: Callback? = n
var searchFilterMode: DisplayMode? var searchFilterMode: DisplayMode?
get() = get() =
DisplayMode.fromInt( DisplayMode.fromInt(
inner.getInt(context.getString(R.string.set_key_search_filter), Int.MIN_VALUE)) inner.getInt(context.getString(R.string.set_key_search_filter), Int.MIN_VALUE)
)
set(value) { set(value) {
inner.edit { inner.edit {
putInt( putInt(
context.getString(R.string.set_key_search_filter), context.getString(R.string.set_key_search_filter),
value?.intCode ?: Int.MIN_VALUE) value?.intCode ?: Int.MIN_VALUE
)
apply() apply()
} }
} }
@ -247,7 +265,8 @@ class Settings(private val context: Context, private val callback: Callback? = n
var libSongSort: Sort var libSongSort: Sort
get() = get() =
Sort.fromIntCode( Sort.fromIntCode(
inner.getInt(context.getString(R.string.set_key_lib_songs_sort), Int.MIN_VALUE)) inner.getInt(context.getString(R.string.set_key_lib_songs_sort), Int.MIN_VALUE)
)
?: Sort(Sort.Mode.ByName, true) ?: Sort(Sort.Mode.ByName, true)
set(value) { set(value) {
inner.edit { inner.edit {
@ -260,7 +279,8 @@ class Settings(private val context: Context, private val callback: Callback? = n
var libAlbumSort: Sort var libAlbumSort: Sort
get() = get() =
Sort.fromIntCode( Sort.fromIntCode(
inner.getInt(context.getString(R.string.set_key_lib_albums_sort), Int.MIN_VALUE)) inner.getInt(context.getString(R.string.set_key_lib_albums_sort), Int.MIN_VALUE)
)
?: Sort(Sort.Mode.ByName, true) ?: Sort(Sort.Mode.ByName, true)
set(value) { set(value) {
inner.edit { inner.edit {
@ -273,7 +293,8 @@ class Settings(private val context: Context, private val callback: Callback? = n
var libArtistSort: Sort var libArtistSort: Sort
get() = get() =
Sort.fromIntCode( Sort.fromIntCode(
inner.getInt(context.getString(R.string.set_key_lib_artists_sort), Int.MIN_VALUE)) inner.getInt(context.getString(R.string.set_key_lib_artists_sort), Int.MIN_VALUE)
)
?: Sort(Sort.Mode.ByName, true) ?: Sort(Sort.Mode.ByName, true)
set(value) { set(value) {
inner.edit { inner.edit {
@ -286,7 +307,8 @@ class Settings(private val context: Context, private val callback: Callback? = n
var libGenreSort: Sort var libGenreSort: Sort
get() = get() =
Sort.fromIntCode( Sort.fromIntCode(
inner.getInt(context.getString(R.string.set_key_lib_genres_sort), Int.MIN_VALUE)) inner.getInt(context.getString(R.string.set_key_lib_genres_sort), Int.MIN_VALUE)
)
?: Sort(Sort.Mode.ByName, true) ?: Sort(Sort.Mode.ByName, true)
set(value) { set(value) {
inner.edit { inner.edit {
@ -301,7 +323,10 @@ class Settings(private val context: Context, private val callback: Callback? = n
var sort = var sort =
Sort.fromIntCode( Sort.fromIntCode(
inner.getInt( inner.getInt(
context.getString(R.string.set_key_detail_album_sort), Int.MIN_VALUE)) context.getString(R.string.set_key_detail_album_sort),
Int.MIN_VALUE
)
)
?: Sort(Sort.Mode.ByDisc, true) ?: Sort(Sort.Mode.ByDisc, true)
// Correct legacy album sort modes to Disc // Correct legacy album sort modes to Disc
@ -322,7 +347,8 @@ class Settings(private val context: Context, private val callback: Callback? = n
var detailArtistSort: Sort var detailArtistSort: Sort
get() = get() =
Sort.fromIntCode( Sort.fromIntCode(
inner.getInt(context.getString(R.string.set_key_detail_artist_sort), Int.MIN_VALUE)) inner.getInt(context.getString(R.string.set_key_detail_artist_sort), Int.MIN_VALUE)
)
?: Sort(Sort.Mode.ByYear, false) ?: Sort(Sort.Mode.ByYear, false)
set(value) { set(value) {
inner.edit { inner.edit {
@ -335,7 +361,8 @@ class Settings(private val context: Context, private val callback: Callback? = n
var detailGenreSort: Sort var detailGenreSort: Sort
get() = get() =
Sort.fromIntCode( Sort.fromIntCode(
inner.getInt(context.getString(R.string.set_key_detail_genre_sort), Int.MIN_VALUE)) inner.getInt(context.getString(R.string.set_key_detail_genre_sort), Int.MIN_VALUE)
)
?: Sort(Sort.Mode.ByName, true) ?: Sort(Sort.Mode.ByName, true)
set(value) { set(value) {
inner.edit { inner.edit {

View file

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

View file

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

View file

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

View file

@ -146,7 +146,8 @@ data class Sort(val mode: Mode, val isAscending: Boolean) {
compareByDynamic(ascending, BasicComparator.ALBUM) { it.album }, compareByDynamic(ascending, BasicComparator.ALBUM) { it.album },
compareBy(NullableComparator.INT) { it.disc }, compareBy(NullableComparator.INT) { it.disc },
compareBy(NullableComparator.INT) { it.track }, compareBy(NullableComparator.INT) { it.track },
compareBy(BasicComparator.SONG)) compareBy(BasicComparator.SONG)
)
} }
/** Sort by the artist of an item, only supported by [Album] and [Song] */ /** Sort by the artist of an item, only supported by [Album] and [Song] */
@ -164,13 +165,15 @@ data class Sort(val mode: Mode, val isAscending: Boolean) {
compareByDescending(BasicComparator.ALBUM) { it.album }, compareByDescending(BasicComparator.ALBUM) { it.album },
compareBy(NullableComparator.INT) { it.disc }, compareBy(NullableComparator.INT) { it.disc },
compareBy(NullableComparator.INT) { it.track }, compareBy(NullableComparator.INT) { it.track },
compareBy(BasicComparator.SONG)) compareBy(BasicComparator.SONG)
)
override fun getAlbumComparator(ascending: Boolean): Comparator<Album> = override fun getAlbumComparator(ascending: Boolean): Comparator<Album> =
MultiComparator( MultiComparator(
compareByDynamic(ascending, BasicComparator.ARTIST) { it.artist }, compareByDynamic(ascending, BasicComparator.ARTIST) { it.artist },
compareByDescending(NullableComparator.DATE) { it.date }, compareByDescending(NullableComparator.DATE) { it.date },
compareBy(BasicComparator.ALBUM)) compareBy(BasicComparator.ALBUM)
)
} }
/** Sort by the year of an item, only supported by [Album] and [Song] */ /** Sort by the year of an item, only supported by [Album] and [Song] */
@ -187,12 +190,14 @@ data class Sort(val mode: Mode, val isAscending: Boolean) {
compareByDescending(BasicComparator.ALBUM) { it.album }, compareByDescending(BasicComparator.ALBUM) { it.album },
compareBy(NullableComparator.INT) { it.disc }, compareBy(NullableComparator.INT) { it.disc },
compareBy(NullableComparator.INT) { it.track }, compareBy(NullableComparator.INT) { it.track },
compareBy(BasicComparator.SONG)) compareBy(BasicComparator.SONG)
)
override fun getAlbumComparator(ascending: Boolean): Comparator<Album> = override fun getAlbumComparator(ascending: Boolean): Comparator<Album> =
MultiComparator( MultiComparator(
compareByDynamic(ascending, NullableComparator.DATE) { it.date }, compareByDynamic(ascending, NullableComparator.DATE) { it.date },
compareBy(BasicComparator.ALBUM)) compareBy(BasicComparator.ALBUM)
)
} }
/** Sort by the duration of the item. Supports all items. */ /** Sort by the duration of the item. Supports all items. */
@ -205,20 +210,27 @@ data class Sort(val mode: Mode, val isAscending: Boolean) {
override fun getSongComparator(ascending: Boolean): Comparator<Song> = override fun getSongComparator(ascending: Boolean): Comparator<Song> =
MultiComparator( MultiComparator(
compareByDynamic(ascending) { it.durationMs }, compareBy(BasicComparator.SONG)) compareByDynamic(ascending) { it.durationMs },
compareBy(BasicComparator.SONG)
)
override fun getAlbumComparator(ascending: Boolean): Comparator<Album> = override fun getAlbumComparator(ascending: Boolean): Comparator<Album> =
MultiComparator( MultiComparator(
compareByDynamic(ascending) { it.durationMs }, compareBy(BasicComparator.ALBUM)) compareByDynamic(ascending) { it.durationMs },
compareBy(BasicComparator.ALBUM)
)
override fun getArtistComparator(ascending: Boolean): Comparator<Artist> = override fun getArtistComparator(ascending: Boolean): Comparator<Artist> =
MultiComparator( MultiComparator(
compareByDynamic(ascending) { it.durationMs }, compareByDynamic(ascending) { it.durationMs },
compareBy(BasicComparator.ARTIST)) compareBy(BasicComparator.ARTIST)
)
override fun getGenreComparator(ascending: Boolean): Comparator<Genre> = override fun getGenreComparator(ascending: Boolean): Comparator<Genre> =
MultiComparator( MultiComparator(
compareByDynamic(ascending) { it.durationMs }, compareBy(BasicComparator.GENRE)) compareByDynamic(ascending) { it.durationMs },
compareBy(BasicComparator.GENRE)
)
} }
/** Sort by the amount of songs. Only applicable to music parents. */ /** Sort by the amount of songs. Only applicable to music parents. */
@ -231,16 +243,21 @@ data class Sort(val mode: Mode, val isAscending: Boolean) {
override fun getAlbumComparator(ascending: Boolean): Comparator<Album> = override fun getAlbumComparator(ascending: Boolean): Comparator<Album> =
MultiComparator( MultiComparator(
compareByDynamic(ascending) { it.songs.size }, compareBy(BasicComparator.ALBUM)) compareByDynamic(ascending) { it.songs.size },
compareBy(BasicComparator.ALBUM)
)
override fun getArtistComparator(ascending: Boolean): Comparator<Artist> = override fun getArtistComparator(ascending: Boolean): Comparator<Artist> =
MultiComparator( MultiComparator(
compareByDynamic(ascending) { it.songs.size }, compareByDynamic(ascending) { it.songs.size },
compareBy(BasicComparator.ARTIST)) compareBy(BasicComparator.ARTIST)
)
override fun getGenreComparator(ascending: Boolean): Comparator<Genre> = override fun getGenreComparator(ascending: Boolean): Comparator<Genre> =
MultiComparator( MultiComparator(
compareByDynamic(ascending) { it.songs.size }, 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]. */ /** Sort by the disc, and then track number of an item. Only supported by [Song]. */
@ -255,7 +272,8 @@ data class Sort(val mode: Mode, val isAscending: Boolean) {
MultiComparator( MultiComparator(
compareByDynamic(ascending, NullableComparator.INT) { it.disc }, compareByDynamic(ascending, NullableComparator.INT) { it.disc },
compareBy(NullableComparator.INT) { it.track }, compareBy(NullableComparator.INT) { it.track },
compareBy(BasicComparator.SONG)) compareBy(BasicComparator.SONG)
)
} }
/** /**
@ -273,7 +291,8 @@ data class Sort(val mode: Mode, val isAscending: Boolean) {
MultiComparator( MultiComparator(
compareBy(NullableComparator.INT) { it.disc }, compareBy(NullableComparator.INT) { it.disc },
compareByDynamic(ascending, NullableComparator.INT) { it.track }, compareByDynamic(ascending, NullableComparator.INT) { it.track },
compareBy(BasicComparator.SONG)) compareBy(BasicComparator.SONG)
)
} }
/** Sort by the time the item was added. Only supported by [Song] */ /** Sort by the time the item was added. Only supported by [Song] */
@ -286,12 +305,15 @@ data class Sort(val mode: Mode, val isAscending: Boolean) {
override fun getSongComparator(ascending: Boolean): Comparator<Song> = override fun getSongComparator(ascending: Boolean): Comparator<Song> =
MultiComparator( MultiComparator(
compareByDynamic(ascending) { it.dateAdded }, compareBy(BasicComparator.SONG)) compareByDynamic(ascending) { it.dateAdded },
compareBy(BasicComparator.SONG)
)
override fun getAlbumComparator(ascending: Boolean): Comparator<Album> = override fun getAlbumComparator(ascending: Boolean): Comparator<Album> =
MultiComparator( MultiComparator(
compareByDynamic(ascending) { album -> album.songs.minOf { it.dateAdded } }, compareByDynamic(ascending) { album -> album.songs.minOf { it.dateAdded } },
compareBy(BasicComparator.ALBUM)) compareBy(BasicComparator.ALBUM)
)
} }
protected inline fun <T : Music, K> compareByDynamic( protected inline fun <T : Music, K> compareByDynamic(

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -122,7 +122,8 @@ class ArtistViewHolder private constructor(private val binding: ItemParentBindin
binding.context.getString( binding.context.getString(
R.string.fmt_two, R.string.fmt_two,
binding.context.getPlural(R.plurals.fmt_album_count, item.albums.size), binding.context.getPlural(R.plurals.fmt_album_count, item.albums.size),
binding.context.getPlural(R.plurals.fmt_song_count, item.songs.size)) binding.context.getPlural(R.plurals.fmt_song_count, item.songs.size)
)
// binding.parentMenu.setOnClickListener { listener.onOpenMenu(item, it) } // binding.parentMenu.setOnClickListener { listener.onOpenMenu(item, it) }
binding.root.setOnLongClickListener { binding.root.setOnLongClickListener {
listener.onOpenMenu(item, it) listener.onOpenMenu(item, it)

View file

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

View file

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

View file

@ -82,11 +82,13 @@ private val Any.autoTag: String
@Suppress("KotlinConstantConditions") @Suppress("KotlinConstantConditions")
private fun basedCopyleftNotice(): Boolean { private fun basedCopyleftNotice(): Boolean {
if (BuildConfig.APPLICATION_ID != "org.oxycblt.auxio" && if (BuildConfig.APPLICATION_ID != "org.oxycblt.auxio" &&
BuildConfig.APPLICATION_ID != "org.oxycblt.auxio.debug") { BuildConfig.APPLICATION_ID != "org.oxycblt.auxio.debug"
) {
Log.d( Log.d(
"Auxio Project", "Auxio Project",
"Friendly reminder: Auxio is licensed under the " + "Friendly reminder: Auxio is licensed under the " +
"GPLv3 and all derivative apps must be made open source!") "GPLv3 and all derivative apps must be made open source!"
)
return true return true
} }

View file

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

View file

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

View file

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

View file

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

View file

@ -204,14 +204,14 @@
<string name="set_rewind_prev_desc">Rewind before skipping to the previous song</string> <string name="set_rewind_prev_desc">Rewind before skipping to the previous song</string>
<string name="set_repeat_pause">Pause on repeat</string> <string name="set_repeat_pause">Pause on repeat</string>
<string name="set_repeat_pause_desc">Pause when a song repeats</string> <string name="set_repeat_pause_desc">Pause when a song repeats</string>
<string name="set_content">Content</string>
<string name="set_save_state">Save playback state</string> <string name="set_save_state">Save playback state</string>
<string name="set_save_desc">Save the current playback state now</string> <string name="set_save_desc">Save the current playback state now</string>
<string name="set_wipe_state">Clear playback state</string> <string name="set_wipe_state">Clear playback state</string>
<string name="set_wipe_desc">Clear the previously saved playback state (if any)</string> <string name="set_wipe_desc">Clear the previously saved playback state (if any)</string>
<string name="set_restore_state">Restore playback state</string> <string name="set_restore_state">Restore playback state</string>
<string name="set_restore_desc">Restore the previously saved playback state (if any)</string> <string name="set_restore_desc">Restore the previously saved playback state (if any)</string>
<string name="set_content">Content</string>
<string name="set_reindex">Reload music</string> <string name="set_reindex">Reload music</string>
<string name="set_reindex_desc">May wipe playback state</string> <string name="set_reindex_desc">May wipe playback state</string>
<string name="set_dirs">Music folders</string> <string name="set_dirs">Music folders</string>