util: clean up context utils

Clean up the context utils to be more appropriately designed and
efficient.
This commit is contained in:
OxygenCobalt 2022-08-04 20:11:25 -06:00
parent 96be8cb6b7
commit 7d04aad9b7
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
48 changed files with 246 additions and 343 deletions

View file

@ -55,6 +55,7 @@ class MainFragment :
private val navModel: NavigationViewModel by activityViewModels() private val navModel: NavigationViewModel by activityViewModels()
private var callback: DynamicBackPressedCallback? = null private var callback: DynamicBackPressedCallback? = null
private var lastInsets: WindowInsets? = null private var lastInsets: WindowInsets? = null
private var elevationNormal = -1f
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -66,10 +67,13 @@ class MainFragment :
override fun onBindingCreated(binding: FragmentMainBinding, savedInstanceState: Bundle?) { override fun onBindingCreated(binding: FragmentMainBinding, savedInstanceState: Bundle?) {
// --- UI SETUP --- // --- UI SETUP ---
requireActivity() val context = requireActivity()
.onBackPressedDispatcher.addCallback(
context.onBackPressedDispatcher.addCallback(
viewLifecycleOwner, DynamicBackPressedCallback().also { callback = it }) viewLifecycleOwner, DynamicBackPressedCallback().also { callback = it })
elevationNormal = requireContext().getDimen(R.dimen.elevation_normal)
binding.root.setOnApplyWindowInsetsListener { _, insets -> binding.root.setOnApplyWindowInsetsListener { _, insets ->
lastInsets = insets lastInsets = insets
insets insets
@ -77,8 +81,9 @@ class MainFragment :
// Send meaningful accessibility events for bottom sheets // Send meaningful accessibility events for bottom sheets
ViewCompat.setAccessibilityPaneTitle( ViewCompat.setAccessibilityPaneTitle(
binding.playbackSheet, getString(R.string.lbl_playback)) binding.playbackSheet, context.getString(R.string.lbl_playback))
ViewCompat.setAccessibilityPaneTitle(binding.queueSheet, getString(R.string.lbl_queue)) ViewCompat.setAccessibilityPaneTitle(
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) {
@ -97,8 +102,8 @@ class MainFragment :
binding.queueSheet.apply { binding.queueSheet.apply {
background = background =
MaterialShapeDrawable.createWithElevationOverlay(context).apply { MaterialShapeDrawable.createWithElevationOverlay(context).apply {
fillColor = context.getAttrColorSafe(R.attr.colorSurface).stateList fillColor = context.getAttrColorCompat(R.attr.colorSurface)
elevation = context.getDimenSafe(R.dimen.elevation_normal) elevation = context.getDimen(R.dimen.elevation_normal)
} }
setOnApplyWindowInsetsListener { v, insets -> setOnApplyWindowInsetsListener { v, insets ->
@ -182,8 +187,7 @@ class MainFragment :
isInvisible = alpha == 0f isInvisible = alpha == 0f
} }
binding.playbackSheet.translationZ = binding.playbackSheet.translationZ = elevationNormal * outPlaybackRatio
requireContext().getDimenSafe(R.dimen.elevation_normal) * outPlaybackRatio
playbackSheetBehavior.sheetBackgroundDrawable.alpha = (outPlaybackRatio * 255).toInt() playbackSheetBehavior.sheetBackgroundDrawable.alpha = (outPlaybackRatio * 255).toInt()
binding.playbackBarFragment.apply { binding.playbackBarFragment.apply {

View file

@ -43,7 +43,6 @@ import org.oxycblt.auxio.ui.Sort
import org.oxycblt.auxio.ui.fragment.MenuFragment import org.oxycblt.auxio.ui.fragment.MenuFragment
import org.oxycblt.auxio.ui.recycler.Header import org.oxycblt.auxio.ui.recycler.Header
import org.oxycblt.auxio.ui.recycler.Item import org.oxycblt.auxio.ui.recycler.Item
import org.oxycblt.auxio.util.applySpans
import org.oxycblt.auxio.util.canScroll import org.oxycblt.auxio.util.canScroll
import org.oxycblt.auxio.util.collect import org.oxycblt.auxio.util.collect
import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.collectImmediately
@ -88,9 +87,9 @@ class AlbumDetailFragment :
binding.detailRecycler.apply { binding.detailRecycler.apply {
adapter = detailAdapter adapter = detailAdapter
applySpans { pos -> setSpanSizeLookup { pos ->
val item = detailAdapter.data.currentList[pos] val item = detailAdapter.data.getItem(pos)
item is Header || item is SortHeader || item is Album item is Album || item is Header || item is SortHeader
} }
} }
@ -240,7 +239,7 @@ class AlbumDetailFragment :
// 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
// that case. // that case.
binding.detailAppbar.isLifted = binding.detailRecycler.canScroll binding.detailAppbar.isLifted = binding.detailRecycler.canScroll()
} }
} }
} }

View file

@ -41,7 +41,6 @@ import org.oxycblt.auxio.ui.Sort
import org.oxycblt.auxio.ui.fragment.MenuFragment import org.oxycblt.auxio.ui.fragment.MenuFragment
import org.oxycblt.auxio.ui.recycler.Header import org.oxycblt.auxio.ui.recycler.Header
import org.oxycblt.auxio.ui.recycler.Item import org.oxycblt.auxio.ui.recycler.Item
import org.oxycblt.auxio.util.applySpans
import org.oxycblt.auxio.util.collect import org.oxycblt.auxio.util.collect
import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.context
@ -83,10 +82,9 @@ class ArtistDetailFragment :
binding.detailRecycler.apply { binding.detailRecycler.apply {
adapter = detailAdapter adapter = detailAdapter
applySpans { pos -> setSpanSizeLookup { pos ->
// If the item is an ActionHeader we need to also make the item full-width val item = detailAdapter.data.getItem(pos)
val item = detailAdapter.data.currentList[pos] item is Artist || item is Header || item is SortHeader
item is Header || item is SortHeader || item is Artist
} }
} }

View file

@ -29,13 +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.Exception
import java.lang.reflect.Field 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 org.oxycblt.auxio.util.logE
import org.oxycblt.auxio.util.logTraceOrThrow
/** /**
* 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
@ -68,14 +65,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
val toolbar = findViewById<Toolbar>(R.id.detail_toolbar) val toolbar = findViewById<Toolbar>(R.id.detail_toolbar)
// Reflect to get the actual title view to do transformations on // Reflect to get the actual title view to do transformations on
val newTitleView = val newTitleView = TOOLBAR_TITLE_TEXT_FIELD.get(toolbar) as AppCompatTextView
try {
TOOLBAR_TITLE_TEXT_FIELD.get(toolbar) as AppCompatTextView
} catch (e: Exception) {
logE("Could not get toolbar title view (likely an internal code change)")
e.logTraceOrThrow()
return null
}
newTitleView.alpha = 0f newTitleView.alpha = 0f
this.titleView = newTitleView this.titleView = newTitleView

View file

@ -42,7 +42,6 @@ import org.oxycblt.auxio.ui.Sort
import org.oxycblt.auxio.ui.fragment.MenuFragment import org.oxycblt.auxio.ui.fragment.MenuFragment
import org.oxycblt.auxio.ui.recycler.Header import org.oxycblt.auxio.ui.recycler.Header
import org.oxycblt.auxio.ui.recycler.Item import org.oxycblt.auxio.ui.recycler.Item
import org.oxycblt.auxio.util.applySpans
import org.oxycblt.auxio.util.collect import org.oxycblt.auxio.util.collect
import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.context
@ -84,9 +83,9 @@ class GenreDetailFragment :
binding.detailRecycler.apply { binding.detailRecycler.apply {
adapter = detailAdapter adapter = detailAdapter
applySpans { pos -> setSpanSizeLookup { pos ->
val item = detailAdapter.data.currentList[pos] val item = detailAdapter.data.getItem(pos)
item is Header || item is SortHeader || item is Genre item is Genre || item is Header || item is SortHeader
} }
} }

View file

@ -64,14 +64,12 @@ class SongDetailDialog : ViewBindingDialogFragment<DialogSongDetailBinding>() {
if (song != null) { if (song != null) {
if (song.info != null) { if (song.info != null) {
val context = requireContext()
binding.detailContainer.isGone = false binding.detailContainer.isGone = false
binding.detailFileName.setText(song.song.path.name) binding.detailFileName.setText(song.song.path.name)
binding.detailRelativeDir.setText( binding.detailRelativeDir.setText(song.song.path.parent.resolveName(context))
song.song.path.parent.resolveName(requireContext())) binding.detailFormat.setText(song.info.resolvedMimeType.resolveName(context))
binding.detailFormat.setText( binding.detailSize.setText(Formatter.formatFileSize(context, song.song.size))
song.info.resolvedMimeType.resolveName(requireContext()))
binding.detailSize.setText(
Formatter.formatFileSize(requireContext(), song.song.size))
binding.detailDuration.setText(song.song.durationSecs.formatDuration(true)) binding.detailDuration.setText(song.song.durationSecs.formatDuration(true))
if (song.info.bitrateKbps != null) { if (song.info.bitrateKbps != null) {

View file

@ -34,7 +34,7 @@ import org.oxycblt.auxio.ui.recycler.MenuItemListener
import org.oxycblt.auxio.ui.recycler.SimpleItemCallback import org.oxycblt.auxio.ui.recycler.SimpleItemCallback
import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.context
import org.oxycblt.auxio.util.formatDuration import org.oxycblt.auxio.util.formatDuration
import org.oxycblt.auxio.util.getPluralSafe import org.oxycblt.auxio.util.getPlural
import org.oxycblt.auxio.util.inflater import org.oxycblt.auxio.util.inflater
/** /**
@ -127,7 +127,7 @@ private class AlbumDetailViewHolder private constructor(private val binding: Ite
item.date?.let { context.getString(R.string.fmt_number, it.year) } item.date?.let { context.getString(R.string.fmt_number, it.year) }
?: context.getString(R.string.def_date) ?: context.getString(R.string.def_date)
val songCount = context.getPluralSafe(R.plurals.fmt_song_count, item.songs.size) val songCount = context.getPlural(R.plurals.fmt_song_count, item.songs.size)
val duration = item.durationSecs.formatDuration(true) val duration = item.durationSecs.formatDuration(true)

View file

@ -34,7 +34,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.SimpleItemCallback import org.oxycblt.auxio.ui.recycler.SimpleItemCallback
import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.context
import org.oxycblt.auxio.util.getPluralSafe import org.oxycblt.auxio.util.getPlural
import org.oxycblt.auxio.util.inflater import org.oxycblt.auxio.util.inflater
/** /**
@ -138,8 +138,8 @@ private class ArtistDetailViewHolder private constructor(private val binding: It
binding.detailInfo.text = binding.detailInfo.text =
binding.context.getString( binding.context.getString(
R.string.fmt_two, R.string.fmt_two,
binding.context.getPluralSafe(R.plurals.fmt_album_count, item.albums.size), binding.context.getPlural(R.plurals.fmt_album_count, item.albums.size),
binding.context.getPluralSafe(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

@ -32,7 +32,7 @@ import org.oxycblt.auxio.ui.recycler.SimpleItemCallback
import org.oxycblt.auxio.ui.recycler.SongViewHolder import org.oxycblt.auxio.ui.recycler.SongViewHolder
import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.context
import org.oxycblt.auxio.util.formatDuration import org.oxycblt.auxio.util.formatDuration
import org.oxycblt.auxio.util.getPluralSafe import org.oxycblt.auxio.util.getPlural
import org.oxycblt.auxio.util.inflater import org.oxycblt.auxio.util.inflater
/** /**
@ -106,7 +106,7 @@ private class GenreDetailViewHolder private constructor(private val binding: Ite
binding.detailCover.bind(item) binding.detailCover.bind(item)
binding.detailName.text = item.resolveName(binding.context) binding.detailName.text = item.resolveName(binding.context)
binding.detailSubhead.text = binding.detailSubhead.text =
binding.context.getPluralSafe(R.plurals.fmt_song_count, item.songs.size) binding.context.getPlural(R.plurals.fmt_song_count, item.songs.size)
binding.detailInfo.text = item.durationSecs.formatDuration(false) binding.detailInfo.text = item.durationSecs.formatDuration(false)
binding.detailPlayButton.setOnClickListener { listener.onPlayParent() } binding.detailPlayButton.setOnClickListener { listener.onPlayParent() }
binding.detailShuffleButton.setOnClickListener { listener.onShuffleParent() } binding.detailShuffleButton.setOnClickListener { listener.onShuffleParent() }

View file

@ -106,7 +106,7 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
// Load the track color in manually as it's unclear whether the track actually supports // Load the track color in manually as it's unclear whether the track actually supports
// using a ColorStateList in the resources // using a ColorStateList in the resources
binding.homeIndexingProgress.trackColor = binding.homeIndexingProgress.trackColor =
requireContext().getColorStateListSafe(R.color.sel_track).defaultColor requireContext().getColorCompat(R.color.sel_track).defaultColor
binding.homePager.apply { binding.homePager.apply {
adapter = HomePagerAdapter() adapter = HomePagerAdapter()
@ -279,6 +279,8 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
binding.homeFab.show() binding.homeFab.show()
binding.homeIndexingContainer.visibility = View.INVISIBLE binding.homeIndexingContainer.visibility = View.INVISIBLE
} else { } else {
val context = requireContext()
binding.homeIndexingContainer.visibility = View.VISIBLE binding.homeIndexingContainer.visibility = View.VISIBLE
logD("Received non-ok response $response") logD("Received non-ok response $response")
@ -286,28 +288,28 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
when (response) { when (response) {
is Indexer.Response.Err -> { is Indexer.Response.Err -> {
binding.homeIndexingProgress.visibility = View.INVISIBLE binding.homeIndexingProgress.visibility = View.INVISIBLE
binding.homeIndexingStatus.text = getString(R.string.err_index_failed) binding.homeIndexingStatus.text = context.getString(R.string.err_index_failed)
binding.homeIndexingAction.apply { binding.homeIndexingAction.apply {
visibility = View.VISIBLE visibility = View.VISIBLE
text = getString(R.string.lbl_retry) text = context.getString(R.string.lbl_retry)
setOnClickListener { musicModel.reindex() } setOnClickListener { musicModel.reindex() }
} }
} }
is Indexer.Response.NoMusic -> { is Indexer.Response.NoMusic -> {
binding.homeIndexingProgress.visibility = View.INVISIBLE binding.homeIndexingProgress.visibility = View.INVISIBLE
binding.homeIndexingStatus.text = getString(R.string.err_no_music) binding.homeIndexingStatus.text = context.getString(R.string.err_no_music)
binding.homeIndexingAction.apply { binding.homeIndexingAction.apply {
visibility = View.VISIBLE visibility = View.VISIBLE
text = getString(R.string.lbl_retry) text = context.getString(R.string.lbl_retry)
setOnClickListener { musicModel.reindex() } setOnClickListener { musicModel.reindex() }
} }
} }
is Indexer.Response.NoPerms -> { is Indexer.Response.NoPerms -> {
binding.homeIndexingProgress.visibility = View.INVISIBLE binding.homeIndexingProgress.visibility = View.INVISIBLE
binding.homeIndexingStatus.text = getString(R.string.err_no_perms) binding.homeIndexingStatus.text = context.getString(R.string.err_no_perms)
binding.homeIndexingAction.apply { binding.homeIndexingAction.apply {
visibility = View.VISIBLE visibility = View.VISIBLE
text = getString(R.string.lbl_grant) text = context.getString(R.string.lbl_grant)
setOnClickListener { setOnClickListener {
storagePermissionLauncher.launch( storagePermissionLauncher.launch(
Manifest.permission.READ_EXTERNAL_STORAGE) Manifest.permission.READ_EXTERNAL_STORAGE)
@ -324,14 +326,16 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
binding.homeIndexingProgress.visibility = View.VISIBLE binding.homeIndexingProgress.visibility = View.VISIBLE
binding.homeIndexingAction.visibility = View.INVISIBLE binding.homeIndexingAction.visibility = View.INVISIBLE
val context = requireContext()
when (indexing) { when (indexing) {
is Indexer.Indexing.Indeterminate -> { is Indexer.Indexing.Indeterminate -> {
binding.homeIndexingStatus.text = getString(R.string.lng_indexing_desc) binding.homeIndexingStatus.text = context.getString(R.string.lng_indexing_desc)
binding.homeIndexingProgress.isIndeterminate = true binding.homeIndexingProgress.isIndeterminate = true
} }
is Indexer.Indexing.Songs -> { is Indexer.Indexing.Songs -> {
binding.homeIndexingStatus.text = binding.homeIndexingStatus.text =
getString(R.string.fmt_indexing, indexing.current, indexing.total) context.getString(R.string.fmt_indexing, indexing.current, indexing.total)
binding.homeIndexingProgress.apply { binding.homeIndexingProgress.apply {
isIndeterminate = false isIndeterminate = false
max = indexing.total max = indexing.total
@ -370,14 +374,9 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
* https://al-e-shevelev.medium.com/how-to-reduce-scroll-sensitivity-of-viewpager2-widget-87797ad02414 * https://al-e-shevelev.medium.com/how-to-reduce-scroll-sensitivity-of-viewpager2-widget-87797ad02414
*/ */
private fun ViewPager2.reduceSensitivity(by: Int) { private fun ViewPager2.reduceSensitivity(by: Int) {
try {
val recycler = VIEW_PAGER_RECYCLER_FIELD.get(this@reduceSensitivity) val recycler = VIEW_PAGER_RECYCLER_FIELD.get(this@reduceSensitivity)
val slop = VIEW_PAGER_TOUCH_SLOP_FIELD.get(recycler) as Int val slop = VIEW_PAGER_TOUCH_SLOP_FIELD.get(recycler) as Int
VIEW_PAGER_TOUCH_SLOP_FIELD.set(recycler, slop * by) VIEW_PAGER_TOUCH_SLOP_FIELD.set(recycler, slop * by)
} catch (e: Exception) {
logE("Unable to reduce ViewPager sensitivity (likely an internal code change)")
e.logTraceOrThrow()
}
} }
/** Forces the view to recreate all fragments contained within it. */ /** Forces the view to recreate all fragments contained within it. */

View file

@ -34,9 +34,8 @@ import android.view.Gravity
import androidx.core.widget.TextViewCompat import androidx.core.widget.TextViewCompat
import com.google.android.material.textview.MaterialTextView import com.google.android.material.textview.MaterialTextView
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.util.getAttrColorSafe import org.oxycblt.auxio.util.getAttrColorCompat
import org.oxycblt.auxio.util.getDimenOffsetSafe import org.oxycblt.auxio.util.getDimenSize
import org.oxycblt.auxio.util.getDimenSizeSafe
import org.oxycblt.auxio.util.isRtl import org.oxycblt.auxio.util.isRtl
class FastScrollPopupView class FastScrollPopupView
@ -44,17 +43,17 @@ class FastScrollPopupView
constructor(context: Context, attrs: AttributeSet? = null, defStyleRes: Int = 0) : constructor(context: Context, attrs: AttributeSet? = null, defStyleRes: Int = 0) :
MaterialTextView(context, attrs, defStyleRes) { MaterialTextView(context, attrs, defStyleRes) {
init { init {
minimumWidth = context.getDimenSizeSafe(R.dimen.fast_scroll_popup_min_width) minimumWidth = context.getDimenSize(R.dimen.fast_scroll_popup_min_width)
minimumHeight = context.getDimenSizeSafe(R.dimen.fast_scroll_popup_min_height) minimumHeight = context.getDimenSize(R.dimen.fast_scroll_popup_min_height)
TextViewCompat.setTextAppearance(this, R.style.TextAppearance_Auxio_HeadlineLarge) TextViewCompat.setTextAppearance(this, R.style.TextAppearance_Auxio_HeadlineLarge)
setTextColor(context.getAttrColorSafe(R.attr.colorOnSecondary)) setTextColor(context.getAttrColorCompat(R.attr.colorOnSecondary))
ellipsize = TextUtils.TruncateAt.MIDDLE ellipsize = TextUtils.TruncateAt.MIDDLE
gravity = Gravity.CENTER gravity = Gravity.CENTER
includeFontPadding = false includeFontPadding = false
alpha = 0f alpha = 0f
elevation = context.getDimenSizeSafe(R.dimen.elevation_normal).toFloat() elevation = context.getDimenSize(R.dimen.elevation_normal).toFloat()
background = FastScrollPopupDrawable(context) background = FastScrollPopupDrawable(context)
} }
@ -62,16 +61,15 @@ constructor(context: Context, attrs: AttributeSet? = null, defStyleRes: Int = 0)
private val paint: Paint = private val paint: Paint =
Paint().apply { Paint().apply {
isAntiAlias = true isAntiAlias = true
color = context.getAttrColorSafe(R.attr.colorSecondary) color = context.getAttrColorCompat(R.attr.colorSecondary).defaultColor
style = Paint.Style.FILL style = Paint.Style.FILL
} }
private val path = Path() private val path = Path()
private val matrix = Matrix() private val matrix = Matrix()
private val paddingStart = private val paddingStart = context.getDimenSize(R.dimen.fast_scroll_popup_padding_start)
context.getDimenOffsetSafe(R.dimen.fast_scroll_popup_padding_start) private val paddingEnd = context.getDimenSize(R.dimen.fast_scroll_popup_padding_end)
private val paddingEnd = context.getDimenOffsetSafe(R.dimen.fast_scroll_popup_padding_end)
override fun draw(canvas: Canvas) { override fun draw(canvas: Canvas) {
canvas.drawPath(path, paint) canvas.drawPath(path, paint)

View file

@ -34,11 +34,9 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import kotlin.math.abs import kotlin.math.abs
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.ui.recycler.EdgeRecyclerView import org.oxycblt.auxio.ui.recycler.AuxioRecyclerView
import org.oxycblt.auxio.util.canScroll import org.oxycblt.auxio.util.getDimenSize
import org.oxycblt.auxio.util.getDimenOffsetSafe import org.oxycblt.auxio.util.getDrawableCompat
import org.oxycblt.auxio.util.getDimenSizeSafe
import org.oxycblt.auxio.util.getDrawableSafe
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
@ -71,12 +69,12 @@ import org.oxycblt.auxio.util.systemBarInsetsCompat
class FastScrollRecyclerView class FastScrollRecyclerView
@JvmOverloads @JvmOverloads
constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) : constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) :
EdgeRecyclerView(context, attrs, defStyleAttr) { AuxioRecyclerView(context, attrs, defStyleAttr) {
// Thumb // Thumb
private val thumbView = private val thumbView =
View(context).apply { View(context).apply {
alpha = 0f alpha = 0f
background = context.getDrawableSafe(R.drawable.ui_scroll_thumb) background = context.getDrawableCompat(R.drawable.ui_scroll_thumb)
} }
private val thumbWidth = thumbView.background.intrinsicWidth private val thumbWidth = thumbView.background.intrinsicWidth
@ -99,7 +97,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
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.getDimenOffsetSafe(R.dimen.spacing_small) marginEnd = context.getDimenSize(R.dimen.spacing_small)
} }
} }
@ -107,7 +105,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
// Touch // Touch
private val minTouchTargetSize = private val minTouchTargetSize =
context.getDimenSizeSafe(R.dimen.fast_scroll_thumb_touch_target_size) context.getDimenSize(R.dimen.fast_scroll_thumb_touch_target_size)
private val touchSlop = ViewConfiguration.get(context).scaledTouchSlop private val touchSlop = ViewConfiguration.get(context).scaledTouchSlop
private var downX = 0f private var downX = 0f
@ -289,7 +287,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
} }
private fun updateScrollbarState() { private fun updateScrollbarState() {
if (!canScroll || childCount == 0) { if (computeVerticalScrollRange() <= height || childCount == 0) {
return return
} }

View file

@ -27,7 +27,6 @@ import org.oxycblt.auxio.ui.fragment.MenuFragment
import org.oxycblt.auxio.ui.recycler.Item import org.oxycblt.auxio.ui.recycler.Item
import org.oxycblt.auxio.ui.recycler.MenuItemListener import org.oxycblt.auxio.ui.recycler.MenuItemListener
import org.oxycblt.auxio.util.androidActivityViewModels import org.oxycblt.auxio.util.androidActivityViewModels
import org.oxycblt.auxio.util.applySpans
/** /**
* A Base [Fragment] implementing the base features shared across all list fragments in the home UI. * A Base [Fragment] implementing the base features shared across all list fragments in the home UI.
@ -46,7 +45,6 @@ abstract class HomeListFragment<T : Item> :
override fun onBindingCreated(binding: FragmentHomeListBinding, savedInstanceState: Bundle?) { override fun onBindingCreated(binding: FragmentHomeListBinding, savedInstanceState: Bundle?) {
binding.homeRecycler.popupProvider = this binding.homeRecycler.popupProvider = this
binding.homeRecycler.listener = this binding.homeRecycler.listener = this
binding.homeRecycler.applySpans()
} }
override fun onDestroyBinding(binding: FragmentHomeListBinding) { override fun onDestroyBinding(binding: FragmentHomeListBinding) {

View file

@ -29,8 +29,8 @@ import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.util.getColorStateListSafe import org.oxycblt.auxio.util.getColorCompat
import org.oxycblt.auxio.util.getDrawableSafe import org.oxycblt.auxio.util.getDrawableCompat
/** /**
* Effectively a super-charged [StyledImageView]. * Effectively a super-charged [StyledImageView].
@ -66,7 +66,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
indicator = indicator =
StyledImageView(context).apply { StyledImageView(context).apply {
cornerRadius = this@ImageGroup.cornerRadius cornerRadius = this@ImageGroup.cornerRadius
staticIcon = context.getDrawableSafe(R.drawable.ic_currently_playing_24) staticIcon = context.getDrawableCompat(R.drawable.ic_currently_playing_24)
} }
addView(inner) addView(inner)
@ -83,7 +83,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
getChildAt(1)?.apply { getChildAt(1)?.apply {
background = background =
MaterialShapeDrawable().apply { MaterialShapeDrawable().apply {
fillColor = context.getColorStateListSafe(R.color.sel_cover_bg) fillColor = context.getColorCompat(R.color.sel_cover_bg)
setCornerSize(cornerRadius) setCornerSize(cornerRadius)
} }
} }

View file

@ -39,8 +39,8 @@ 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.settings.Settings import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.util.getColorStateListSafe import org.oxycblt.auxio.util.getColorCompat
import org.oxycblt.auxio.util.getDrawableSafe import org.oxycblt.auxio.util.getDrawableCompat
/** /**
* An [AppCompatImageView] that applies many of the stylistic choices that Auxio uses regarding * An [AppCompatImageView] that applies many of the stylistic choices that Auxio uses regarding
@ -87,7 +87,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
clipToOutline = true clipToOutline = true
background = background =
MaterialShapeDrawable().apply { MaterialShapeDrawable().apply {
fillColor = context.getColorStateListSafe(R.color.sel_cover_bg) fillColor = context.getColorCompat(R.color.sel_cover_bg)
setCornerSize(cornerRadius) setCornerSize(cornerRadius)
} }
@ -96,7 +96,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
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.getDrawableSafe(staticIcon) this.staticIcon = context.getDrawableCompat(staticIcon)
} }
useLargeIcon = styledAttrs.getBoolean(R.styleable.StyledImageView_useLargeIcon, false) useLargeIcon = styledAttrs.getBoolean(R.styleable.StyledImageView_useLargeIcon, false)
@ -126,14 +126,14 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
dispose() dispose()
load(music) { load(music) {
error(StyledDrawable(context, context.getDrawableSafe(error))) error(StyledDrawable(context, context.getDrawableCompat(error)))
transformations(SquareFrameTransform.INSTANCE) transformations(SquareFrameTransform.INSTANCE)
} }
} }
private class StyledDrawable(context: Context, private val src: Drawable) : Drawable() { private class StyledDrawable(context: Context, private val src: Drawable) : Drawable() {
init { init {
DrawableCompat.setTintList(src, context.getColorStateListSafe(R.color.sel_on_cover_bg)) DrawableCompat.setTintList(src, context.getColorCompat(R.color.sel_on_cover_bg))
} }
override fun draw(canvas: Canvas) { override fun draw(canvas: Canvas) {

View file

@ -32,7 +32,7 @@ import org.oxycblt.auxio.music.Directory
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 org.oxycblt.auxio.util.getSystemServiceSafe import org.oxycblt.auxio.util.getSystemServiceCompat
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.showToast import org.oxycblt.auxio.util.showToast
@ -45,7 +45,7 @@ class MusicDirsDialog :
private val dirAdapter = MusicDirAdapter(this) private val dirAdapter = MusicDirAdapter(this)
private val settings: Settings by lifecycleObject { binding -> Settings(binding.context) } private val settings: Settings by lifecycleObject { binding -> Settings(binding.context) }
private val storageManager: StorageManager by lifecycleObject { binding -> private val storageManager: StorageManager by lifecycleObject { binding ->
binding.context.getSystemServiceSafe(StorageManager::class) binding.context.getSystemServiceCompat(StorageManager::class)
} }
override fun onCreateBinding(inflater: LayoutInflater) = override fun onCreateBinding(inflater: LayoutInflater) =

View file

@ -34,7 +34,7 @@ import org.oxycblt.auxio.playback.state.PlaybackStateManager
import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.ui.system.ForegroundManager import org.oxycblt.auxio.ui.system.ForegroundManager
import org.oxycblt.auxio.util.contentResolverSafe import org.oxycblt.auxio.util.contentResolverSafe
import org.oxycblt.auxio.util.getSystemServiceSafe import org.oxycblt.auxio.util.getSystemServiceCompat
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
/** /**
@ -73,7 +73,7 @@ class IndexerService : Service(), Indexer.Controller, Settings.Callback {
observingNotification = ObservingNotification(this) observingNotification = ObservingNotification(this)
wakeLock = wakeLock =
getSystemServiceSafe(PowerManager::class) getSystemServiceCompat(PowerManager::class)
.newWakeLock( .newWakeLock(
PowerManager.PARTIAL_WAKE_LOCK, BuildConfig.APPLICATION_ID + ":IndexerService") PowerManager.PARTIAL_WAKE_LOCK, BuildConfig.APPLICATION_ID + ":IndexerService")

View file

@ -30,7 +30,7 @@ import java.io.File
import org.oxycblt.auxio.music.* import org.oxycblt.auxio.music.*
import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.util.contentResolverSafe import org.oxycblt.auxio.util.contentResolverSafe
import org.oxycblt.auxio.util.getSystemServiceSafe import org.oxycblt.auxio.util.getSystemServiceCompat
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
/* /*
@ -114,7 +114,7 @@ abstract class MediaStoreBackend : Indexer.Backend {
override fun query(context: Context): Cursor { override fun query(context: Context): Cursor {
val settings = Settings(context) val settings = Settings(context)
val storageManager = context.getSystemServiceSafe(StorageManager::class) val storageManager = context.getSystemServiceCompat(StorageManager::class)
volumes.addAll(storageManager.storageVolumesCompat) volumes.addAll(storageManager.storageVolumesCompat)
val dirs = settings.getMusicDirs(storageManager) val dirs = settings.getMusicDirs(storageManager)

View file

@ -28,7 +28,7 @@ import org.oxycblt.auxio.ui.NavigationViewModel
import org.oxycblt.auxio.ui.fragment.ViewBindingFragment import org.oxycblt.auxio.ui.fragment.ViewBindingFragment
import org.oxycblt.auxio.util.androidActivityViewModels import org.oxycblt.auxio.util.androidActivityViewModels
import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.getColorStateListSafe import org.oxycblt.auxio.util.getColorCompat
/** /**
* A fragment showing the current playback state in a compact manner. Used as the bar for the * A fragment showing the current playback state in a compact manner. Used as the bar for the
@ -58,7 +58,7 @@ class PlaybackBarFragment : ViewBindingFragment<FragmentPlaybackBarBinding>() {
// Load the track color in manually as it's unclear whether the track actually supports // Load the track color in manually as it's unclear whether the track actually supports
// using a ColorStateList in the resources // using a ColorStateList in the resources
binding.playbackProgressBar.trackColor = binding.playbackProgressBar.trackColor =
requireContext().getColorStateListSafe(R.color.sel_track).defaultColor requireContext().getColorCompat(R.color.sel_track).defaultColor
binding.playbackPlayPause.setOnClickListener { playbackModel.invertPlaying() } binding.playbackPlayPause.setOnClickListener { playbackModel.invertPlaying() }

View file

@ -34,7 +34,7 @@ import org.oxycblt.auxio.ui.NavigationViewModel
import org.oxycblt.auxio.ui.fragment.ViewBindingFragment import org.oxycblt.auxio.ui.fragment.ViewBindingFragment
import org.oxycblt.auxio.util.androidActivityViewModels import org.oxycblt.auxio.util.androidActivityViewModels
import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.getDrawableSafe import org.oxycblt.auxio.util.getDrawableCompat
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.systemBarInsetsCompat import org.oxycblt.auxio.util.systemBarInsetsCompat
@ -148,8 +148,11 @@ class PlaybackPanelFragment :
} }
private fun updateParent(parent: MusicParent?) { private fun updateParent(parent: MusicParent?) {
requireBinding().playbackToolbar.subtitle = val binding = requireBinding()
parent?.resolveName(requireContext()) ?: getString(R.string.lbl_all_songs) val context = requireContext()
binding.playbackToolbar.subtitle =
parent?.resolveName(context) ?: context.getString(R.string.lbl_all_songs)
} }
private fun updatePosition(positionSecs: Long) { private fun updatePosition(positionSecs: Long) {
@ -157,8 +160,6 @@ class PlaybackPanelFragment :
} }
private fun updateRepeat(repeatMode: RepeatMode) { private fun updateRepeat(repeatMode: RepeatMode) {
requireBinding().playbackRepeat.apply {
isActivated = repeatMode != RepeatMode.NONE
val iconRes = val iconRes =
when (repeatMode) { when (repeatMode) {
RepeatMode.NONE -> R.drawable.ic_repeat_off_24 RepeatMode.NONE -> R.drawable.ic_repeat_off_24
@ -166,7 +167,9 @@ class PlaybackPanelFragment :
RepeatMode.TRACK -> R.drawable.ic_repeat_one_24 RepeatMode.TRACK -> R.drawable.ic_repeat_one_24
} }
icon = requireContext().getDrawableSafe(iconRes) requireBinding().playbackRepeat.apply {
icon = requireContext().getDrawableCompat(iconRes)
isActivated = repeatMode != RepeatMode.NONE
} }
} }

View file

@ -87,8 +87,8 @@ private constructor(
val backgroundDrawable = val backgroundDrawable =
MaterialShapeDrawable.createWithElevationOverlay(binding.root.context).apply { MaterialShapeDrawable.createWithElevationOverlay(binding.root.context).apply {
fillColor = binding.context.getAttrColorSafe(R.attr.colorSurface).stateList fillColor = binding.context.getAttrColorCompat(R.attr.colorSurface)
elevation = binding.context.getDimenSafe(R.dimen.elevation_normal) * 5 elevation = binding.context.getDimen(R.dimen.elevation_normal) * 5
alpha = 0 alpha = 0
} }
@ -114,8 +114,8 @@ private constructor(
LayerDrawable( LayerDrawable(
arrayOf( arrayOf(
MaterialShapeDrawable.createWithElevationOverlay(binding.context).apply { MaterialShapeDrawable.createWithElevationOverlay(binding.context).apply {
fillColor = binding.context.getAttrColorSafe(R.attr.colorSurface).stateList fillColor = binding.context.getAttrColorCompat(R.attr.colorSurface)
elevation = binding.context.getDimenSafe(R.dimen.elevation_normal) elevation = binding.context.getDimen(R.dimen.elevation_normal)
}, },
backgroundDrawable)) backgroundDrawable))
} }

View file

@ -23,7 +23,7 @@ import androidx.core.view.isInvisible
import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.util.getDimenSafe import org.oxycblt.auxio.util.getDimen
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
/** /**
@ -68,7 +68,7 @@ class QueueDragCallback(private val playbackModel: QueueViewModel) : ItemTouchHe
logD("Lifting queue item") logD("Lifting queue item")
val bg = holder.backgroundDrawable val bg = holder.backgroundDrawable
val elevation = recyclerView.context.getDimenSafe(R.dimen.elevation_normal) val elevation = recyclerView.context.getDimen(R.dimen.elevation_normal)
holder.itemView holder.itemView
.animate() .animate()
.translationZ(elevation) .translationZ(elevation)
@ -108,7 +108,7 @@ class QueueDragCallback(private val playbackModel: QueueViewModel) : ItemTouchHe
logD("Dropping queue item") logD("Dropping queue item")
val bg = holder.backgroundDrawable val bg = holder.backgroundDrawable
val elevation = recyclerView.context.getDimenSafe(R.dimen.elevation_normal) val elevation = recyclerView.context.getDimen(R.dimen.elevation_normal)
holder.itemView holder.itemView
.animate() .animate()
.translationZ(0f) .translationZ(0f)

View file

@ -33,11 +33,11 @@ import org.oxycblt.auxio.util.*
class QueueSheetBehavior<V : View>(context: Context, attributeSet: AttributeSet?) : class QueueSheetBehavior<V : View>(context: Context, attributeSet: AttributeSet?) :
AuxioSheetBehavior<V>(context, attributeSet) { AuxioSheetBehavior<V>(context, attributeSet) {
private var barHeight = 0 private var barHeight = 0
private var barSpacing = context.getDimenSizeSafe(R.dimen.spacing_small) private var barSpacing = context.getDimenSize(R.dimen.spacing_small)
init { init {
isHideable = false isHideable = false
sheetBackgroundDrawable.setCornerSize(context.getDimenSafe(R.dimen.size_corners_medium)) sheetBackgroundDrawable.setCornerSize(context.getDimen(R.dimen.size_corners_medium))
} }
override fun layoutDependsOn(parent: CoordinatorLayout, child: V, dependency: View) = override fun layoutDependsOn(parent: CoordinatorLayout, child: V, dependency: View) =

View file

@ -44,11 +44,10 @@ import org.oxycblt.auxio.ui.recycler.Header
import org.oxycblt.auxio.ui.recycler.Item import org.oxycblt.auxio.ui.recycler.Item
import org.oxycblt.auxio.ui.recycler.MenuItemListener import org.oxycblt.auxio.ui.recycler.MenuItemListener
import org.oxycblt.auxio.util.androidViewModels import org.oxycblt.auxio.util.androidViewModels
import org.oxycblt.auxio.util.applySpans
import org.oxycblt.auxio.util.collect import org.oxycblt.auxio.util.collect
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 org.oxycblt.auxio.util.getSystemServiceSafe import org.oxycblt.auxio.util.getSystemServiceCompat
import org.oxycblt.auxio.util.logW import org.oxycblt.auxio.util.logW
/** /**
@ -64,7 +63,7 @@ class SearchFragment :
private val searchAdapter = SearchAdapter(this) private val searchAdapter = SearchAdapter(this)
private val settings: Settings by lifecycleObject { binding -> Settings(binding.context) } private val settings: Settings by lifecycleObject { binding -> Settings(binding.context) }
private val imm: InputMethodManager by lifecycleObject { binding -> private val imm: InputMethodManager by lifecycleObject { binding ->
binding.context.getSystemServiceSafe(InputMethodManager::class) binding.context.getSystemServiceCompat(InputMethodManager::class)
} }
private var launchedKeyboard = false private var launchedKeyboard = false
@ -108,7 +107,7 @@ class SearchFragment :
binding.searchRecycler.apply { binding.searchRecycler.apply {
adapter = searchAdapter adapter = searchAdapter
applySpans { pos -> searchAdapter.data.currentList[pos] is Header } setSpanSizeLookup { pos -> searchAdapter.data.getItem(pos) is Header }
} }
// --- VIEWMODEL SETUP --- // --- VIEWMODEL SETUP ---

View file

@ -104,6 +104,8 @@ class AboutFragment : ViewBindingFragment<FragmentAboutBinding>() {
private fun openLinkInBrowser(link: String) { private fun openLinkInBrowser(link: String) {
logD("Opening $link") logD("Opening $link")
val context = requireContext()
val browserIntent = val browserIntent =
Intent(Intent.ACTION_VIEW, link.toUri()).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) Intent(Intent.ACTION_VIEW, link.toUri()).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
@ -112,10 +114,10 @@ class AboutFragment : ViewBindingFragment<FragmentAboutBinding>() {
// [along with adding a new permission that breaks the old manual code], so // [along with adding a new permission that breaks the old manual code], so
// we just do a typical activity launch. // we just do a typical activity launch.
try { try {
requireContext().startActivity(browserIntent) context.startActivity(browserIntent)
} catch (e: ActivityNotFoundException) { } catch (e: ActivityNotFoundException) {
// No app installed to open the link // No app installed to open the link
requireContext().showToast(R.string.err_no_app) context.showToast(R.string.err_no_app)
} }
} else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { } else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
// On older versions of android, opening links from an ACTION_VIEW intent might // On older versions of android, opening links from an ACTION_VIEW intent might
@ -123,8 +125,7 @@ class AboutFragment : ViewBindingFragment<FragmentAboutBinding>() {
// case, we will try to manually handle these cases before we try to launch the // case, we will try to manually handle these cases before we try to launch the
// browser. // browser.
val pkgName = val pkgName =
requireContext() context.packageManager
.packageManager
.resolveActivity(browserIntent, PackageManager.MATCH_DEFAULT_ONLY) .resolveActivity(browserIntent, PackageManager.MATCH_DEFAULT_ONLY)
?.run { activityInfo.packageName } ?.run { activityInfo.packageName }
@ -144,7 +145,7 @@ class AboutFragment : ViewBindingFragment<FragmentAboutBinding>() {
} }
} else { } else {
// No app installed to open the link // No app installed to open the link
requireContext().showToast(R.string.err_no_app) context.showToast(R.string.err_no_app)
} }
} }
} }

View file

@ -95,41 +95,49 @@ class SettingsListFragment : PreferenceFragmentCompat() {
dialog.setTargetFragment(this, 0) dialog.setTargetFragment(this, 0)
dialog.show(parentFragmentManager, IntListPreferenceDialog.TAG) dialog.show(parentFragmentManager, IntListPreferenceDialog.TAG)
} }
is WrappedDialogPreference -> is WrappedDialogPreference -> {
val context = requireContext()
when (preference.key) { when (preference.key) {
getString(R.string.set_key_accent) -> context.getString(R.string.set_key_accent) ->
AccentCustomizeDialog() AccentCustomizeDialog()
.show(childFragmentManager, AccentCustomizeDialog.TAG) .show(childFragmentManager, AccentCustomizeDialog.TAG)
getString(R.string.set_key_lib_tabs) -> context.getString(R.string.set_key_lib_tabs) ->
TabCustomizeDialog().show(childFragmentManager, TabCustomizeDialog.TAG) TabCustomizeDialog().show(childFragmentManager, TabCustomizeDialog.TAG)
getString(R.string.set_key_pre_amp) -> context.getString(R.string.set_key_pre_amp) ->
PreAmpCustomizeDialog() PreAmpCustomizeDialog()
.show(childFragmentManager, PreAmpCustomizeDialog.TAG) .show(childFragmentManager, PreAmpCustomizeDialog.TAG)
getString(R.string.set_key_music_dirs) -> context.getString(R.string.set_key_music_dirs) ->
MusicDirsDialog().show(childFragmentManager, MusicDirsDialog.TAG) MusicDirsDialog().show(childFragmentManager, MusicDirsDialog.TAG)
else -> logEOrThrow("Unexpected dialog key ${preference.key}") else -> logEOrThrow("Unexpected dialog key ${preference.key}")
} }
}
else -> super.onDisplayPreferenceDialog(preference) else -> super.onDisplayPreferenceDialog(preference)
} }
} }
override fun onPreferenceTreeClick(preference: Preference): Boolean { override fun onPreferenceTreeClick(preference: Preference): Boolean {
val context = requireContext()
when (preference.key) { when (preference.key) {
getString(R.string.set_key_save_state) -> { context.getString(R.string.set_key_save_state) -> {
playbackModel.savePlaybackState { context?.showToast(R.string.lng_state_saved) } playbackModel.savePlaybackState {
this.context?.showToast(R.string.lng_state_saved)
} }
getString(R.string.set_key_wipe_state) -> {
playbackModel.wipePlaybackState { context?.showToast(R.string.lng_state_wiped) }
} }
getString(R.string.set_key_restore_state) -> context.getString(R.string.set_key_wipe_state) -> {
playbackModel.wipePlaybackState {
this.context?.showToast(R.string.lng_state_wiped)
}
}
context.getString(R.string.set_key_restore_state) ->
playbackModel.tryRestorePlaybackState { restored -> playbackModel.tryRestorePlaybackState { restored ->
if (restored) { if (restored) {
context?.showToast(R.string.lng_state_restored) this.context?.showToast(R.string.lng_state_restored)
} else { } else {
context?.showToast(R.string.err_did_not_restore) this.context?.showToast(R.string.err_did_not_restore)
} }
} }
getString(R.string.set_key_reindex) -> { context.getString(R.string.set_key_reindex) -> {
musicModel.reindex() musicModel.reindex()
} }
else -> return super.onPreferenceTreeClick(preference) else -> return super.onPreferenceTreeClick(preference)
@ -139,7 +147,8 @@ class SettingsListFragment : PreferenceFragmentCompat() {
} }
private fun setupPreference(preference: Preference) { private fun setupPreference(preference: Preference) {
val settings = Settings(requireContext()) val context = requireActivity()
val settings = Settings(context)
if (!preference.isVisible) return if (!preference.isVisible) return
@ -151,31 +160,31 @@ class SettingsListFragment : PreferenceFragmentCompat() {
preference.apply { preference.apply {
when (key) { when (key) {
getString(R.string.set_key_theme) -> { context.getString(R.string.set_key_theme) -> {
onPreferenceChangeListener = onPreferenceChangeListener =
Preference.OnPreferenceChangeListener { _, value -> Preference.OnPreferenceChangeListener { _, value ->
AppCompatDelegate.setDefaultNightMode(value as Int) AppCompatDelegate.setDefaultNightMode(value as Int)
true true
} }
} }
getString(R.string.set_key_accent) -> { context.getString(R.string.set_key_accent) -> {
summary = context.getString(settings.accent.name) summary = context.getString(settings.accent.name)
} }
getString(R.string.set_key_black_theme) -> { context.getString(R.string.set_key_black_theme) -> {
onPreferenceChangeListener = onPreferenceChangeListener =
Preference.OnPreferenceChangeListener { _, _ -> Preference.OnPreferenceChangeListener { _, _ ->
if (requireContext().isNight) { if (context.isNight) {
requireActivity().recreate() context.recreate()
} }
true true
} }
} }
getString(R.string.set_key_show_covers), context.getString(R.string.set_key_show_covers),
getString(R.string.set_key_quality_covers) -> { context.getString(R.string.set_key_quality_covers) -> {
onPreferenceChangeListener = onPreferenceChangeListener =
Preference.OnPreferenceChangeListener { _, _ -> Preference.OnPreferenceChangeListener { _, _ ->
Coil.imageLoader(requireContext()).apply { this.memoryCache?.clear() } Coil.imageLoader(context).apply { this.memoryCache?.clear() }
true true
} }
} }

View file

@ -17,7 +17,6 @@
package org.oxycblt.auxio.settings.ui package org.oxycblt.auxio.settings.ui
import android.app.Dialog
import android.os.Bundle import android.os.Bundle
import androidx.preference.PreferenceDialogFragmentCompat import androidx.preference.PreferenceDialogFragmentCompat
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
@ -29,22 +28,20 @@ class IntListPreferenceDialog : PreferenceDialogFragmentCompat() {
get() = (preference as IntListPreference) get() = (preference as IntListPreference)
private var pendingValueIndex = -1 private var pendingValueIndex = -1
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { override fun onCreateDialog(savedInstanceState: Bundle?) =
// PreferenceDialogFragmentCompat does not allow us to customize the actual creation // PreferenceDialogFragmentCompat does not allow us to customize the actual creation
// of the alert dialog, so we have to manually override onCreateDialog and customize it // of the alert dialog, so we have to manually override onCreateDialog and customize it
// ourselves. // ourselves.
val builder = MaterialAlertDialogBuilder(requireContext(), theme) MaterialAlertDialogBuilder(requireContext(), theme)
builder.setTitle(listPreference.title) .setTitle(listPreference.title)
builder.setPositiveButton(null, null) .setPositiveButton(null, null)
builder.setNegativeButton(R.string.lbl_cancel, null) .setNegativeButton(R.string.lbl_cancel, null)
builder.setSingleChoiceItems(listPreference.entries, listPreference.getValueIndex()) { .setSingleChoiceItems(listPreference.entries, listPreference.getValueIndex()) { _, index
_, ->
index ->
pendingValueIndex = index pendingValueIndex = index
dismiss() dismiss()
} }
return builder.create() .create()
}
override fun onDialogClosed(positiveResult: Boolean) { override fun onDialogClosed(positiveResult: Boolean) {
if (pendingValueIndex > -1) { if (pendingValueIndex > -1) {

View file

@ -26,6 +26,7 @@ import androidx.annotation.AttrRes
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.content.res.ResourcesCompat import androidx.core.content.res.ResourcesCompat
import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.AppBarLayout
import org.oxycblt.auxio.util.coordinatorLayoutBehavior
/** /**
* An [AppBarLayout] that fixes a bug with the default implementation where the lifted state will * An [AppBarLayout] that fixes a bug with the default implementation where the lifted state will
@ -47,8 +48,8 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
if (child != null) { if (child != null) {
val coordinator = parent as CoordinatorLayout val coordinator = parent as CoordinatorLayout
(layoutParams as CoordinatorLayout.LayoutParams) coordinatorLayoutBehavior?.onNestedPreScroll(
.behavior?.onNestedPreScroll(coordinator, this, coordinator, 0, 0, tConsumed, 0) coordinator, this, coordinator, 0, 0, tConsumed, 0)
} }
true true

View file

@ -40,8 +40,8 @@ abstract class AuxioSheetBehavior<V : View>(context: Context, attributeSet: Attr
private var setup = false private var setup = false
val sheetBackgroundDrawable = val sheetBackgroundDrawable =
MaterialShapeDrawable.createWithElevationOverlay(context).apply { MaterialShapeDrawable.createWithElevationOverlay(context).apply {
fillColor = context.getAttrColorSafe(R.attr.colorSurface).stateList fillColor = context.getAttrColorCompat(R.attr.colorSurface)
elevation = context.getDimenSafe(R.dimen.elevation_normal) elevation = context.getDimen(R.dimen.elevation_normal)
} }
init { init {
@ -75,7 +75,8 @@ abstract class AuxioSheetBehavior<V : View>(context: Context, attributeSet: Attr
background = background =
LayerDrawable( LayerDrawable(
arrayOf( arrayOf(
ColorDrawable(context.getAttrColorSafe(R.attr.colorSurface)), ColorDrawable(
context.getAttrColorCompat(R.attr.colorSurface).defaultColor),
sheetBackgroundDrawable)) sheetBackgroundDrawable))
// Try to disable drop shadows if possible. // Try to disable drop shadows if possible.

View file

@ -37,8 +37,8 @@ class BottomSheetContentBehavior<V : View>(context: Context, attributeSet: Attri
CoordinatorLayout.Behavior<V>(context, attributeSet) { CoordinatorLayout.Behavior<V>(context, attributeSet) {
private var dep: View? = null private var dep: View? = null
private var lastInsets: WindowInsets? = null private var lastInsets: WindowInsets? = null
private var lastConsumed: Int? = null private var lastConsumed = -1
private var setup: Boolean = false private var setup = false
override fun layoutDependsOn(parent: CoordinatorLayout, child: V, dependency: View): Boolean { override fun layoutDependsOn(parent: CoordinatorLayout, child: V, dependency: View): Boolean {
if (dependency.coordinatorLayoutBehavior is NeoBottomSheetBehavior) { if (dependency.coordinatorLayoutBehavior is NeoBottomSheetBehavior) {
@ -56,7 +56,7 @@ class BottomSheetContentBehavior<V : View>(context: Context, attributeSet: Attri
): Boolean { ): Boolean {
val behavior = dependency.coordinatorLayoutBehavior as NeoBottomSheetBehavior val behavior = dependency.coordinatorLayoutBehavior as NeoBottomSheetBehavior
val consumed = behavior.calculateConsumedByBar() val consumed = behavior.calculateConsumedByBar()
if (consumed < Int.MIN_VALUE) { if (consumed == Int.MIN_VALUE) {
return false return false
} }

View file

@ -350,6 +350,8 @@ data class Sort(val mode: Mode, val isAscending: Boolean) {
} }
private class BasicComparator<T : Music> private constructor() : Comparator<T> { private class BasicComparator<T : Music> private constructor() : Comparator<T> {
// TODO: Use Collator for sorting?
override fun compare(a: T, b: T): Int { override fun compare(a: T, b: T): Int {
val aSortName = a.sortName val aSortName = a.sortName
val bSortName = b.sortName val bSortName = b.sortName

View file

@ -24,10 +24,9 @@ import org.oxycblt.auxio.databinding.ItemAccentBinding
import org.oxycblt.auxio.ui.recycler.BackingData import org.oxycblt.auxio.ui.recycler.BackingData
import org.oxycblt.auxio.ui.recycler.BindingViewHolder import org.oxycblt.auxio.ui.recycler.BindingViewHolder
import org.oxycblt.auxio.ui.recycler.MonoAdapter import org.oxycblt.auxio.ui.recycler.MonoAdapter
import org.oxycblt.auxio.util.getAttrColorSafe import org.oxycblt.auxio.util.getAttrColorCompat
import org.oxycblt.auxio.util.getColorSafe import org.oxycblt.auxio.util.getColorCompat
import org.oxycblt.auxio.util.inflater import org.oxycblt.auxio.util.inflater
import org.oxycblt.auxio.util.stateList
/** /**
* An adapter that displays the accent palette. * An adapter that displays the accent palette.
@ -82,7 +81,7 @@ class AccentViewHolder private constructor(private val binding: ItemAccentBindin
setSelected(false) setSelected(false)
binding.accent.apply { binding.accent.apply {
backgroundTintList = context.getColorSafe(item.primary).stateList backgroundTintList = context.getColorCompat(item.primary)
contentDescription = context.getString(item.name) contentDescription = context.getString(item.name)
TooltipCompat.setTooltipText(this, contentDescription) TooltipCompat.setTooltipText(this, contentDescription)
setOnClickListener { listener.onAccentSelected(item) } setOnClickListener { listener.onAccentSelected(item) }
@ -94,9 +93,9 @@ class AccentViewHolder private constructor(private val binding: ItemAccentBindin
isEnabled = !isSelected isEnabled = !isSelected
iconTint = iconTint =
if (isSelected) { if (isSelected) {
context.getAttrColorSafe(R.attr.colorSurface).stateList context.getAttrColorCompat(R.attr.colorSurface)
} else { } else {
context.getColorSafe(android.R.color.transparent).stateList context.getColorCompat(android.R.color.transparent)
} }
} }
} }

View file

@ -22,7 +22,8 @@ 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 kotlin.math.max
import org.oxycblt.auxio.util.pxOfDp import org.oxycblt.auxio.R
import org.oxycblt.auxio.util.getDimenSize
/** /**
* 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
@ -35,9 +36,9 @@ class AccentGridLayoutManager(
defStyleAttr: Int, defStyleAttr: Int,
defStyleRes: Int defStyleRes: Int
) : GridLayoutManager(context, attrs, defStyleAttr, defStyleRes) { ) : GridLayoutManager(context, attrs, defStyleAttr, defStyleRes) {
// We use 72dp here since that's the rough size of the accent item. // We use 56dp here since that's the rough size of the accent item.
// This will need to be modified if this is used beyond the accent dialog. // This will need to be modified if this is used beyond the accent dialog.
private var columnWidth = context.pxOfDp(56f) private var columnWidth = context.getDimenSize(R.dimen.size_accent_item)
private var lastWidth = -1 private var lastWidth = -1
private var lastHeight = -1 private var lastHeight = -1
@ -45,9 +46,7 @@ class AccentGridLayoutManager(
override fun onLayoutChildren(recycler: RecyclerView.Recycler?, state: RecyclerView.State?) { override fun onLayoutChildren(recycler: RecyclerView.Recycler?, state: RecyclerView.State?) {
if (width > 0 && height > 0 && (lastWidth != width || lastHeight != height)) { if (width > 0 && height > 0 && (lastWidth != width || lastHeight != height)) {
val totalSpace = width - paddingRight - paddingLeft val totalSpace = width - paddingRight - paddingLeft
val spanCount = max(1, totalSpace / columnWidth) spanCount = max(1, totalSpace / columnWidth)
setSpanCount(spanCount)
} }
lastWidth = width lastWidth = width

View file

@ -23,11 +23,12 @@ import android.util.AttributeSet
import android.view.WindowInsets import android.view.WindowInsets
import androidx.annotation.AttrRes import androidx.annotation.AttrRes
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.util.systemBarInsetsCompat import org.oxycblt.auxio.util.systemBarInsetsCompat
/** A [RecyclerView] that automatically applies insets to itself. */ /** A [RecyclerView] that automatically applies insets to itself. */
open class EdgeRecyclerView open class AuxioRecyclerView
@JvmOverloads @JvmOverloads
constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) : constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) :
RecyclerView(context, attrs, defStyleAttr) { RecyclerView(context, attrs, defStyleAttr) {
@ -52,4 +53,13 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
return insets return insets
} }
fun setSpanSizeLookup(lookup: (Int) -> Boolean) {
val glm = layoutManager as GridLayoutManager
val spanCount = glm.spanCount
glm.spanSizeLookup =
object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int) = if (lookup(position)) spanCount else 1
}
}
} }

