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 var callback: DynamicBackPressedCallback? = null
private var lastInsets: WindowInsets? = null
private var elevationNormal = -1f
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -66,10 +67,13 @@ class MainFragment :
override fun onBindingCreated(binding: FragmentMainBinding, savedInstanceState: Bundle?) {
// --- UI SETUP ---
requireActivity()
.onBackPressedDispatcher.addCallback(
val context = requireActivity()
context.onBackPressedDispatcher.addCallback(
viewLifecycleOwner, DynamicBackPressedCallback().also { callback = it })
elevationNormal = requireContext().getDimen(R.dimen.elevation_normal)
binding.root.setOnApplyWindowInsetsListener { _, insets ->
lastInsets = insets
insets
@ -77,8 +81,9 @@ class MainFragment :
// Send meaningful accessibility events for bottom sheets
ViewCompat.setAccessibilityPaneTitle(
binding.playbackSheet, getString(R.string.lbl_playback))
ViewCompat.setAccessibilityPaneTitle(binding.queueSheet, getString(R.string.lbl_queue))
binding.playbackSheet, context.getString(R.string.lbl_playback))
ViewCompat.setAccessibilityPaneTitle(
binding.queueSheet, context.getString(R.string.lbl_queue))
val queueSheetBehavior = binding.queueSheet.coordinatorLayoutBehavior as QueueSheetBehavior?
if (queueSheetBehavior != null) {
@ -97,8 +102,8 @@ class MainFragment :
binding.queueSheet.apply {
background =
MaterialShapeDrawable.createWithElevationOverlay(context).apply {
fillColor = context.getAttrColorSafe(R.attr.colorSurface).stateList
elevation = context.getDimenSafe(R.dimen.elevation_normal)
fillColor = context.getAttrColorCompat(R.attr.colorSurface)
elevation = context.getDimen(R.dimen.elevation_normal)
}
setOnApplyWindowInsetsListener { v, insets ->
@ -182,8 +187,7 @@ class MainFragment :
isInvisible = alpha == 0f
}
binding.playbackSheet.translationZ =
requireContext().getDimenSafe(R.dimen.elevation_normal) * outPlaybackRatio
binding.playbackSheet.translationZ = elevationNormal * outPlaybackRatio
playbackSheetBehavior.sheetBackgroundDrawable.alpha = (outPlaybackRatio * 255).toInt()
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.recycler.Header
import org.oxycblt.auxio.ui.recycler.Item
import org.oxycblt.auxio.util.applySpans
import org.oxycblt.auxio.util.canScroll
import org.oxycblt.auxio.util.collect
import org.oxycblt.auxio.util.collectImmediately
@ -88,9 +87,9 @@ class AlbumDetailFragment :
binding.detailRecycler.apply {
adapter = detailAdapter
applySpans { pos ->
val item = detailAdapter.data.currentList[pos]
item is Header || item is SortHeader || item is Album
setSpanSizeLookup { pos ->
val item = detailAdapter.data.getItem(pos)
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
// correctly center the playing item, so make sure that the Toolbar is lifted in
// 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.recycler.Header
import org.oxycblt.auxio.ui.recycler.Item
import org.oxycblt.auxio.util.applySpans
import org.oxycblt.auxio.util.collect
import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.context
@ -83,10 +82,9 @@ class ArtistDetailFragment :
binding.detailRecycler.apply {
adapter = detailAdapter
applySpans { pos ->
// If the item is an ActionHeader we need to also make the item full-width
val item = detailAdapter.data.currentList[pos]
item is Header || item is SortHeader || item is Artist
setSpanSizeLookup { pos ->
val item = detailAdapter.data.getItem(pos)
item is Artist || item is Header || item is SortHeader
}
}

View file

@ -29,13 +29,10 @@ import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.appbar.AppBarLayout
import java.lang.Exception
import java.lang.reflect.Field
import org.oxycblt.auxio.R
import org.oxycblt.auxio.ui.AuxioAppBarLayout
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
@ -68,14 +65,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
val toolbar = findViewById<Toolbar>(R.id.detail_toolbar)
// Reflect to get the actual title view to do transformations on
val newTitleView =
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
}
val newTitleView = TOOLBAR_TITLE_TEXT_FIELD.get(toolbar) as AppCompatTextView
newTitleView.alpha = 0f
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.recycler.Header
import org.oxycblt.auxio.ui.recycler.Item
import org.oxycblt.auxio.util.applySpans
import org.oxycblt.auxio.util.collect
import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.context
@ -84,9 +83,9 @@ class GenreDetailFragment :
binding.detailRecycler.apply {
adapter = detailAdapter
applySpans { pos ->
val item = detailAdapter.data.currentList[pos]
item is Header || item is SortHeader || item is Genre
setSpanSizeLookup { pos ->
val item = detailAdapter.data.getItem(pos)
item is Genre || item is Header || item is SortHeader
}
}

View file

@ -64,14 +64,12 @@ class SongDetailDialog : ViewBindingDialogFragment<DialogSongDetailBinding>() {
if (song != null) {
if (song.info != null) {
val context = requireContext()
binding.detailContainer.isGone = false
binding.detailFileName.setText(song.song.path.name)
binding.detailRelativeDir.setText(
song.song.path.parent.resolveName(requireContext()))
binding.detailFormat.setText(
song.info.resolvedMimeType.resolveName(requireContext()))
binding.detailSize.setText(
Formatter.formatFileSize(requireContext(), song.song.size))
binding.detailRelativeDir.setText(song.song.path.parent.resolveName(context))
binding.detailFormat.setText(song.info.resolvedMimeType.resolveName(context))
binding.detailSize.setText(Formatter.formatFileSize(context, song.song.size))
binding.detailDuration.setText(song.song.durationSecs.formatDuration(true))
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.util.context
import org.oxycblt.auxio.util.formatDuration
import org.oxycblt.auxio.util.getPluralSafe
import org.oxycblt.auxio.util.getPlural
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) }
?: 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)

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.SimpleItemCallback
import org.oxycblt.auxio.util.context
import org.oxycblt.auxio.util.getPluralSafe
import org.oxycblt.auxio.util.getPlural
import org.oxycblt.auxio.util.inflater
/**
@ -138,8 +138,8 @@ private class ArtistDetailViewHolder private constructor(private val binding: It
binding.detailInfo.text =
binding.context.getString(
R.string.fmt_two,
binding.context.getPluralSafe(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_album_count, item.albums.size),
binding.context.getPlural(R.plurals.fmt_song_count, item.songs.size))
binding.detailPlayButton.setOnClickListener { listener.onPlayParent() }
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.util.context
import org.oxycblt.auxio.util.formatDuration
import org.oxycblt.auxio.util.getPluralSafe
import org.oxycblt.auxio.util.getPlural
import org.oxycblt.auxio.util.inflater
/**
@ -106,7 +106,7 @@ private class GenreDetailViewHolder private constructor(private val binding: Ite
binding.detailCover.bind(item)
binding.detailName.text = item.resolveName(binding.context)
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.detailPlayButton.setOnClickListener { listener.onPlayParent() }
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
// using a ColorStateList in the resources
binding.homeIndexingProgress.trackColor =
requireContext().getColorStateListSafe(R.color.sel_track).defaultColor
requireContext().getColorCompat(R.color.sel_track).defaultColor
binding.homePager.apply {
adapter = HomePagerAdapter()
@ -279,6 +279,8 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
binding.homeFab.show()
binding.homeIndexingContainer.visibility = View.INVISIBLE
} else {
val context = requireContext()
binding.homeIndexingContainer.visibility = View.VISIBLE
logD("Received non-ok response $response")
@ -286,28 +288,28 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
when (response) {
is Indexer.Response.Err -> {
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 {
visibility = View.VISIBLE
text = getString(R.string.lbl_retry)
text = context.getString(R.string.lbl_retry)
setOnClickListener { musicModel.reindex() }
}
}
is Indexer.Response.NoMusic -> {
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 {
visibility = View.VISIBLE
text = getString(R.string.lbl_retry)
text = context.getString(R.string.lbl_retry)
setOnClickListener { musicModel.reindex() }
}
}
is Indexer.Response.NoPerms -> {
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 {
visibility = View.VISIBLE
text = getString(R.string.lbl_grant)
text = context.getString(R.string.lbl_grant)
setOnClickListener {
storagePermissionLauncher.launch(
Manifest.permission.READ_EXTERNAL_STORAGE)
@ -324,14 +326,16 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
binding.homeIndexingProgress.visibility = View.VISIBLE
binding.homeIndexingAction.visibility = View.INVISIBLE
val context = requireContext()
when (indexing) {
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
}
is Indexer.Indexing.Songs -> {
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 {
isIndeterminate = false
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
*/
private fun ViewPager2.reduceSensitivity(by: Int) {
try {
val recycler = VIEW_PAGER_RECYCLER_FIELD.get(this@reduceSensitivity)
val slop = VIEW_PAGER_TOUCH_SLOP_FIELD.get(recycler) as Int
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. */

View file

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

View file

@ -34,11 +34,9 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kotlin.math.abs
import org.oxycblt.auxio.R
import org.oxycblt.auxio.ui.recycler.EdgeRecyclerView
import org.oxycblt.auxio.util.canScroll
import org.oxycblt.auxio.util.getDimenOffsetSafe
import org.oxycblt.auxio.util.getDimenSizeSafe
import org.oxycblt.auxio.util.getDrawableSafe
import org.oxycblt.auxio.ui.recycler.AuxioRecyclerView
import org.oxycblt.auxio.util.getDimenSize
import org.oxycblt.auxio.util.getDrawableCompat
import org.oxycblt.auxio.util.isRtl
import org.oxycblt.auxio.util.isUnder
import org.oxycblt.auxio.util.systemBarInsetsCompat
@ -71,12 +69,12 @@ import org.oxycblt.auxio.util.systemBarInsetsCompat
class FastScrollRecyclerView
@JvmOverloads
constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) :
EdgeRecyclerView(context, attrs, defStyleAttr) {
AuxioRecyclerView(context, attrs, defStyleAttr) {
// Thumb
private val thumbView =
View(context).apply {
alpha = 0f
background = context.getDrawableSafe(R.drawable.ui_scroll_thumb)
background = context.getDrawableCompat(R.drawable.ui_scroll_thumb)
}
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)
.apply {
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
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 var downX = 0f
@ -289,7 +287,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
}
private fun updateScrollbarState() {
if (!canScroll || childCount == 0) {
if (computeVerticalScrollRange() <= height || childCount == 0) {
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.MenuItemListener
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.
@ -46,7 +45,6 @@ abstract class HomeListFragment<T : Item> :
override fun onBindingCreated(binding: FragmentHomeListBinding, savedInstanceState: Bundle?) {
binding.homeRecycler.popupProvider = this
binding.homeRecycler.listener = this
binding.homeRecycler.applySpans()
}
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.Genre
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.util.getColorStateListSafe
import org.oxycblt.auxio.util.getDrawableSafe
import org.oxycblt.auxio.util.getColorCompat
import org.oxycblt.auxio.util.getDrawableCompat
/**
* Effectively a super-charged [StyledImageView].
@ -66,7 +66,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
indicator =
StyledImageView(context).apply {
cornerRadius = this@ImageGroup.cornerRadius
staticIcon = context.getDrawableSafe(R.drawable.ic_currently_playing_24)
staticIcon = context.getDrawableCompat(R.drawable.ic_currently_playing_24)
}
addView(inner)
@ -83,7 +83,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
getChildAt(1)?.apply {
background =
MaterialShapeDrawable().apply {
fillColor = context.getColorStateListSafe(R.color.sel_cover_bg)
fillColor = context.getColorCompat(R.color.sel_cover_bg)
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.Song
import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.util.getColorStateListSafe
import org.oxycblt.auxio.util.getDrawableSafe
import org.oxycblt.auxio.util.getColorCompat
import org.oxycblt.auxio.util.getDrawableCompat
/**
* 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
background =
MaterialShapeDrawable().apply {
fillColor = context.getColorStateListSafe(R.color.sel_cover_bg)
fillColor = context.getColorCompat(R.color.sel_cover_bg)
setCornerSize(cornerRadius)
}
@ -96,7 +96,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
styledAttrs.getResourceId(
R.styleable.StyledImageView_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)
@ -126,14 +126,14 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
dispose()
load(music) {
error(StyledDrawable(context, context.getDrawableSafe(error)))
error(StyledDrawable(context, context.getDrawableCompat(error)))
transformations(SquareFrameTransform.INSTANCE)
}
}
private class StyledDrawable(context: Context, private val src: Drawable) : Drawable() {
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) {

View file

@ -32,7 +32,7 @@ import org.oxycblt.auxio.music.Directory
import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.ui.fragment.ViewBindingDialogFragment
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.showToast
@ -45,7 +45,7 @@ class MusicDirsDialog :
private val dirAdapter = MusicDirAdapter(this)
private val settings: Settings by lifecycleObject { binding -> Settings(binding.context) }
private val storageManager: StorageManager by lifecycleObject { binding ->
binding.context.getSystemServiceSafe(StorageManager::class)
binding.context.getSystemServiceCompat(StorageManager::class)
}
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.ui.system.ForegroundManager
import org.oxycblt.auxio.util.contentResolverSafe
import org.oxycblt.auxio.util.getSystemServiceSafe
import org.oxycblt.auxio.util.getSystemServiceCompat
import org.oxycblt.auxio.util.logD
/**
@ -73,7 +73,7 @@ class IndexerService : Service(), Indexer.Controller, Settings.Callback {
observingNotification = ObservingNotification(this)
wakeLock =
getSystemServiceSafe(PowerManager::class)
getSystemServiceCompat(PowerManager::class)
.newWakeLock(
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.settings.Settings
import org.oxycblt.auxio.util.contentResolverSafe
import org.oxycblt.auxio.util.getSystemServiceSafe
import org.oxycblt.auxio.util.getSystemServiceCompat
import org.oxycblt.auxio.util.logD
/*
@ -114,7 +114,7 @@ abstract class MediaStoreBackend : Indexer.Backend {
override fun query(context: Context): Cursor {
val settings = Settings(context)
val storageManager = context.getSystemServiceSafe(StorageManager::class)
val storageManager = context.getSystemServiceCompat(StorageManager::class)
volumes.addAll(storageManager.storageVolumesCompat)
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.util.androidActivityViewModels
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
@ -58,7 +58,7 @@ class PlaybackBarFragment : ViewBindingFragment<FragmentPlaybackBarBinding>() {
// Load the track color in manually as it's unclear whether the track actually supports
// using a ColorStateList in the resources
binding.playbackProgressBar.trackColor =
requireContext().getColorStateListSafe(R.color.sel_track).defaultColor
requireContext().getColorCompat(R.color.sel_track).defaultColor
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.util.androidActivityViewModels
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.systemBarInsetsCompat
@ -148,8 +148,11 @@ class PlaybackPanelFragment :
}
private fun updateParent(parent: MusicParent?) {
requireBinding().playbackToolbar.subtitle =
parent?.resolveName(requireContext()) ?: getString(R.string.lbl_all_songs)
val binding = requireBinding()
val context = requireContext()
binding.playbackToolbar.subtitle =
parent?.resolveName(context) ?: context.getString(R.string.lbl_all_songs)
}
private fun updatePosition(positionSecs: Long) {
@ -157,8 +160,6 @@ class PlaybackPanelFragment :
}
private fun updateRepeat(repeatMode: RepeatMode) {
requireBinding().playbackRepeat.apply {
isActivated = repeatMode != RepeatMode.NONE
val iconRes =
when (repeatMode) {
RepeatMode.NONE -> R.drawable.ic_repeat_off_24
@ -166,7 +167,9 @@ class PlaybackPanelFragment :
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 =
MaterialShapeDrawable.createWithElevationOverlay(binding.root.context).apply {
fillColor = binding.context.getAttrColorSafe(R.attr.colorSurface).stateList
elevation = binding.context.getDimenSafe(R.dimen.elevation_normal) * 5
fillColor = binding.context.getAttrColorCompat(R.attr.colorSurface)
elevation = binding.context.getDimen(R.dimen.elevation_normal) * 5
alpha = 0
}
@ -114,8 +114,8 @@ private constructor(
LayerDrawable(
arrayOf(
MaterialShapeDrawable.createWithElevationOverlay(binding.context).apply {
fillColor = binding.context.getAttrColorSafe(R.attr.colorSurface).stateList
elevation = binding.context.getDimenSafe(R.dimen.elevation_normal)
fillColor = binding.context.getAttrColorCompat(R.attr.colorSurface)
elevation = binding.context.getDimen(R.dimen.elevation_normal)
},
backgroundDrawable))
}

View file

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

View file

@ -33,11 +33,11 @@ import org.oxycblt.auxio.util.*
class QueueSheetBehavior<V : View>(context: Context, attributeSet: AttributeSet?) :
AuxioSheetBehavior<V>(context, attributeSet) {
private var barHeight = 0
private var barSpacing = context.getDimenSizeSafe(R.dimen.spacing_small)
private var barSpacing = context.getDimenSize(R.dimen.spacing_small)
init {
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) =

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

View file

@ -104,6 +104,8 @@ class AboutFragment : ViewBindingFragment<FragmentAboutBinding>() {
private fun openLinkInBrowser(link: String) {
logD("Opening $link")
val context = requireContext()
val browserIntent =
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
// we just do a typical activity launch.
try {
requireContext().startActivity(browserIntent)
context.startActivity(browserIntent)
} catch (e: ActivityNotFoundException) {
// 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) {
// 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
// browser.
val pkgName =
requireContext()
.packageManager
context.packageManager
.resolveActivity(browserIntent, PackageManager.MATCH_DEFAULT_ONLY)
?.run { activityInfo.packageName }
@ -144,7 +145,7 @@ class AboutFragment : ViewBindingFragment<FragmentAboutBinding>() {
}
} else {
// 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.show(parentFragmentManager, IntListPreferenceDialog.TAG)
}
is WrappedDialogPreference ->
is WrappedDialogPreference -> {
val context = requireContext()
when (preference.key) {
getString(R.string.set_key_accent) ->
context.getString(R.string.set_key_accent) ->
AccentCustomizeDialog()
.show(childFragmentManager, AccentCustomizeDialog.TAG)
getString(R.string.set_key_lib_tabs) ->
context.getString(R.string.set_key_lib_tabs) ->
TabCustomizeDialog().show(childFragmentManager, TabCustomizeDialog.TAG)
getString(R.string.set_key_pre_amp) ->
context.getString(R.string.set_key_pre_amp) ->
PreAmpCustomizeDialog()
.show(childFragmentManager, PreAmpCustomizeDialog.TAG)
getString(R.string.set_key_music_dirs) ->
context.getString(R.string.set_key_music_dirs) ->
MusicDirsDialog().show(childFragmentManager, MusicDirsDialog.TAG)
else -> logEOrThrow("Unexpected dialog key ${preference.key}")
}
}
else -> super.onDisplayPreferenceDialog(preference)
}
}
override fun onPreferenceTreeClick(preference: Preference): Boolean {
val context = requireContext()
when (preference.key) {
getString(R.string.set_key_save_state) -> {
playbackModel.savePlaybackState { context?.showToast(R.string.lng_state_saved) }
context.getString(R.string.set_key_save_state) -> {
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 ->
if (restored) {
context?.showToast(R.string.lng_state_restored)
this.context?.showToast(R.string.lng_state_restored)
} 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()
}
else -> return super.onPreferenceTreeClick(preference)
@ -139,7 +147,8 @@ class SettingsListFragment : PreferenceFragmentCompat() {
}
private fun setupPreference(preference: Preference) {
val settings = Settings(requireContext())
val context = requireActivity()
val settings = Settings(context)
if (!preference.isVisible) return
@ -151,31 +160,31 @@ class SettingsListFragment : PreferenceFragmentCompat() {
preference.apply {
when (key) {
getString(R.string.set_key_theme) -> {
context.getString(R.string.set_key_theme) -> {
onPreferenceChangeListener =
Preference.OnPreferenceChangeListener { _, value ->
AppCompatDelegate.setDefaultNightMode(value as Int)
true
}
}
getString(R.string.set_key_accent) -> {
context.getString(R.string.set_key_accent) -> {
summary = context.getString(settings.accent.name)
}
getString(R.string.set_key_black_theme) -> {
context.getString(R.string.set_key_black_theme) -> {
onPreferenceChangeListener =
Preference.OnPreferenceChangeListener { _, _ ->
if (requireContext().isNight) {
requireActivity().recreate()
if (context.isNight) {
context.recreate()
}
true
}
}
getString(R.string.set_key_show_covers),
getString(R.string.set_key_quality_covers) -> {
context.getString(R.string.set_key_show_covers),
context.getString(R.string.set_key_quality_covers) -> {
onPreferenceChangeListener =
Preference.OnPreferenceChangeListener { _, _ ->
Coil.imageLoader(requireContext()).apply { this.memoryCache?.clear() }
Coil.imageLoader(context).apply { this.memoryCache?.clear() }
true
}
}

View file

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

View file

@ -26,6 +26,7 @@ import androidx.annotation.AttrRes
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.content.res.ResourcesCompat
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
@ -47,8 +48,8 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
if (child != null) {
val coordinator = parent as CoordinatorLayout
(layoutParams as CoordinatorLayout.LayoutParams)
.behavior?.onNestedPreScroll(coordinator, this, coordinator, 0, 0, tConsumed, 0)
coordinatorLayoutBehavior?.onNestedPreScroll(
coordinator, this, coordinator, 0, 0, tConsumed, 0)
}
true

View file

@ -40,8 +40,8 @@ abstract class AuxioSheetBehavior<V : View>(context: Context, attributeSet: Attr
private var setup = false
val sheetBackgroundDrawable =
MaterialShapeDrawable.createWithElevationOverlay(context).apply {
fillColor = context.getAttrColorSafe(R.attr.colorSurface).stateList
elevation = context.getDimenSafe(R.dimen.elevation_normal)
fillColor = context.getAttrColorCompat(R.attr.colorSurface)
elevation = context.getDimen(R.dimen.elevation_normal)
}
init {
@ -75,7 +75,8 @@ abstract class AuxioSheetBehavior<V : View>(context: Context, attributeSet: Attr
background =
LayerDrawable(
arrayOf(
ColorDrawable(context.getAttrColorSafe(R.attr.colorSurface)),
ColorDrawable(
context.getAttrColorCompat(R.attr.colorSurface).defaultColor),
sheetBackgroundDrawable))
// 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) {
private var dep: View? = null
private var lastInsets: WindowInsets? = null
private var lastConsumed: Int? = null
private var setup: Boolean = false
private var lastConsumed = -1
private var setup = false
override fun layoutDependsOn(parent: CoordinatorLayout, child: V, dependency: View): Boolean {
if (dependency.coordinatorLayoutBehavior is NeoBottomSheetBehavior) {
@ -56,7 +56,7 @@ class BottomSheetContentBehavior<V : View>(context: Context, attributeSet: Attri
): Boolean {
val behavior = dependency.coordinatorLayoutBehavior as NeoBottomSheetBehavior
val consumed = behavior.calculateConsumedByBar()
if (consumed < Int.MIN_VALUE) {
if (consumed == Int.MIN_VALUE) {
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> {
// TODO: Use Collator for sorting?
override fun compare(a: T, b: T): Int {
val aSortName = a.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.BindingViewHolder
import org.oxycblt.auxio.ui.recycler.MonoAdapter
import org.oxycblt.auxio.util.getAttrColorSafe
import org.oxycblt.auxio.util.getColorSafe
import org.oxycblt.auxio.util.getAttrColorCompat
import org.oxycblt.auxio.util.getColorCompat
import org.oxycblt.auxio.util.inflater
import org.oxycblt.auxio.util.stateList
/**
* An adapter that displays the accent palette.
@ -82,7 +81,7 @@ class AccentViewHolder private constructor(private val binding: ItemAccentBindin
setSelected(false)
binding.accent.apply {
backgroundTintList = context.getColorSafe(item.primary).stateList
backgroundTintList = context.getColorCompat(item.primary)
contentDescription = context.getString(item.name)
TooltipCompat.setTooltipText(this, contentDescription)
setOnClickListener { listener.onAccentSelected(item) }
@ -94,9 +93,9 @@ class AccentViewHolder private constructor(private val binding: ItemAccentBindin
isEnabled = !isSelected
iconTint =
if (isSelected) {
context.getAttrColorSafe(R.attr.colorSurface).stateList
context.getAttrColorCompat(R.attr.colorSurface)
} 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.RecyclerView
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
@ -35,9 +36,9 @@ class AccentGridLayoutManager(
defStyleAttr: Int,
defStyleRes: Int
) : 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.
private var columnWidth = context.pxOfDp(56f)
private var columnWidth = context.getDimenSize(R.dimen.size_accent_item)
private var lastWidth = -1
private var lastHeight = -1
@ -45,9 +46,7 @@ class AccentGridLayoutManager(
override fun onLayoutChildren(recycler: RecyclerView.Recycler?, state: RecyclerView.State?) {
if (width > 0 && height > 0 && (lastWidth != width || lastHeight != height)) {
val totalSpace = width - paddingRight - paddingLeft
val spanCount = max(1, totalSpace / columnWidth)
setSpanCount(spanCount)
spanCount = max(1, totalSpace / columnWidth)
}
lastWidth = width

View file

@ -23,11 +23,12 @@ import android.util.AttributeSet
import android.view.WindowInsets
import androidx.annotation.AttrRes
import androidx.core.view.updatePadding
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.util.systemBarInsetsCompat
/** A [RecyclerView] that automatically applies insets to itself. */
open class EdgeRecyclerView
open class AuxioRecyclerView
@JvmOverloads
constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) :
RecyclerView(context, attrs, defStyleAttr) {
@ -52,4 +53,13 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
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 com.google.android.material.divider.MaterialDivider
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
* used in dialogs.
* @author OxygenCobalt
*/
class ScrollIndicatorRecyclerView
class DialogRecyclerView
@JvmOverloads
constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) :
RecyclerView(context, attrs, defStyleAttr) {
private val topDivider = 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 {
updatePadding(top = spacingMedium)

View file

@ -26,7 +26,7 @@ import androidx.recyclerview.widget.AsyncListDiffer
import androidx.recyclerview.widget.DiffUtil
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

View file

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

View file

@ -23,7 +23,7 @@ import android.content.Context
import android.os.Build
import androidx.annotation.StringRes
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
@ -32,7 +32,7 @@ import org.oxycblt.auxio.util.getSystemServiceSafe
*/
abstract class ServiceNotification(context: Context, info: ChannelInfo) :
NotificationCompat.Builder(context, info.id) {
private val notificationManager = context.getSystemServiceSafe(NotificationManager::class)
private val notificationManager = context.getSystemServiceCompat(NotificationManager::class)
init {
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.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
@ -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
* as well.
*/
val Context.isNight: Boolean
val Context.isNight
get() =
resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK ==
Configuration.UI_MODE_NIGHT_YES
@ -73,48 +71,25 @@ val Context.contentResolverSafe: ContentResolver
* @param value Int value for the plural.
* @return The formatted string requested
*/
fun Context.getPluralSafe(@PluralsRes pluralsRes: Int, value: Int): String {
return try {
fun Context.getPlural(@PluralsRes pluralsRes: Int, value: Int) =
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.
* @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 {
return try {
unlikelyToBeNull(ContextCompat.getColorStateList(this, color))
} catch (e: Exception) {
handleResourceFailure(e, "color state list", getColorSafe(android.R.color.black).stateList)
fun Context.getColorCompat(@ColorRes color: Int) =
requireNotNull(ContextCompat.getColorStateList(this, color)) {
"Invalid resource: State list was null"
}
}
/**
* Convenience method for getting a color attribute safely.
* @param attr The color attribute
* @return The attribute requested, or black if an error occurred.
* @return The attribute requested
*/
@ColorInt
fun Context.getAttrColorSafe(@AttrRes attr: Int): Int {
fun Context.getAttrColorCompat(@AttrRes attr: Int): ColorStateList {
// First resolve the attribute into its ID
val resolvedAttr = TypedValue()
theme.resolveAttribute(attr, resolvedAttr, true)
@ -127,80 +102,32 @@ fun Context.getAttrColorSafe(@AttrRes attr: Int): Int {
resolvedAttr.data
}
return getColorSafe(color)
return getColorCompat(color)
}
/**
* Convenience method for getting a [Drawable] safely.
* @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 {
return try {
requireNotNull(ContextCompat.getDrawable(this, drawable))
} catch (e: Exception) {
handleResourceFailure(e, "drawable", ColorDrawable(getColorSafe(android.R.color.black)))
fun Context.getDrawableCompat(@DrawableRes drawable: Int) =
requireNotNull(ContextCompat.getDrawable(this, drawable)) {
"Invalid resource: Drawable was null"
}
}
/**
* Convenience method for getting a dimension safely.
* @param dimen The dimension resource
* @return The dimension requested, or 0 if an error occurred.
* @return The dimension requested
*/
@Dimension
fun Context.getDimenSafe(@DimenRes dimen: Int): Float {
return try {
resources.getDimension(dimen)
} catch (e: Exception) {
handleResourceFailure(e, "dimen", 0f)
}
}
@Dimension fun Context.getDimen(@DimenRes dimen: Int) = resources.getDimension(dimen)
/**
* 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.
* @return The dimension requested, in pixels
*/
@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)
}
}
/**
* 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
}
@Px fun Context.getDimenSize(@DimenRes dimen: Int) = resources.getDimensionPixelSize(dimen)
/**
* 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
* @throws IllegalArgumentException If the system service cannot be retrieved.
*/
fun <T : Any> Context.getSystemServiceSafe(serviceClass: KClass<T>): T {
return requireNotNull(ContextCompat.getSystemService(this, serviceClass.java)) {
fun <T : Any> Context.getSystemServiceCompat(serviceClass: KClass<T>) =
requireNotNull(ContextCompat.getSystemService(this, serviceClass.java)) {
"System service ${serviceClass.simpleName} could not be instantiated"
}
}
/** Create a toast using the provided string resource. */
fun Context.showToast(@StringRes str: Int) {

View file

@ -19,7 +19,6 @@ package org.oxycblt.auxio.util
import android.app.Application
import android.content.Context
import android.content.res.ColorStateList
import android.database.Cursor
import android.database.sqlite.SQLiteDatabase
import android.graphics.drawable.Drawable
@ -27,7 +26,6 @@ import android.os.Build
import android.view.View
import android.view.WindowInsets
import androidx.activity.viewModels
import androidx.annotation.ColorRes
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import androidx.coordinatorlayout.widget.CoordinatorLayout
@ -41,7 +39,6 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding
import kotlinx.coroutines.CoroutineScope
@ -56,7 +53,7 @@ import org.oxycblt.auxio.R
*/
fun View.disableDropShadowCompat() {
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
outlineSpotShadowColor = transparent
}
@ -113,42 +110,16 @@ val Drawable.isRtl: Boolean
val ViewBinding.context: 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. */
val RecyclerView.canScroll: Boolean
get() = computeVerticalScrollRange() > height
fun RecyclerView.canScroll() = 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
/** Converts this color to a single-color [ColorStateList]. */
val @receiver:ColorRes Int.stateList
get() = ColorStateList.valueOf(this)
/**
* 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.RepeatMode
import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.util.getDimenSizeSafe
import org.oxycblt.auxio.util.getDimenSize
import org.oxycblt.auxio.util.logD
/**
@ -85,10 +85,10 @@ class WidgetComponent(private val context: Context) :
val cornerRadius =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
// 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) {
// < Android 12, but the user still enabled round mode.
context.getDimenSizeSafe(R.dimen.size_corners_medium)
context.getDimenSize(R.dimen.size_corners_medium)
} else {
// User did not enable round mode.
0

View file

@ -1,5 +1,5 @@
<?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:tools="http://schemas.android.com/tools"
android:id="@+id/accent_recycler"

View file

@ -1,5 +1,5 @@
<?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:tools="http://schemas.android.com/tools"
android:id="@+id/tab_recycler"

View file

@ -20,11 +20,12 @@
</org.oxycblt.auxio.detail.DetailAppBarLayout>
<org.oxycblt.auxio.ui.recycler.EdgeRecyclerView
<org.oxycblt.auxio.ui.recycler.AuxioRecyclerView
android:id="@+id/detail_recycler"
android:layout_width="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"
tools:listitem="@layout/item_detail" />

View file

@ -6,6 +6,7 @@
style="@style/Widget.Auxio.RecyclerView.WithAdaptiveFab"
android:layout_width="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"
tools:listitem="@layout/item_parent" />

View file

@ -6,7 +6,7 @@
android:layout_width="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:layout_width="match_parent"
android:layout_height="match_parent"

View file

@ -45,11 +45,12 @@
</org.oxycblt.auxio.ui.AuxioAppBarLayout>
<org.oxycblt.auxio.ui.recycler.EdgeRecyclerView
<org.oxycblt.auxio.ui.recycler.AuxioRecyclerView
android:id="@+id/search_recycler"
android:layout_width="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"
tools:listitem="@layout/item_song" />

View file

@ -21,6 +21,7 @@
<dimen name="size_corners_mid_large">24dp</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_play_pause_button">72dp</dimen>