util: rework context utilities

Completely rework the Context extensions for resources.

Previously, Auxio has used a strange hodge-podge of context extensions
and verbose code to get resources. Fix this by unifying most of the
resource accesses under a single, unified set of extensions. The only
ones excluded for now is the getString call, as that is used in far too
many places to effectively replace.
This commit is contained in:
OxygenCobalt 2022-02-06 10:35:21 -07:00
parent bd099aee7b
commit 4b919b121a
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
20 changed files with 261 additions and 168 deletions

View file

@ -18,16 +18,15 @@
package org.oxycblt.auxio.accent package org.oxycblt.auxio.accent
import android.content.res.ColorStateList
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.widget.TooltipCompat import androidx.appcompat.widget.TooltipCompat
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.ItemAccentBinding import org.oxycblt.auxio.databinding.ItemAccentBinding
import org.oxycblt.auxio.util.getAttrColorSafe
import org.oxycblt.auxio.util.getColorSafe
import org.oxycblt.auxio.util.inflater import org.oxycblt.auxio.util.inflater
import org.oxycblt.auxio.util.resolveAttr import org.oxycblt.auxio.util.stateList
import org.oxycblt.auxio.util.resolveColor
import org.oxycblt.auxio.util.resolveStateList
/** /**
* An adapter that displays the list of all possible accents, and highlights the current one. * An adapter that displays the list of all possible accents, and highlights the current one.
@ -63,7 +62,7 @@ class AccentAdapter(
setSelected(accent == curAccent) setSelected(accent == curAccent)
binding.accent.apply { binding.accent.apply {
backgroundTintList = ColorStateList.valueOf(accent.primary.resolveColor(context)) backgroundTintList = context.getColorSafe(accent.primary).stateList
contentDescription = context.getString(accent.name) contentDescription = context.getString(accent.name)
TooltipCompat.setTooltipText(this, contentDescription) TooltipCompat.setTooltipText(this, contentDescription)
} }
@ -84,9 +83,9 @@ class AccentAdapter(
selectedViewHolder?.setSelected(false) selectedViewHolder?.setSelected(false)
selectedViewHolder = this selectedViewHolder = this
ColorStateList.valueOf(R.attr.colorSurface.resolveAttr(context)) context.getAttrColorSafe(R.attr.colorSurface).stateList
} else { } else {
android.R.color.transparent.resolveStateList(context) context.getColorSafe(android.R.color.transparent).stateList
} }
} }
} }

View file

@ -20,9 +20,9 @@ package org.oxycblt.auxio.accent
import android.content.Context import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import android.util.TypedValue
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.util.pxOfDp
import kotlin.math.max import kotlin.math.max
/** /**
@ -38,9 +38,7 @@ class AutoGridLayoutManager(
) : 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 72dp 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 = TypedValue.applyDimension( private var columnWidth = context.pxOfDp(72f)
TypedValue.COMPLEX_UNIT_DIP, 72F, context.resources.displayMetrics
).toInt()
private var lastWidth = -1 private var lastWidth = -1
private var lastHeight = -1 private var lastHeight = -1

View file

@ -36,7 +36,7 @@ import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.ui.ActionHeaderViewHolder import org.oxycblt.auxio.ui.ActionHeaderViewHolder
import org.oxycblt.auxio.ui.BaseViewHolder import org.oxycblt.auxio.ui.BaseViewHolder
import org.oxycblt.auxio.ui.DiffCallback import org.oxycblt.auxio.ui.DiffCallback
import org.oxycblt.auxio.util.getPlural import org.oxycblt.auxio.util.getPluralSafe
import org.oxycblt.auxio.util.inflater import org.oxycblt.auxio.util.inflater
/** /**
@ -156,7 +156,7 @@ class AlbumDetailAdapter(
binding.detailInfo.text = binding.detailInfo.context.getString( binding.detailInfo.text = binding.detailInfo.context.getString(
R.string.fmt_three, R.string.fmt_three,
data.year.toDate(binding.detailInfo.context), data.year.toDate(binding.detailInfo.context),
binding.detailInfo.context.getPlural( binding.detailInfo.context.getPluralSafe(
R.plurals.fmt_song_count, R.plurals.fmt_song_count,
data.songs.size data.songs.size
), ),

View file

@ -38,7 +38,7 @@ import org.oxycblt.auxio.ui.ActionHeaderViewHolder
import org.oxycblt.auxio.ui.BaseViewHolder import org.oxycblt.auxio.ui.BaseViewHolder
import org.oxycblt.auxio.ui.DiffCallback import org.oxycblt.auxio.ui.DiffCallback
import org.oxycblt.auxio.ui.HeaderViewHolder import org.oxycblt.auxio.ui.HeaderViewHolder
import org.oxycblt.auxio.util.getPlural import org.oxycblt.auxio.util.getPluralSafe
import org.oxycblt.auxio.util.inflater import org.oxycblt.auxio.util.inflater
/** /**
@ -207,8 +207,8 @@ class ArtistDetailAdapter(
binding.detailInfo.text = context.getString( binding.detailInfo.text = context.getString(
R.string.fmt_counts, R.string.fmt_counts,
context.getPlural(R.plurals.fmt_album_count, data.albums.size), context.getPluralSafe(R.plurals.fmt_album_count, data.albums.size),
context.getPlural(R.plurals.fmt_song_count, data.songs.size) context.getPluralSafe(R.plurals.fmt_song_count, data.songs.size)
) )
binding.detailPlayButton.setOnClickListener { binding.detailPlayButton.setOnClickListener {

View file

@ -34,7 +34,7 @@ import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.ui.ActionHeaderViewHolder import org.oxycblt.auxio.ui.ActionHeaderViewHolder
import org.oxycblt.auxio.ui.BaseViewHolder import org.oxycblt.auxio.ui.BaseViewHolder
import org.oxycblt.auxio.ui.DiffCallback import org.oxycblt.auxio.ui.DiffCallback
import org.oxycblt.auxio.util.getPlural import org.oxycblt.auxio.util.getPluralSafe
import org.oxycblt.auxio.util.inflater import org.oxycblt.auxio.util.inflater
/** /**
@ -145,7 +145,7 @@ class GenreDetailAdapter(
binding.detailName.text = data.resolvedName binding.detailName.text = data.resolvedName
binding.detailSubhead.apply { binding.detailSubhead.apply {
text = context.getPlural(R.plurals.fmt_song_count, data.songs.size) text = context.getPluralSafe(R.plurals.fmt_song_count, data.songs.size)
} }
binding.detailInfo.text = data.totalDuration binding.detailInfo.text = data.totalDuration

View file

@ -3,6 +3,7 @@ package org.oxycblt.auxio.home
import android.content.Context import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import com.google.android.material.floatingactionbutton.FloatingActionButton import com.google.android.material.floatingactionbutton.FloatingActionButton
import org.oxycblt.auxio.util.getDimenSizeSafe
import com.google.android.material.R as MaterialR import com.google.android.material.R as MaterialR
/** /**
@ -20,11 +21,17 @@ class AdaptiveFloatingActionButton @JvmOverloads constructor(
size = SIZE_NORMAL size = SIZE_NORMAL
if (resources.configuration.smallestScreenWidthDp >= 640) { if (resources.configuration.smallestScreenWidthDp >= 640) {
// Use a large FAB on large screens, as it makes it easier to touch. val largeFabSize = context.getDimenSizeSafe(
customSize = resources.getDimensionPixelSize(MaterialR.dimen.m3_large_fab_size) MaterialR.dimen.m3_large_fab_size
setMaxImageSize(
resources.getDimensionPixelSize(MaterialR.dimen.m3_large_fab_max_image_size)
) )
val largeImageSize = context.getDimenSizeSafe(
MaterialR.dimen.m3_large_fab_max_image_size
)
// Use a large FAB on large screens, as it makes it easier to touch.
customSize = largeFabSize
setMaxImageSize(largeImageSize)
} }
} }
} }

View file

@ -31,7 +31,8 @@ import android.os.Build
import android.view.View import android.view.View
import androidx.core.graphics.drawable.DrawableCompat import androidx.core.graphics.drawable.DrawableCompat
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.util.resolveAttr import org.oxycblt.auxio.util.getAttrColorSafe
import org.oxycblt.auxio.util.getDimenOffsetSafe
import kotlin.math.sqrt import kotlin.math.sqrt
/** /**
@ -54,15 +55,15 @@ import kotlin.math.sqrt
class FastScrollPopupDrawable(context: Context) : Drawable() { class FastScrollPopupDrawable(context: Context) : Drawable() {
private val paint: Paint = Paint().apply { private val paint: Paint = Paint().apply {
isAntiAlias = true isAntiAlias = true
color = R.attr.colorSecondary.resolveAttr(context) color = context.getAttrColorSafe(R.attr.colorSecondary)
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 = context.resources.getDimensionPixelOffset(R.dimen.spacing_medium) private val paddingStart = context.getDimenOffsetSafe(R.dimen.spacing_medium)
private val paddingEnd = context.resources.getDimensionPixelOffset(R.dimen.popup_padding_end) private val paddingEnd = context.getDimenOffsetSafe(R.dimen.popup_padding_end)
override fun draw(canvas: Canvas) { override fun draw(canvas: Canvas) {
canvas.drawPath(path, paint) canvas.drawPath(path, paint)

View file

@ -42,8 +42,10 @@ import androidx.recyclerview.widget.LinearLayoutManager
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.canScroll import org.oxycblt.auxio.util.canScroll
import org.oxycblt.auxio.util.resolveAttr import org.oxycblt.auxio.util.getAttrColorSafe
import org.oxycblt.auxio.util.resolveDrawable import org.oxycblt.auxio.util.getDimenOffsetSafe
import org.oxycblt.auxio.util.getDimenSizeSafe
import org.oxycblt.auxio.util.getDrawableSafe
import org.oxycblt.auxio.util.systemBarInsetsCompat import org.oxycblt.auxio.util.systemBarInsetsCompat
import kotlin.math.abs import kotlin.math.abs
@ -86,7 +88,7 @@ class FastScrollRecyclerView @JvmOverloads constructor(
*/ */
var onDragListener: ((Boolean) -> Unit)? = null var onDragListener: ((Boolean) -> Unit)? = null
private val minTouchTargetSize: Int = resources.getDimensionPixelSize(R.dimen.size_btn_small) private val minTouchTargetSize: Int = context.getDimenSizeSafe(R.dimen.size_btn_small)
private val touchSlop: Int = ViewConfiguration.get(context).scaledTouchSlop private val touchSlop: Int = ViewConfiguration.get(context).scaledTouchSlop
// Views for the track, thumb, and popup. Note that the track view is mostly vestigial // Views for the track, thumb, and popup. Note that the track view is mostly vestigial
@ -122,7 +124,7 @@ class FastScrollRecyclerView @JvmOverloads constructor(
private val scrollerPadding = Rect(0, 0, 0, 0) private val scrollerPadding = Rect(0, 0, 0, 0)
init { init {
val thumbDrawable = R.drawable.ui_scroll_thumb.resolveDrawable(context) val thumbDrawable = context.getDrawableSafe(R.drawable.ui_scroll_thumb)
trackView = View(context) trackView = View(context)
thumbView = View(context).apply { thumbView = View(context).apply {
@ -136,25 +138,19 @@ class FastScrollRecyclerView @JvmOverloads constructor(
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT
) )
minimumWidth = resources.getDimensionPixelSize( minimumWidth = context.getDimenSizeSafe(R.dimen.popup_min_width)
R.dimen.popup_min_width minimumHeight = context.getDimenSizeSafe(R.dimen.size_btn_large)
)
minimumHeight = resources.getDimensionPixelSize(
R.dimen.size_btn_large
)
(layoutParams as FrameLayout.LayoutParams).apply { (layoutParams as FrameLayout.LayoutParams).apply {
gravity = Gravity.CENTER_HORIZONTAL or Gravity.TOP gravity = Gravity.CENTER_HORIZONTAL or Gravity.TOP
marginEnd = resources.getDimensionPixelOffset( marginEnd = context.getDimenOffsetSafe(R.dimen.spacing_small)
R.dimen.spacing_small
)
} }
TextViewCompat.setTextAppearance(this, R.style.TextAppearance_Auxio_HeadlineLarge) TextViewCompat.setTextAppearance(this, R.style.TextAppearance_Auxio_HeadlineLarge)
setTextColor(R.attr.colorOnSecondary.resolveAttr(context)) setTextColor(context.getAttrColorSafe(R.attr.colorOnSecondary))
background = FastScrollPopupDrawable(context) background = FastScrollPopupDrawable(context)
elevation = resources.getDimensionPixelOffset(R.dimen.elevation_normal).toFloat() elevation = context.getDimenSizeSafe(R.dimen.elevation_normal).toFloat()
ellipsize = TextUtils.TruncateAt.MIDDLE ellipsize = TextUtils.TruncateAt.MIDDLE
gravity = Gravity.CENTER gravity = Gravity.CENTER
includeFontPadding = false includeFontPadding = false

View file

@ -24,7 +24,7 @@ import android.widget.TextView
import androidx.core.text.isDigitsOnly import androidx.core.text.isDigitsOnly
import androidx.databinding.BindingAdapter import androidx.databinding.BindingAdapter
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.util.getPlural import org.oxycblt.auxio.util.getPluralSafe
/** /**
* A complete array of all the hardcoded genre values for ID3(v2), contains standard genres and * A complete array of all the hardcoded genre values for ID3(v2), contains standard genres and
@ -128,7 +128,7 @@ fun Int.toDate(context: Context): String {
fun TextView.bindArtistCounts(artist: Artist) { fun TextView.bindArtistCounts(artist: Artist) {
text = context.getString( text = context.getString(
R.string.fmt_counts, R.string.fmt_counts,
context.getPlural(R.plurals.fmt_album_count, artist.albums.size), context.getPluralSafe(R.plurals.fmt_album_count, artist.albums.size),
context.getPlural(R.plurals.fmt_song_count, artist.songs.size) context.getPluralSafe(R.plurals.fmt_song_count, artist.songs.size)
) )
} }

View file

@ -30,8 +30,8 @@ import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.ViewPlaybackBarBinding import org.oxycblt.auxio.databinding.ViewPlaybackBarBinding
import org.oxycblt.auxio.detail.DetailViewModel import org.oxycblt.auxio.detail.DetailViewModel
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.util.getAttrColorSafe
import org.oxycblt.auxio.util.inflater import org.oxycblt.auxio.util.inflater
import org.oxycblt.auxio.util.resolveAttr
import org.oxycblt.auxio.util.systemBarInsetsCompat import org.oxycblt.auxio.util.systemBarInsetsCompat
/** /**
@ -53,7 +53,7 @@ class PlaybackBarView @JvmOverloads constructor(
// colorSurfaceVariant is used with the assumption that the view that is using it is // colorSurfaceVariant is used with the assumption that the view that is using it is
// not elevated and is therefore not colored. This view is elevated. // not elevated and is therefore not colored. This view is elevated.
binding.playbackProgressBar.trackColor = MaterialColors.compositeARGBWithAlpha( binding.playbackProgressBar.trackColor = MaterialColors.compositeARGBWithAlpha(
R.attr.colorSecondary.resolveAttr(context), (255 * 0.2).toInt() context.getAttrColorSafe(R.attr.colorSecondary), (255 * 0.2).toInt()
) )
} }

View file

@ -1,7 +1,6 @@
package org.oxycblt.auxio.playback package org.oxycblt.auxio.playback
import android.content.Context import android.content.Context
import android.content.res.ColorStateList
import android.graphics.Canvas import android.graphics.Canvas
import android.graphics.Rect import android.graphics.Rect
import android.graphics.drawable.LayerDrawable import android.graphics.drawable.LayerDrawable
@ -24,9 +23,12 @@ import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.detail.DetailViewModel import org.oxycblt.auxio.detail.DetailViewModel
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.util.getAttrColorSafe
import org.oxycblt.auxio.util.getDimenSafe
import org.oxycblt.auxio.util.getDrawableSafe
import org.oxycblt.auxio.util.pxOfDp
import org.oxycblt.auxio.util.replaceInsetsCompat import org.oxycblt.auxio.util.replaceInsetsCompat
import org.oxycblt.auxio.util.resolveAttr import org.oxycblt.auxio.util.stateList
import org.oxycblt.auxio.util.resolveDrawable
import org.oxycblt.auxio.util.systemBarInsetsCompat import org.oxycblt.auxio.util.systemBarInsetsCompat
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.max import kotlin.math.max
@ -96,6 +98,7 @@ class PlaybackLayout @JvmOverloads constructor(
private var initMotionX = 0f private var initMotionX = 0f
private var initMotionY = 0f private var initMotionY = 0f
private val tRect = Rect() private val tRect = Rect()
private val elevationNormal = context.getDimenSafe(R.dimen.elevation_normal)
/** See [isDragging] */ /** See [isDragging] */
private val dragStateField = ViewDragHelper::class.java.getDeclaredField("mDragState").apply { private val dragStateField = ViewDragHelper::class.java.getDeclaredField("mDragState").apply {
@ -115,15 +118,15 @@ class PlaybackLayout @JvmOverloads constructor(
isFocusableInTouchMode = false isFocusableInTouchMode = false
playbackContainerBg = MaterialShapeDrawable.createWithElevationOverlay(context).apply { playbackContainerBg = MaterialShapeDrawable.createWithElevationOverlay(context).apply {
fillColor = ColorStateList.valueOf(R.attr.colorSurface.resolveAttr(context)) fillColor = context.getAttrColorSafe(R.attr.colorSurface).stateList
elevation = resources.getDimensionPixelSize(R.dimen.elevation_normal).toFloat() elevation = context.pxOfDp(elevationNormal).toFloat()
} }
// The way we fade out the elevation overlay is not by actually reducing the elevation // The way we fade out the elevation overlay is not by actually reducing the elevation
// but by fading out the background drawable itself. To be safe, we apply this // but by fading out the background drawable itself. To be safe, we apply this
// background drawable to a layer list with another colorSurface shape drawable, just // background drawable to a layer list with another colorSurface shape drawable, just
// in case weird things happen if background drawable is completely transparent. // in case weird things happen if background drawable is completely transparent.
background = (R.drawable.ui_panel_bg.resolveDrawable(context) as LayerDrawable).apply { background = (context.getDrawableSafe(R.drawable.ui_panel_bg) as LayerDrawable).apply {
setDrawableByLayerId(R.id.panel_overlay, playbackContainerBg) setDrawableByLayerId(R.id.panel_overlay, playbackContainerBg)
} }
} }
@ -534,6 +537,7 @@ class PlaybackLayout @JvmOverloads constructor(
// Slowly reduce the elevation of the container as we slide up, eventually resulting in a // Slowly reduce the elevation of the container as we slide up, eventually resulting in a
// neutral color instead of an elevated one when fully expanded. // neutral color instead of an elevated one when fully expanded.
playbackContainerBg.alpha = (outRatio * 255).toInt() playbackContainerBg.alpha = (outRatio * 255).toInt()
playbackContainerView.translationZ = elevationNormal * outRatio
// Fade out our bar view as we slide up // Fade out our bar view as we slide up
playbackBarView.apply { playbackBarView.apply {

View file

@ -20,7 +20,6 @@ package org.oxycblt.auxio.playback
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.content.res.ColorStateList
import android.util.AttributeSet import android.util.AttributeSet
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import com.google.android.material.color.MaterialColors import com.google.android.material.color.MaterialColors
@ -28,8 +27,9 @@ import com.google.android.material.slider.Slider
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.ViewSeekBarBinding import org.oxycblt.auxio.databinding.ViewSeekBarBinding
import org.oxycblt.auxio.music.toDuration import org.oxycblt.auxio.music.toDuration
import org.oxycblt.auxio.util.getAttrColorSafe
import org.oxycblt.auxio.util.inflater import org.oxycblt.auxio.util.inflater
import org.oxycblt.auxio.util.resolveAttr import org.oxycblt.auxio.util.stateList
/** /**
* A custom view that bundles together a seekbar with a current duration and a total duration. * A custom view that bundles together a seekbar with a current duration and a total duration.
@ -53,11 +53,9 @@ class PlaybackSeekBar @JvmOverloads constructor(
binding.seekBar.addOnSliderTouchListener(this) binding.seekBar.addOnSliderTouchListener(this)
// Override the inactive color so that it lines up with the playback progress bar. // Override the inactive color so that it lines up with the playback progress bar.
binding.seekBar.trackInactiveTintList = ColorStateList.valueOf( binding.seekBar.trackInactiveTintList = MaterialColors.compositeARGBWithAlpha(
MaterialColors.compositeARGBWithAlpha( context.getAttrColorSafe(R.attr.colorSecondary), (255 * 0.2).toInt()
R.attr.colorSecondary.resolveAttr(context), (255 * 0.2).toInt() ).stateList
)
)
} }
fun setProgress(seconds: Long) { fun setProgress(seconds: Long) {

View file

@ -19,7 +19,6 @@
package org.oxycblt.auxio.playback.queue package org.oxycblt.auxio.playback.queue
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.res.ColorStateList
import android.graphics.drawable.ColorDrawable import android.graphics.drawable.ColorDrawable
import android.view.MotionEvent import android.view.MotionEvent
import android.view.View import android.view.View
@ -40,6 +39,7 @@ import org.oxycblt.auxio.ui.DiffCallback
import org.oxycblt.auxio.ui.HeaderViewHolder import org.oxycblt.auxio.ui.HeaderViewHolder
import org.oxycblt.auxio.util.inflater import org.oxycblt.auxio.util.inflater
import org.oxycblt.auxio.util.logE import org.oxycblt.auxio.util.logE
import org.oxycblt.auxio.util.stateList
/** /**
* The single adapter for both the Next Queue and the User Queue. * The single adapter for both the Next Queue and the User Queue.
@ -130,7 +130,7 @@ class QueueAdapter(
binding.body.background = MaterialShapeDrawable.createWithElevationOverlay( binding.body.background = MaterialShapeDrawable.createWithElevationOverlay(
binding.root.context binding.root.context
).apply { ).apply {
fillColor = ColorStateList.valueOf((binding.body.background as ColorDrawable).color) fillColor = (binding.body.background as ColorDrawable).color.stateList
} }
} }

View file

@ -26,6 +26,7 @@ import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.shape.MaterialShapeDrawable import com.google.android.material.shape.MaterialShapeDrawable
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.util.getDimenSafe
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
@ -90,7 +91,7 @@ class QueueDragCallback(private val playbackModel: PlaybackViewModel) : ItemTouc
if (shouldLift && isCurrentlyActive && actionState == ItemTouchHelper.ACTION_STATE_DRAG) { if (shouldLift && isCurrentlyActive && actionState == ItemTouchHelper.ACTION_STATE_DRAG) {
val bg = holder.bodyView.background as MaterialShapeDrawable val bg = holder.bodyView.background as MaterialShapeDrawable
val elevation = recyclerView.resources.getDimension(R.dimen.elevation_small) val elevation = recyclerView.context.getDimenSafe(R.dimen.elevation_small)
holder.itemView.animate() holder.itemView.animate()
.translationZ(elevation) .translationZ(elevation)

View file

@ -7,8 +7,8 @@ import androidx.appcompat.widget.SwitchCompat
import androidx.preference.PreferenceViewHolder import androidx.preference.PreferenceViewHolder
import androidx.preference.SwitchPreferenceCompat import androidx.preference.SwitchPreferenceCompat
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.util.resolveDrawable import org.oxycblt.auxio.util.getColorStateListSafe
import org.oxycblt.auxio.util.resolveStateList import org.oxycblt.auxio.util.getDrawableSafe
/** /**
* A [SwitchPreferenceCompat] that emulates the M3 switches until the design team * A [SwitchPreferenceCompat] that emulates the M3 switches until the design team
@ -31,10 +31,10 @@ class M3SwitchPreference @JvmOverloads constructor(
if (switch is SwitchCompat) { if (switch is SwitchCompat) {
switch.apply { switch.apply {
trackDrawable = R.drawable.ui_m3_switch_track.resolveDrawable(context) trackDrawable = context.getDrawableSafe(R.drawable.ui_m3_switch_track)
trackTintList = R.color.sel_m3_switch_track.resolveStateList(context) trackTintList = context.getColorStateListSafe(R.color.sel_m3_switch_track)
thumbDrawable = R.drawable.ui_m3_switch_thumb.resolveDrawable(context) thumbDrawable = context.getDrawableSafe(R.drawable.ui_m3_switch_thumb)
thumbTintList = R.color.sel_m3_switch_thumb.resolveStateList(context) thumbTintList = context.getColorStateListSafe(R.color.sel_m3_switch_thumb)
} }
needToUpdateSwitch = false needToUpdateSwitch = false

View file

@ -21,13 +21,25 @@ package org.oxycblt.auxio.util
import android.app.PendingIntent import android.app.PendingIntent
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
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.os.Build import android.os.Build
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.ColorInt
import androidx.annotation.ColorRes
import androidx.annotation.DimenRes
import androidx.annotation.Dimension
import androidx.annotation.DrawableRes
import androidx.annotation.PluralsRes import androidx.annotation.PluralsRes
import androidx.annotation.Px
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.MainActivity import org.oxycblt.auxio.MainActivity
import kotlin.reflect.KClass import kotlin.reflect.KClass
import kotlin.system.exitProcess import kotlin.system.exitProcess
@ -47,6 +59,150 @@ val Context.isNight: Boolean 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
/**
* Returns if this device is in landscape.
*/
val Context.isLandscape get() =
resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
/**
* Convenience method for getting a plural.
* @param pluralsRes Resource for the plural
* @param value Int value for the plural.
* @return The formatted string requested
*/
fun Context.getPluralSafe(@PluralsRes pluralsRes: Int, value: Int): String {
return try {
resources.getQuantityString(pluralsRes, value, value)
} catch (e: Exception) {
logE("plural load failed")
return "<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.
* @param color The color resource
* @return The [ColorStateList] requested, or black if an error occurred.
*/
fun Context.getColorStateListSafe(@ColorRes color: Int): ColorStateList {
return try {
requireNotNull(ContextCompat.getColorStateList(this, color))
} catch (e: Exception) {
handleResourceFailure(e, "color state list", getColorSafe(android.R.color.black).stateList)
}
}
/**
* Convenience method for getting a color attribute safely.
* @param attr The color attribute
* @return The attribute requested, or black if an error occurred.
*/
@ColorInt
fun Context.getAttrColorSafe(@AttrRes attr: Int): Int {
// First resolve the attribute into its ID
val resolvedAttr = TypedValue()
theme.resolveAttribute(attr, resolvedAttr, true)
// Then convert it to a proper color
val color = if (resolvedAttr.resourceId != 0) {
resolvedAttr.resourceId
} else {
resolvedAttr.data
}
return getColorSafe(color)
}
/**
* Convenience method for getting a [Drawable] safely.
* @param drawable The drawable resource
* @return The drawable requested, or black if an error occurred.
*/
fun Context.getDrawableSafe(@DrawableRes drawable: Int): Drawable {
return try {
requireNotNull(ContextCompat.getDrawable(this, drawable))
} catch (e: Exception) {
handleResourceFailure(e, "drawable", ColorDrawable(getColorSafe(android.R.color.black)))
}
}
/**
* Convenience method for getting a dimension safely.
* @param dimen The dimension resource
* @return The dimension requested, or 0 if an error occurred.
*/
@Dimension
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.
* @param dimen The dimension resource
* @return The dimension requested, in pixels, or 0 if an error occurred.
*/
@Px
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)
}
}
@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.")
if (BuildConfig.DEBUG) {
// I'd rather be aware of a sudden crash when debugging.
throw e
} else {
// Not so much when the app is in production.
logE(e.stackTraceToString())
return default
}
}
/** /**
* Convenience method for getting a system service without nullability issues. * Convenience method for getting a system service without nullability issues.
* @param T The system service in question. * @param T The system service in question.
@ -60,6 +216,25 @@ fun <T : Any> Context.getSystemServiceSafe(serviceClass: KClass<T>): T {
} }
} }
/**
* Create a toast using the provided string resource.
*/
fun Context.showToast(@StringRes str: Int) {
Toast.makeText(applicationContext, getString(str), Toast.LENGTH_SHORT).show()
}
/**
* Create a [PendingIntent] that leads to Auxio's [MainActivity]
*/
fun Context.newMainIntent(): PendingIntent {
return PendingIntent.getActivity(
this, INTENT_REQUEST_CODE, Intent(this, MainActivity::class.java),
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
PendingIntent.FLAG_IMMUTABLE
else 0
)
}
/** /**
* Create a broadcast [PendingIntent] * Create a broadcast [PendingIntent]
*/ */
@ -73,17 +248,8 @@ fun Context.newBroadcastIntent(what: String): PendingIntent {
} }
/** /**
* Create a [PendingIntent] that leads to Auxio's [MainActivity] * Hard-restarts the app. Useful for forcing the app to reload music.
*/ */
fun Context.newMainIntent(): PendingIntent {
return PendingIntent.getActivity(
this, INTENT_REQUEST_CODE, Intent(this, MainActivity::class.java),
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
PendingIntent.FLAG_IMMUTABLE
else 0
)
}
fun Context.hardRestart() { fun Context.hardRestart() {
// Instead of having to do a ton of cleanup and horrible code changes // Instead of having to do a ton of cleanup and horrible code changes
// to restart this application non-destructively, I just restart the UI task [There is only // to restart this application non-destructively, I just restart the UI task [There is only
@ -96,27 +262,3 @@ fun Context.hardRestart() {
exitProcess(0) exitProcess(0)
} }
/**
* Create a toast using the provided string resource.
*/
fun Context.showToast(@StringRes str: Int) {
Toast.makeText(applicationContext, getString(str), Toast.LENGTH_SHORT).show()
}
/**
* Convenience method for getting a plural.
* @param pluralsRes Resource for the plural
* @param value Int value for the plural.
* @return The formatted string requested
*/
fun Context.getPlural(@PluralsRes pluralsRes: Int, value: Int): String {
return resources.getQuantityString(pluralsRes, value, value)
}
/**
* Determine if the device is currently in landscape.
*/
fun Context.isLandscape(): Boolean {
return resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
}

View file

@ -18,23 +18,21 @@
package org.oxycblt.auxio.util package org.oxycblt.auxio.util
import android.content.Context
import android.content.res.ColorStateList import android.content.res.ColorStateList
import android.content.res.Resources
import android.graphics.Insets import android.graphics.Insets
import android.graphics.Rect import android.graphics.Rect
import android.os.Build import android.os.Build
import android.util.TypedValue
import android.view.WindowInsets import android.view.WindowInsets
import androidx.annotation.AttrRes
import androidx.annotation.ColorInt
import androidx.annotation.ColorRes import androidx.annotation.ColorRes
import androidx.annotation.DrawableRes
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
/**
* Converts this color to a single-color [ColorStateList].
*/
val @receiver:ColorRes Int.stateList get() = ColorStateList.valueOf(this)
/** /**
* Apply the recommended spans for a [RecyclerView]. * Apply the recommended spans for a [RecyclerView].
* *
@ -64,60 +62,6 @@ fun RecyclerView.applySpans(shouldBeFullWidth: ((Int) -> Boolean)? = null) {
*/ */
fun RecyclerView.canScroll(): Boolean = computeVerticalScrollRange() > height fun RecyclerView.canScroll(): Boolean = computeVerticalScrollRange() > height
/**
* Resolve a color.
* @param context [Context] required
* @return The resolved color, black if the resolving process failed.
*/
@ColorInt
fun @receiver:ColorRes Int.resolveColor(context: Context): Int {
return try {
ContextCompat.getColor(context, this)
} catch (e: Resources.NotFoundException) {
logE("Attempted color load failed: ${e.stackTraceToString()}")
// Default to the emergency color [Black] if the loading fails.
ContextCompat.getColor(context, android.R.color.black)
}
}
/**
* Resolve a color and turn it into a [ColorStateList]
* @param context [Context] required
* @return The resolved color as a [ColorStateList]
* @see resolveColor
*/
fun @receiver:ColorRes Int.resolveStateList(context: Context) =
ContextCompat.getColorStateList(context, this)
/*
* Resolve a color and turn it into a [ColorStateList]
* @param context [Context] required
* @return The resolved color as a [ColorStateList]
* @see resolveColor
*/
fun @receiver:DrawableRes Int.resolveDrawable(context: Context) =
requireNotNull(ContextCompat.getDrawable(context, this))
/**
* Resolve this int into a color as if it was an attribute
*/
@ColorInt
fun @receiver:AttrRes Int.resolveAttr(context: Context): Int {
// First resolve the attribute into its ID
val resolvedAttr = TypedValue()
context.theme.resolveAttribute(this, resolvedAttr, true)
// Then convert it to a proper color
val color = if (resolvedAttr.resourceId != 0) {
resolvedAttr.resourceId
} else {
resolvedAttr.data
}
return color.resolveColor(context)
}
/** /**
* Resolve window insets in a version-aware manner. This can be used to apply padding to * Resolve window insets in a version-aware manner. This can be used to apply padding to
* a view that properly follows all the frustrating changes that were made between 8-11. * a view that properly follows all the frustrating changes that were made between 8-11.

View file

@ -36,6 +36,7 @@ import coil.transform.RoundedCornersTransformation
import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.playback.state.PlaybackStateManager
import org.oxycblt.auxio.util.getDimenSizeSafe
import org.oxycblt.auxio.util.isLandscape import org.oxycblt.auxio.util.isLandscape
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import kotlin.math.min import kotlin.math.min
@ -106,9 +107,8 @@ class WidgetProvider : AppWidgetProvider() {
// we get a 1:1 aspect ratio image results in clipToOutline not working well. // we get a 1:1 aspect ratio image results in clipToOutline not working well.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val transform = RoundedCornersTransformation( val transform = RoundedCornersTransformation(
context.resources.getDimensionPixelSize( context.getDimenSizeSafe(android.R.dimen.system_app_widget_inner_radius)
android.R.dimen.system_app_widget_inner_radius .toFloat()
).toFloat()
) )
coverRequest.transformations(transform) coverRequest.transformations(transform)
@ -199,7 +199,7 @@ class WidgetProvider : AppWidgetProvider() {
var height: Int var height: Int
// Landscape/Portrait modes use different dimen bounds // Landscape/Portrait modes use different dimen bounds
if (context.isLandscape()) { if (context.isLandscape) {
width = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH) width = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH)
height = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT) height = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT)
} else { } else {

View file

@ -39,12 +39,15 @@
<!-- Widget TextView that mimics the main Auxio Primary TextView --> <!-- Widget TextView that mimics the main Auxio Primary TextView -->
<style name="Widget.Auxio.TextView.Primary.AppWidget" parent="Widget.Auxio.TextView.AppWidget"> <style name="Widget.Auxio.TextView.Primary.AppWidget" parent="Widget.Auxio.TextView.AppWidget">
<item name="android:textStyle">normal</item>
<item name="android:fontFamily">sans-serif-medium</item> <item name="android:fontFamily">sans-serif-medium</item>
<item name="android:textAppearance">@style/TextAppearance.Auxio.TitleMidLarge</item> <item name="android:textAppearance">@style/TextAppearance.Auxio.TitleMidLarge</item>
</style> </style>
<!-- Widget TextView that mimics the main Auxio Secondary TextView --> <!-- Widget TextView that mimics the main Auxio Secondary TextView -->
<style name="Widget.Auxio.TextView.Secondary.AppWidget" parent="Widget.Auxio.TextView.AppWidget"> <style name="Widget.Auxio.TextView.Secondary.AppWidget" parent="Widget.Auxio.TextView.AppWidget">
<item name="android:textStyle">normal</item>
<item name="android:fontFamily">sans-serif</item>
<item name="android:textColor">?android:attr/textColorSecondary</item> <item name="android:textColor">?android:attr/textColorSecondary</item>
<item name="android:textAppearance">@style/TextAppearance.Auxio.TitleMedium</item> <item name="android:textAppearance">@style/TextAppearance.Auxio.TitleMedium</item>
</style> </style>

View file

@ -163,7 +163,7 @@
<!-- <!--
Abuse this floating action button to act more like an old-school auxio button. Abuse this floating action button to act more like an old-school auxio button.
This is only done because the elevation show acts weird with the panel layout. This is only done because elevation acts weird with the panel layout.
--> -->
<item name="android:elevation">0dp</item> <item name="android:elevation">0dp</item>
<item name="elevation">0dp</item> <item name="elevation">0dp</item>