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
import android.content.res.ColorStateList
import android.view.ViewGroup
import androidx.appcompat.widget.TooltipCompat
import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.R
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.resolveAttr
import org.oxycblt.auxio.util.resolveColor
import org.oxycblt.auxio.util.resolveStateList
import org.oxycblt.auxio.util.stateList
/**
* An adapter that displays the list of all possible accents, and highlights the current one.
@ -63,7 +62,7 @@ class AccentAdapter(
setSelected(accent == curAccent)
binding.accent.apply {
backgroundTintList = ColorStateList.valueOf(accent.primary.resolveColor(context))
backgroundTintList = context.getColorSafe(accent.primary).stateList
contentDescription = context.getString(accent.name)
TooltipCompat.setTooltipText(this, contentDescription)
}
@ -84,9 +83,9 @@ class AccentAdapter(
selectedViewHolder?.setSelected(false)
selectedViewHolder = this
ColorStateList.valueOf(R.attr.colorSurface.resolveAttr(context))
context.getAttrColorSafe(R.attr.colorSurface).stateList
} 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.util.AttributeSet
import android.util.TypedValue
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.util.pxOfDp
import kotlin.math.max
/**
@ -38,9 +38,7 @@ class AutoGridLayoutManager(
) : GridLayoutManager(context, attrs, defStyleAttr, defStyleRes) {
// 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.
private var columnWidth = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, 72F, context.resources.displayMetrics
).toInt()
private var columnWidth = context.pxOfDp(72f)
private var lastWidth = -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.BaseViewHolder
import org.oxycblt.auxio.ui.DiffCallback
import org.oxycblt.auxio.util.getPlural
import org.oxycblt.auxio.util.getPluralSafe
import org.oxycblt.auxio.util.inflater
/**
@ -156,7 +156,7 @@ class AlbumDetailAdapter(
binding.detailInfo.text = binding.detailInfo.context.getString(
R.string.fmt_three,
data.year.toDate(binding.detailInfo.context),
binding.detailInfo.context.getPlural(
binding.detailInfo.context.getPluralSafe(
R.plurals.fmt_song_count,
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.DiffCallback
import org.oxycblt.auxio.ui.HeaderViewHolder
import org.oxycblt.auxio.util.getPlural
import org.oxycblt.auxio.util.getPluralSafe
import org.oxycblt.auxio.util.inflater
/**
@ -207,8 +207,8 @@ class ArtistDetailAdapter(
binding.detailInfo.text = context.getString(
R.string.fmt_counts,
context.getPlural(R.plurals.fmt_album_count, data.albums.size),
context.getPlural(R.plurals.fmt_song_count, data.songs.size)
context.getPluralSafe(R.plurals.fmt_album_count, data.albums.size),
context.getPluralSafe(R.plurals.fmt_song_count, data.songs.size)
)
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.BaseViewHolder
import org.oxycblt.auxio.ui.DiffCallback
import org.oxycblt.auxio.util.getPlural
import org.oxycblt.auxio.util.getPluralSafe
import org.oxycblt.auxio.util.inflater
/**
@ -145,7 +145,7 @@ class GenreDetailAdapter(
binding.detailName.text = data.resolvedName
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

View file

@ -3,6 +3,7 @@ package org.oxycblt.auxio.home
import android.content.Context
import android.util.AttributeSet
import com.google.android.material.floatingactionbutton.FloatingActionButton
import org.oxycblt.auxio.util.getDimenSizeSafe
import com.google.android.material.R as MaterialR
/**
@ -20,11 +21,17 @@ class AdaptiveFloatingActionButton @JvmOverloads constructor(
size = SIZE_NORMAL
if (resources.configuration.smallestScreenWidthDp >= 640) {
// Use a large FAB on large screens, as it makes it easier to touch.
customSize = resources.getDimensionPixelSize(MaterialR.dimen.m3_large_fab_size)
setMaxImageSize(
resources.getDimensionPixelSize(MaterialR.dimen.m3_large_fab_max_image_size)
val largeFabSize = context.getDimenSizeSafe(
MaterialR.dimen.m3_large_fab_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 androidx.core.graphics.drawable.DrawableCompat
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
/**
@ -54,15 +55,15 @@ import kotlin.math.sqrt
class FastScrollPopupDrawable(context: Context) : Drawable() {
private val paint: Paint = Paint().apply {
isAntiAlias = true
color = R.attr.colorSecondary.resolveAttr(context)
color = context.getAttrColorSafe(R.attr.colorSecondary)
style = Paint.Style.FILL
}
private val path = Path()
private val matrix = Matrix()
private val paddingStart = context.resources.getDimensionPixelOffset(R.dimen.spacing_medium)
private val paddingEnd = context.resources.getDimensionPixelOffset(R.dimen.popup_padding_end)
private val paddingStart = context.getDimenOffsetSafe(R.dimen.spacing_medium)
private val paddingEnd = context.getDimenOffsetSafe(R.dimen.popup_padding_end)
override fun draw(canvas: Canvas) {
canvas.drawPath(path, paint)

View file

@ -42,8 +42,10 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.R
import org.oxycblt.auxio.util.canScroll
import org.oxycblt.auxio.util.resolveAttr
import org.oxycblt.auxio.util.resolveDrawable
import org.oxycblt.auxio.util.getAttrColorSafe
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 kotlin.math.abs
@ -86,7 +88,7 @@ class FastScrollRecyclerView @JvmOverloads constructor(
*/
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
// 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)
init {
val thumbDrawable = R.drawable.ui_scroll_thumb.resolveDrawable(context)
val thumbDrawable = context.getDrawableSafe(R.drawable.ui_scroll_thumb)
trackView = View(context)
thumbView = View(context).apply {
@ -136,25 +138,19 @@ class FastScrollRecyclerView @JvmOverloads constructor(
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT
)
minimumWidth = resources.getDimensionPixelSize(
R.dimen.popup_min_width
)
minimumHeight = resources.getDimensionPixelSize(
R.dimen.size_btn_large
)
minimumWidth = context.getDimenSizeSafe(R.dimen.popup_min_width)
minimumHeight = context.getDimenSizeSafe(R.dimen.size_btn_large)
(layoutParams as FrameLayout.LayoutParams).apply {
gravity = Gravity.CENTER_HORIZONTAL or Gravity.TOP
marginEnd = resources.getDimensionPixelOffset(
R.dimen.spacing_small
)
marginEnd = context.getDimenOffsetSafe(R.dimen.spacing_small)
}
TextViewCompat.setTextAppearance(this, R.style.TextAppearance_Auxio_HeadlineLarge)
setTextColor(R.attr.colorOnSecondary.resolveAttr(context))
setTextColor(context.getAttrColorSafe(R.attr.colorOnSecondary))
background = FastScrollPopupDrawable(context)
elevation = resources.getDimensionPixelOffset(R.dimen.elevation_normal).toFloat()
elevation = context.getDimenSizeSafe(R.dimen.elevation_normal).toFloat()
ellipsize = TextUtils.TruncateAt.MIDDLE
gravity = Gravity.CENTER
includeFontPadding = false

View file

@ -24,7 +24,7 @@ import android.widget.TextView
import androidx.core.text.isDigitsOnly
import androidx.databinding.BindingAdapter
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
@ -128,7 +128,7 @@ fun Int.toDate(context: Context): String {
fun TextView.bindArtistCounts(artist: Artist) {
text = context.getString(
R.string.fmt_counts,
context.getPlural(R.plurals.fmt_album_count, artist.albums.size),
context.getPlural(R.plurals.fmt_song_count, artist.songs.size)
context.getPluralSafe(R.plurals.fmt_album_count, artist.albums.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.detail.DetailViewModel
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.util.getAttrColorSafe
import org.oxycblt.auxio.util.inflater
import org.oxycblt.auxio.util.resolveAttr
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
// not elevated and is therefore not colored. This view is elevated.
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
import android.content.Context
import android.content.res.ColorStateList
import android.graphics.Canvas
import android.graphics.Rect
import android.graphics.drawable.LayerDrawable
@ -24,9 +23,12 @@ import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.R
import org.oxycblt.auxio.detail.DetailViewModel
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.resolveAttr
import org.oxycblt.auxio.util.resolveDrawable
import org.oxycblt.auxio.util.stateList
import org.oxycblt.auxio.util.systemBarInsetsCompat
import kotlin.math.abs
import kotlin.math.max
@ -96,6 +98,7 @@ class PlaybackLayout @JvmOverloads constructor(
private var initMotionX = 0f
private var initMotionY = 0f
private val tRect = Rect()
private val elevationNormal = context.getDimenSafe(R.dimen.elevation_normal)
/** See [isDragging] */
private val dragStateField = ViewDragHelper::class.java.getDeclaredField("mDragState").apply {
@ -115,15 +118,15 @@ class PlaybackLayout @JvmOverloads constructor(
isFocusableInTouchMode = false
playbackContainerBg = MaterialShapeDrawable.createWithElevationOverlay(context).apply {
fillColor = ColorStateList.valueOf(R.attr.colorSurface.resolveAttr(context))
elevation = resources.getDimensionPixelSize(R.dimen.elevation_normal).toFloat()
fillColor = context.getAttrColorSafe(R.attr.colorSurface).stateList
elevation = context.pxOfDp(elevationNormal).toFloat()
}
// 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
// background drawable to a layer list with another colorSurface shape drawable, just
// 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)
}
}
@ -534,6 +537,7 @@ class PlaybackLayout @JvmOverloads constructor(
// 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.
playbackContainerBg.alpha = (outRatio * 255).toInt()
playbackContainerView.translationZ = elevationNormal * outRatio
// Fade out our bar view as we slide up
playbackBarView.apply {

View file

@ -20,7 +20,6 @@ package org.oxycblt.auxio.playback
import android.annotation.SuppressLint
import android.content.Context
import android.content.res.ColorStateList
import android.util.AttributeSet
import androidx.constraintlayout.widget.ConstraintLayout
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.databinding.ViewSeekBarBinding
import org.oxycblt.auxio.music.toDuration
import org.oxycblt.auxio.util.getAttrColorSafe
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.
@ -53,11 +53,9 @@ class PlaybackSeekBar @JvmOverloads constructor(
binding.seekBar.addOnSliderTouchListener(this)
// Override the inactive color so that it lines up with the playback progress bar.
binding.seekBar.trackInactiveTintList = ColorStateList.valueOf(
MaterialColors.compositeARGBWithAlpha(
R.attr.colorSecondary.resolveAttr(context), (255 * 0.2).toInt()
)
)
binding.seekBar.trackInactiveTintList = MaterialColors.compositeARGBWithAlpha(
context.getAttrColorSafe(R.attr.colorSecondary), (255 * 0.2).toInt()
).stateList
}
fun setProgress(seconds: Long) {

View file

@ -19,7 +19,6 @@
package org.oxycblt.auxio.playback.queue
import android.annotation.SuppressLint
import android.content.res.ColorStateList
import android.graphics.drawable.ColorDrawable
import android.view.MotionEvent
import android.view.View
@ -40,6 +39,7 @@ import org.oxycblt.auxio.ui.DiffCallback
import org.oxycblt.auxio.ui.HeaderViewHolder
import org.oxycblt.auxio.util.inflater
import org.oxycblt.auxio.util.logE
import org.oxycblt.auxio.util.stateList
/**
* The single adapter for both the Next Queue and the User Queue.
@ -130,7 +130,7 @@ class QueueAdapter(
binding.body.background = MaterialShapeDrawable.createWithElevationOverlay(
binding.root.context
).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 org.oxycblt.auxio.R
import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.util.getDimenSafe
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min
@ -90,7 +91,7 @@ class QueueDragCallback(private val playbackModel: PlaybackViewModel) : ItemTouc
if (shouldLift && isCurrentlyActive && actionState == ItemTouchHelper.ACTION_STATE_DRAG) {
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()
.translationZ(elevation)

View file

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

View file

@ -21,13 +21,25 @@ package org.oxycblt.auxio.util
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.res.ColorStateList
import android.content.res.Configuration
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import android.os.Build
import android.util.TypedValue
import android.view.LayoutInflater
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.Px
import androidx.annotation.StringRes
import androidx.core.content.ContextCompat
import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.MainActivity
import kotlin.reflect.KClass
import kotlin.system.exitProcess
@ -47,6 +59,150 @@ val Context.isNight: Boolean get() =
resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK ==
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.
* @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]
*/
@ -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() {
// 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
@ -96,27 +262,3 @@ fun Context.hardRestart() {
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
import android.content.Context
import android.content.res.ColorStateList
import android.content.res.Resources
import android.graphics.Insets
import android.graphics.Rect
import android.os.Build
import android.util.TypedValue
import android.view.WindowInsets
import androidx.annotation.AttrRes
import androidx.annotation.ColorInt
import androidx.annotation.ColorRes
import androidx.annotation.DrawableRes
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
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].
*
@ -64,60 +62,6 @@ fun RecyclerView.applySpans(shouldBeFullWidth: ((Int) -> Boolean)? = null) {
*/
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
* 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.music.Song
import org.oxycblt.auxio.playback.state.PlaybackStateManager
import org.oxycblt.auxio.util.getDimenSizeSafe
import org.oxycblt.auxio.util.isLandscape
import org.oxycblt.auxio.util.logD
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.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val transform = RoundedCornersTransformation(
context.resources.getDimensionPixelSize(
android.R.dimen.system_app_widget_inner_radius
).toFloat()
context.getDimenSizeSafe(android.R.dimen.system_app_widget_inner_radius)
.toFloat()
)
coverRequest.transformations(transform)
@ -199,7 +199,7 @@ class WidgetProvider : AppWidgetProvider() {
var height: Int
// Landscape/Portrait modes use different dimen bounds
if (context.isLandscape()) {
if (context.isLandscape) {
width = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH)
height = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT)
} else {

View file

@ -39,12 +39,15 @@
<!-- Widget TextView that mimics the main Auxio Primary TextView -->
<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:textAppearance">@style/TextAppearance.Auxio.TitleMidLarge</item>
</style>
<!-- Widget TextView that mimics the main Auxio Secondary TextView -->
<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:textAppearance">@style/TextAppearance.Auxio.TitleMedium</item>
</style>

View file

@ -163,7 +163,7 @@
<!--
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="elevation">0dp</item>