View file

@ -27,21 +27,21 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.divider.MaterialDivider import com.google.android.material.divider.MaterialDivider
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.util.getDimenSizeSafe import org.oxycblt.auxio.util.getDimenSize
/** /**
* A RecyclerView that enables something resembling the android:scrollIndicators attribute. Only * A RecyclerView that enables something resembling the android:scrollIndicators attribute. Only
* used in dialogs. * used in dialogs.
* @author OxygenCobalt * @author OxygenCobalt
*/ */
class ScrollIndicatorRecyclerView class DialogRecyclerView
@JvmOverloads @JvmOverloads
constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) : constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) :
RecyclerView(context, attrs, defStyleAttr) { RecyclerView(context, attrs, defStyleAttr) {
private val topDivider = MaterialDivider(context) private val topDivider = MaterialDivider(context)
private val bottomDivider = MaterialDivider(context) private val bottomDivider = MaterialDivider(context)
private val spacingMedium = context.getDimenSizeSafe(R.dimen.spacing_medium) private val spacingMedium = context.getDimenSize(R.dimen.spacing_medium)
init { init {
updatePadding(top = spacingMedium) updatePadding(top = spacingMedium)

View file

@ -26,7 +26,7 @@ import androidx.recyclerview.widget.AsyncListDiffer
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
// TODO: Reunify music updates and sorts under replace // TODO: Unify music updates and sorts under replace
/** /**
* An adapter for one viewholder tied to one type of data. All functionality is derived from the * An adapter for one viewholder tied to one type of data. All functionality is derived from the

View file

@ -28,7 +28,7 @@ import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.context
import org.oxycblt.auxio.util.getPluralSafe import org.oxycblt.auxio.util.getPlural
import org.oxycblt.auxio.util.inflater import org.oxycblt.auxio.util.inflater
/** /**
@ -124,8 +124,8 @@ class ArtistViewHolder private constructor(private val binding: ItemParentBindin
binding.parentInfo.text = binding.parentInfo.text =
binding.context.getString( binding.context.getString(
R.string.fmt_two, R.string.fmt_two,
binding.context.getPluralSafe(R.plurals.fmt_album_count, item.albums.size), binding.context.getPlural(R.plurals.fmt_album_count, item.albums.size),
binding.context.getPluralSafe(R.plurals.fmt_song_count, item.songs.size)) binding.context.getPlural(R.plurals.fmt_song_count, item.songs.size))
binding.root.apply { binding.root.apply {
setOnClickListener { listener.onItemClick(item) } setOnClickListener { listener.onItemClick(item) }
setOnLongClickListener { view -> setOnLongClickListener { view ->
@ -168,7 +168,7 @@ private constructor(
binding.parentImage.bind(item) binding.parentImage.bind(item)
binding.parentName.text = item.resolveName(binding.context) binding.parentName.text = item.resolveName(binding.context)
binding.parentInfo.text = binding.parentInfo.text =
binding.context.getPluralSafe(R.plurals.fmt_song_count, item.songs.size) binding.context.getPlural(R.plurals.fmt_song_count, item.songs.size)
binding.root.apply { binding.root.apply {
setOnClickListener { listener.onItemClick(item) } setOnClickListener { listener.onItemClick(item) }
setOnLongClickListener { view -> setOnLongClickListener { view ->

View file

@ -23,7 +23,7 @@ import android.content.Context
import android.os.Build import android.os.Build
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import org.oxycblt.auxio.util.getSystemServiceSafe import org.oxycblt.auxio.util.getSystemServiceCompat
/** /**
* Wrapper around [NotificationCompat.Builder] that automates parts of the notification setup, under * Wrapper around [NotificationCompat.Builder] that automates parts of the notification setup, under
@ -32,7 +32,7 @@ import org.oxycblt.auxio.util.getSystemServiceSafe
*/ */
abstract class ServiceNotification(context: Context, info: ChannelInfo) : abstract class ServiceNotification(context: Context, info: ChannelInfo) :
NotificationCompat.Builder(context, info.id) { NotificationCompat.Builder(context, info.id) {
private val notificationManager = context.getSystemServiceSafe(NotificationManager::class) private val notificationManager = context.getSystemServiceCompat(NotificationManager::class)
init { init {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {

View file

@ -23,14 +23,12 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.res.ColorStateList import android.content.res.ColorStateList
import android.content.res.Configuration import android.content.res.Configuration
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.os.Build import android.os.Build
import android.util.TypedValue import android.util.TypedValue
import android.view.LayoutInflater import android.view.LayoutInflater
import android.widget.Toast import android.widget.Toast
import androidx.annotation.AttrRes import androidx.annotation.AttrRes
import androidx.annotation.ColorInt
import androidx.annotation.ColorRes import androidx.annotation.ColorRes
import androidx.annotation.DimenRes import androidx.annotation.DimenRes
import androidx.annotation.Dimension import androidx.annotation.Dimension
@ -51,7 +49,7 @@ val Context.inflater: LayoutInflater
* Returns whether the current UI is in night mode or not. This will work if the theme is automatic * Returns whether the current UI is in night mode or not. This will work if the theme is automatic
* as well. * as well.
*/ */
val Context.isNight: Boolean val Context.isNight
get() = get() =
resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK ==
Configuration.UI_MODE_NIGHT_YES Configuration.UI_MODE_NIGHT_YES
@ -73,48 +71,25 @@ val Context.contentResolverSafe: ContentResolver
* @param value Int value for the plural. * @param value Int value for the plural.
* @return The formatted string requested * @return The formatted string requested
*/ */
fun Context.getPluralSafe(@PluralsRes pluralsRes: Int, value: Int): String { fun Context.getPlural(@PluralsRes pluralsRes: Int, value: Int) =
return try {
resources.getQuantityString(pluralsRes, value, value) resources.getQuantityString(pluralsRes, value, value)
} catch (e: Exception) {
handleResourceFailure(e, "plural", "<plural error>")
}
}
/**
* Convenience method for getting a color safely.
* @param color The color resource
* @return The color integer requested, or black if an error occurred.
*/
@ColorInt
fun Context.getColorSafe(@ColorRes color: Int): Int {
return try {
ContextCompat.getColor(this, color)
} catch (e: Exception) {
handleResourceFailure(e, "color", getColorSafe(android.R.color.black))
}
}
/** /**
* Convenience method for getting a [ColorStateList] resource safely. * Convenience method for getting a [ColorStateList] resource safely.
* @param color The color resource * @param color The color resource
* @return The [ColorStateList] requested, or black if an error occurred. * @return The [ColorStateList] requested
*/ */
fun Context.getColorStateListSafe(@ColorRes color: Int): ColorStateList { fun Context.getColorCompat(@ColorRes color: Int) =
return try { requireNotNull(ContextCompat.getColorStateList(this, color)) {
unlikelyToBeNull(ContextCompat.getColorStateList(this, color)) "Invalid resource: State list was null"
} catch (e: Exception) {
handleResourceFailure(e, "color state list", getColorSafe(android.R.color.black).stateList)
} }
}
/** /**
* Convenience method for getting a color attribute safely. * Convenience method for getting a color attribute safely.
* @param attr The color attribute * @param attr The color attribute
* @return The attribute requested, or black if an error occurred. * @return The attribute requested
*/ */
@ColorInt fun Context.getAttrColorCompat(@AttrRes attr: Int): ColorStateList {
fun Context.getAttrColorSafe(@AttrRes attr: Int): Int {
// First resolve the attribute into its ID // First resolve the attribute into its ID
val resolvedAttr = TypedValue() val resolvedAttr = TypedValue()
theme.resolveAttribute(attr, resolvedAttr, true) theme.resolveAttribute(attr, resolvedAttr, true)
@ -127,80 +102,32 @@ fun Context.getAttrColorSafe(@AttrRes attr: Int): Int {
resolvedAttr.data resolvedAttr.data
} }
return getColorSafe(color) return getColorCompat(color)
} }
/** /**
* Convenience method for getting a [Drawable] safely. * Convenience method for getting a [Drawable] safely.
* @param drawable The drawable resource * @param drawable The drawable resource
* @return The drawable requested, or black if an error occurred. * @return The drawable requested
*/ */
fun Context.getDrawableSafe(@DrawableRes drawable: Int): Drawable { fun Context.getDrawableCompat(@DrawableRes drawable: Int) =
return try { requireNotNull(ContextCompat.getDrawable(this, drawable)) {
requireNotNull(ContextCompat.getDrawable(this, drawable)) "Invalid resource: Drawable was null"
} catch (e: Exception) {
handleResourceFailure(e, "drawable", ColorDrawable(getColorSafe(android.R.color.black)))
} }
}
/** /**
* Convenience method for getting a dimension safely. * Convenience method for getting a dimension safely.
* @param dimen The dimension resource * @param dimen The dimension resource
* @return The dimension requested, or 0 if an error occurred. * @return The dimension requested
*/ */
@Dimension @Dimension fun Context.getDimen(@DimenRes dimen: Int) = resources.getDimension(dimen)
fun Context.getDimenSafe(@DimenRes dimen: Int): Float {
return try {
resources.getDimension(dimen)
} catch (e: Exception) {
handleResourceFailure(e, "dimen", 0f)
}
}
/** /**
* Convenience method for getting a dimension pixel size safely. * Convenience method for getting a dimension pixel size safely.
* @param dimen The dimension resource * @param dimen The dimension resource
* @return The dimension requested, in pixels, or 0 if an error occurred. * @return The dimension requested, in pixels
*/ */
@Px @Px fun Context.getDimenSize(@DimenRes dimen: Int) = resources.getDimensionPixelSize(dimen)
fun Context.getDimenSizeSafe(@DimenRes dimen: Int): Int {
return try {
resources.getDimensionPixelSize(dimen)
} catch (e: Exception) {
handleResourceFailure(e, "dimen", 0)
}
}
/**
* Convenience method for getting a dimension pixel offset safely.
* @param dimen The dimension resource
* @return The dimension requested, in pixels, or 0 if an error occurred.
*/
@Px
fun Context.getDimenOffsetSafe(@DimenRes dimen: Int): Int {
return try {
resources.getDimensionPixelOffset(dimen)
} catch (e: Exception) {
handleResourceFailure(e, "dimen", 0)
}
}
/**
* Calculates the pixels of the given dimension [dp].
* @param dp the dimension value
* @return The equivalent amount of pixels for [dp].
*/
@Px
fun Context.pxOfDp(@Dimension dp: Float): Int {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, resources.displayMetrics)
.toInt()
}
private fun <T> Context.handleResourceFailure(e: Exception, what: String, default: T): T {
logE("$what load failed")
e.logTraceOrThrow()
return default
}
/** /**
* Convenience method for getting a system service without nullability issues. * Convenience method for getting a system service without nullability issues.
@ -209,11 +136,10 @@ private fun <T> Context.handleResourceFailure(e: Exception, what: String, defaul
* @return The system service * @return The system service
* @throws IllegalArgumentException If the system service cannot be retrieved. * @throws IllegalArgumentException If the system service cannot be retrieved.
*/ */
fun <T : Any> Context.getSystemServiceSafe(serviceClass: KClass<T>): T { fun <T : Any> Context.getSystemServiceCompat(serviceClass: KClass<T>) =
return requireNotNull(ContextCompat.getSystemService(this, serviceClass.java)) { requireNotNull(ContextCompat.getSystemService(this, serviceClass.java)) {
"System service ${serviceClass.simpleName} could not be instantiated" "System service ${serviceClass.simpleName} could not be instantiated"
} }
}
/** Create a toast using the provided string resource. */ /** Create a toast using the provided string resource. */
fun Context.showToast(@StringRes str: Int) { fun Context.showToast(@StringRes str: Int) {

View file

@ -19,7 +19,6 @@ package org.oxycblt.auxio.util
import android.app.Application import android.app.Application
import android.content.Context import android.content.Context
import android.content.res.ColorStateList
import android.database.Cursor import android.database.Cursor
import android.database.sqlite.SQLiteDatabase import android.database.sqlite.SQLiteDatabase
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
@ -27,7 +26,6 @@ import android.os.Build
import android.view.View import android.view.View
import android.view.WindowInsets import android.view.WindowInsets
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.annotation.ColorRes
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
@ -41,7 +39,6 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding import androidx.viewbinding.ViewBinding
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -56,7 +53,7 @@ import org.oxycblt.auxio.R
*/ */
fun View.disableDropShadowCompat() { fun View.disableDropShadowCompat() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
val transparent = context.getColorSafe(android.R.color.transparent) val transparent = context.getColorCompat(android.R.color.transparent).defaultColor
outlineAmbientShadowColor = transparent outlineAmbientShadowColor = transparent
outlineSpotShadowColor = transparent outlineSpotShadowColor = transparent
} }
@ -113,42 +110,16 @@ val Drawable.isRtl: Boolean
val ViewBinding.context: Context val ViewBinding.context: Context
get() = root.context get() = root.context
/**
* Apply the recommended spans for a [RecyclerView].
*
* @param shouldBeFullWidth Optional callback for determining whether an item should be full-width,
* regardless of spans
*/
fun RecyclerView.applySpans(shouldBeFullWidth: ((Int) -> Boolean)? = null) {
val spans = resources.getInteger(R.integer.recycler_spans)
if (spans > 1) {
val mgr = GridLayoutManager(context, spans)
if (shouldBeFullWidth != null) {
mgr.spanSizeLookup =
object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int): Int {
return if (shouldBeFullWidth(position)) spans else 1
}
}
}
layoutManager = mgr
}
}
/** Returns whether a recyclerview can scroll. */ /** Returns whether a recyclerview can scroll. */
val RecyclerView.canScroll: Boolean fun RecyclerView.canScroll() = computeVerticalScrollRange() > height
get() = computeVerticalScrollRange() > height
val View.coordinatorLayoutBehavior: CoordinatorLayout.Behavior<*>? /**
* Shortcut to obtain the CoordinatorLayout behavior of a view. Null if not from a coordinator
* layout or if no behavior is present.
*/
val View.coordinatorLayoutBehavior: CoordinatorLayout.Behavior<View>?
get() = (layoutParams as? CoordinatorLayout.LayoutParams)?.behavior get() = (layoutParams as? CoordinatorLayout.LayoutParams)?.behavior
/** Converts this color to a single-color [ColorStateList]. */
val @receiver:ColorRes Int.stateList
get() = ColorStateList.valueOf(this)
/** /**
* Collect a [stateFlow] into [block] eventually. * Collect a [stateFlow] into [block] eventually.
* *

View file

@ -31,7 +31,7 @@ import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.playback.state.PlaybackStateManager
import org.oxycblt.auxio.playback.state.RepeatMode import org.oxycblt.auxio.playback.state.RepeatMode
import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.util.getDimenSizeSafe import org.oxycblt.auxio.util.getDimenSize
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
/** /**
@ -85,10 +85,10 @@ class WidgetComponent(private val context: Context) :
val cornerRadius = val cornerRadius =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
// Android 12, always round the cover with the app widget's inner radius // Android 12, always round the cover with the app widget's inner radius
context.getDimenSizeSafe(android.R.dimen.system_app_widget_inner_radius) context.getDimenSize(android.R.dimen.system_app_widget_inner_radius)
} else if (settings.roundMode) { } else if (settings.roundMode) {
// < Android 12, but the user still enabled round mode. // < Android 12, but the user still enabled round mode.
context.getDimenSizeSafe(R.dimen.size_corners_medium) context.getDimenSize(R.dimen.size_corners_medium)
} else { } else {
// User did not enable round mode. // User did not enable round mode.
0 0

View file

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<org.oxycblt.auxio.ui.recycler.ScrollIndicatorRecyclerView xmlns:android="http://schemas.android.com/apk/res/android" <org.oxycblt.auxio.ui.recycler.DialogRecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/accent_recycler" android:id="@+id/accent_recycler"

View file

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<org.oxycblt.auxio.ui.recycler.ScrollIndicatorRecyclerView xmlns:android="http://schemas.android.com/apk/res/android" <org.oxycblt.auxio.ui.recycler.DialogRecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/tab_recycler" android:id="@+id/tab_recycler"

View file

@ -20,11 +20,12 @@
</org.oxycblt.auxio.detail.DetailAppBarLayout> </org.oxycblt.auxio.detail.DetailAppBarLayout>
<org.oxycblt.auxio.ui.recycler.EdgeRecyclerView <org.oxycblt.auxio.ui.recycler.AuxioRecyclerView
android:id="@+id/detail_recycler" android:id="@+id/detail_recycler"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
app:spanCount="@integer/recycler_spans"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"
tools:listitem="@layout/item_detail" /> tools:listitem="@layout/item_detail" />

View file

@ -6,6 +6,7 @@
style="@style/Widget.Auxio.RecyclerView.WithAdaptiveFab" style="@style/Widget.Auxio.RecyclerView.WithAdaptiveFab"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
app:spanCount="@integer/recycler_spans"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"
tools:listitem="@layout/item_parent" /> tools:listitem="@layout/item_parent" />

View file

@ -6,7 +6,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<org.oxycblt.auxio.ui.recycler.EdgeRecyclerView <org.oxycblt.auxio.ui.recycler.AuxioRecyclerView
android:id="@+id/queue_recycler" android:id="@+id/queue_recycler"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"

View file

@ -45,11 +45,12 @@
</org.oxycblt.auxio.ui.AuxioAppBarLayout> </org.oxycblt.auxio.ui.AuxioAppBarLayout>
<org.oxycblt.auxio.ui.recycler.EdgeRecyclerView <org.oxycblt.auxio.ui.recycler.AuxioRecyclerView
android:id="@+id/search_recycler" android:id="@+id/search_recycler"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
app:spanCount="@integer/recycler_spans"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"
tools:listitem="@layout/item_song" /> tools:listitem="@layout/item_song" />

View file

@ -21,6 +21,7 @@
<dimen name="size_corners_mid_large">24dp</dimen> <dimen name="size_corners_mid_large">24dp</dimen>
<dimen name="size_btn">48dp</dimen> <dimen name="size_btn">48dp</dimen>
<dimen name="size_accent_item">56dp</dimen>
<dimen name="size_bottom_sheet_bar">64dp</dimen> <dimen name="size_bottom_sheet_bar">64dp</dimen>
<dimen name="size_play_pause_button">72dp</dimen> <dimen name="size_play_pause_button">72dp</dimen>