util: redocument

Redocument the util module.

This should make the purpose of the utilities used in this app clearer.
This commit is contained in:
Alexander Capehart 2022-12-22 20:19:59 -07:00
parent e92b69e399
commit 7415c28e2d
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
27 changed files with 346 additions and 210 deletions

View file

@ -20,5 +20,6 @@
# hide the original source file name.
#-renamesourcefileattribute SourceFile
# Free software does not obsfucate. Also it's easier to debug stack traces.
# Obsfucation is what proprietary software does to keep the user unaware of it's abuses.
# Also it's easier to debug if the class names remain unmangled.
-dontobfuscate

View file

@ -33,15 +33,14 @@ import org.oxycblt.auxio.image.extractor.MusicKeyer
import org.oxycblt.auxio.settings.Settings
/**
* Auxio.
* Auxio: A simple, rational music player for android.
* @author Alexander Capehart (OxygenCobalt)
*/
class AuxioApp : Application(), ImageLoaderFactory {
override fun onCreate() {
super.onCreate()
// Migrate any settings that may have changed in an app update.
Settings(this).migrate()
// Adding static shortcuts in a dynamic manner is better than declaring them
// manually, as it will properly handle the difference between debug and release
// Auxio instances.
@ -75,7 +74,14 @@ class AuxioApp : Application(), ImageLoaderFactory {
.build()
companion object {
/**
* The ID of the "Shuffle All" shortcut.
*/
const val SHORTCUT_SHUFFLE_ID = "shortcut_shuffle"
/**
* The [Intent] name for the "Shuffle All" shortcut.
*/
const val INTENT_KEY_SHORTCUT_SHUFFLE = BuildConfig.APPLICATION_ID + ".action.SHUFFLE_ALL"
}
}

View file

@ -56,7 +56,6 @@ class MainFragment :
private val navModel: NavigationViewModel by activityViewModels()
private val callback = DynamicBackPressedCallback()
private var lastInsets: WindowInsets? = null
private val elevationNormal: Float by lifecycleObject { binding ->
binding.context.getDimen(R.dimen.elevation_normal)
}
@ -72,7 +71,8 @@ class MainFragment :
override fun onBindingCreated(binding: FragmentMainBinding, savedInstanceState: Bundle?) {
// --- UI SETUP ---
val context = requireActivity()
// Override the back pressed callback so we can map back navigation to collapsing
// navigation, navigation out of detail views, etc.
context.onBackPressedDispatcher.addCallback(viewLifecycleOwner, callback)
binding.root.setOnApplyWindowInsetsListener { _, insets ->
@ -89,24 +89,26 @@ class MainFragment :
val queueSheetBehavior =
binding.queueSheet.coordinatorLayoutBehavior as QueueBottomSheetBehavior?
if (queueSheetBehavior != null) {
// Bottom sheet mode, set up click listeners.
val playbackSheetBehavior =
binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior
unlikelyToBeNull(binding.handleWrapper).setOnClickListener {
if (playbackSheetBehavior.state == NeoBottomSheetBehavior.STATE_EXPANDED &&
queueSheetBehavior.state == NeoBottomSheetBehavior.STATE_COLLAPSED) {
// Playback sheet is expanded and queue sheet is collapsed, we can expand it.
queueSheetBehavior.state = NeoBottomSheetBehavior.STATE_EXPANDED
}
}
} else {
// Dual-pane mode, color/pad the queue sheet manually.
// Dual-pane mode, manually style the static queue sheet.
binding.queueSheet.apply {
// Emulate the elevated bottom sheet style.
background =
MaterialShapeDrawable.createWithElevationOverlay(context).apply {
fillColor = context.getAttrColorCompat(R.attr.colorSurface)
elevation = context.getDimen(R.dimen.elevation_normal)
}
// Apply bar insets for the queue's RecyclerView to usee.
setOnApplyWindowInsetsListener { v, insets ->
v.updatePadding(top = insets.systemBarInsetsCompat.top)
insets
@ -115,7 +117,6 @@ class MainFragment :
}
// --- VIEWMODEL SETUP ---
collect(navModel.mainNavigationAction, ::handleMainNavigation)
collect(navModel.exploreNavigationItem, ::handleExploreNavigation)
collect(navModel.exploreNavigationArtists, ::handleExplorePicker)
@ -125,7 +126,6 @@ class MainFragment :
override fun onStart() {
super.onStart()
// Callback could still reasonably fire even if we clear the binding, attach/detach
// our pre-draw listener our listener in onStart/onStop respectively.
requireBinding().playbackSheet.viewTreeObserver.addOnPreDrawListener(this)
@ -139,33 +139,33 @@ class MainFragment :
override fun onPreDraw(): Boolean {
// We overload CoordinatorLayout far too much to rely on any of it's typical
// callback functionality. Just update all transitions before every draw. Should
// probably be cheap *enough.*
// probably be cheap enough.
val binding = requireBinding()
val playbackSheetBehavior =
binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior
val queueSheetBehavior =
binding.queueSheet.coordinatorLayoutBehavior as QueueBottomSheetBehavior?
val playbackRatio = max(playbackSheetBehavior.calculateSlideOffset(), 0f)
val outPlaybackRatio = 1 - playbackRatio
val halfOutRatio = min(playbackRatio * 2, 1f)
val halfInPlaybackRatio = max(playbackRatio - 0.5f, 0f) * 2
val queueSheetBehavior =
binding.queueSheet.coordinatorLayoutBehavior as QueueBottomSheetBehavior?
if (queueSheetBehavior != null) {
// Queue sheet, take queue into account so the playback bar is shown and the playback
// panel is hidden when the queue sheet is expanded.
// Queue sheet available, the normal transition applies, but it now much be combined
// with another transition where the playback panel disappears and the playback bar
// appears as the queue sheet expands.
val queueRatio = max(queueSheetBehavior.calculateSlideOffset(), 0f)
val halfOutQueueRatio = min(queueRatio * 2, 1f)
val halfInQueueRatio = max(queueRatio - 0.5f, 0f) * 2
binding.playbackBarFragment.alpha = max(1 - halfOutRatio, halfInQueueRatio)
binding.playbackPanelFragment.alpha = min(halfInPlaybackRatio, 1 - halfOutQueueRatio)
binding.queueFragment.alpha = queueRatio
if (playbackModel.song.value != null) {
// Hack around the playback sheet intercepting swipe events on the queue bar
// Playback sheet intercepts queue sheet touch events, prevent that from
// occurring by disabling dragging whenever the queue sheet is expanded.
playbackSheetBehavior.isDraggable =
queueSheetBehavior.state == NeoBottomSheetBehavior.STATE_COLLAPSED
}
@ -175,26 +175,42 @@ class MainFragment :
binding.playbackPanelFragment.alpha = halfInPlaybackRatio
}
// Fade out the content as the playback panel expands.
// TODO: Replace with shadow?
binding.exploreNavHost.apply {
alpha = outPlaybackRatio
// Prevent interactions when the content fully fades out.
isInvisible = alpha == 0f
}
// Reduce playback sheet elevation as it expands. This involves both updating the
// shadow elevation for older versions, and fading out the background drawable
// containing the elevation overlay.
binding.playbackSheet.translationZ = elevationNormal * outPlaybackRatio
playbackSheetBehavior.sheetBackgroundDrawable.alpha = (outPlaybackRatio * 255).toInt()
// Fade out the playback bar as the panel expands.
binding.playbackBarFragment.apply {
// Prevent interactions when the playback bar fully fades out.
isInvisible = alpha == 0f
// As the playback bar expands, we also want to subtly translate the bar to
// align with the top inset. This results in both a smooth transition from the bar
// to the playback panel's toolbar, but also a correctly positioned playback bar
// for when the queue sheet expands.
lastInsets?.let { translationY = it.systemBarInsetsCompat.top * halfOutRatio }
}
// Prevent interactions when the playback panell fully fades out.
binding.playbackPanelFragment.isInvisible = binding.playbackPanelFragment.alpha == 0f
binding.queueSheet.apply {
// Queue sheet (not queue content) should fade out with the playback panel.
alpha = halfInPlaybackRatio
// Prevent interactions when the queue sheet fully fades out.
binding.queueSheet.isInvisible = alpha == 0f
}
// Prevent interactions when the queue content fully fades out.
binding.queueFragment.isInvisible = binding.queueFragment.alpha == 0f
if (playbackModel.song.value == null) {
@ -216,8 +232,7 @@ class MainFragment :
when (action) {
is MainNavigationAction.Expand -> tryExpandAll()
is MainNavigationAction.Collapse -> tryCollapseAll()
// No need to reset selection despite navigating to another screen, as the
// main fragment destinations don't have selections
// TODO: Figure out how to clear out the selections as one moves between screens.
is MainNavigationAction.Directions -> findNavController().navigate(action.directions)
}

View file

@ -32,6 +32,7 @@ import com.google.android.material.appbar.AppBarLayout
import java.lang.reflect.Field
import org.oxycblt.auxio.R
import org.oxycblt.auxio.shared.AuxioAppBarLayout
import org.oxycblt.auxio.util.getInteger
import org.oxycblt.auxio.util.lazyReflectedField
/**
@ -131,9 +132,9 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
addUpdateListener { titleView.alpha = it.animatedValue as Float }
duration =
if (titleShown == true) {
context.resources.getInteger(R.integer.anim_fade_enter_duration).toLong()
context.getInteger(R.integer.anim_fade_enter_duration).toLong()
} else {
context.resources.getInteger(R.integer.anim_fade_exit_duration).toLong()
context.getInteger(R.integer.anim_fade_exit_duration).toLong()
}
start()
}

View file

@ -295,7 +295,7 @@ class DetailViewModel(application: Application) :
val extractor = MediaExtractor()
try {
extractor.setDataSource(application, song.uri, emptyMap())
extractor.setDataSource(context, song.uri, emptyMap())
} catch (e: Exception) {
// Can feasibly fail with invalid file formats. Note that this isn't considered
// an error condition in the UI, as there is still plenty of other song information

View file

@ -31,7 +31,7 @@ import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.Sort
import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.util.application
import org.oxycblt.auxio.util.context
import org.oxycblt.auxio.util.logD
/**
@ -131,13 +131,13 @@ class HomeViewModel(application: Application) :
override fun onSettingChanged(key: String) {
when (key) {
application.getString(R.string.set_key_lib_tabs) -> {
context.getString(R.string.set_key_lib_tabs) -> {
// Tabs changed, update the current tabs and set up a re-create event.
currentTabModes = getVisibleTabModes()
_shouldRecreate.value = true
}
application.getString(R.string.set_key_hide_collaborators) -> {
context.getString(R.string.set_key_hide_collaborators) -> {
// Changes in the hide collaborator setting will change the artist contents
// of the library, consider it a library update.
onLibraryChanged(musicStore.library)

View file

@ -35,7 +35,7 @@ import androidx.core.widget.TextViewCompat
import com.google.android.material.textview.MaterialTextView
import org.oxycblt.auxio.R
import org.oxycblt.auxio.util.getAttrColorCompat
import org.oxycblt.auxio.util.getDimenSize
import org.oxycblt.auxio.util.getDimenPixels
import org.oxycblt.auxio.util.isRtl
/**
@ -47,8 +47,8 @@ class FastScrollPopupView
constructor(context: Context, attrs: AttributeSet? = null, defStyleRes: Int = 0) :
MaterialTextView(context, attrs, defStyleRes) {
init {
minimumWidth = context.getDimenSize(R.dimen.fast_scroll_popup_min_width)
minimumHeight = context.getDimenSize(R.dimen.fast_scroll_popup_min_height)
minimumWidth = context.getDimenPixels(R.dimen.fast_scroll_popup_min_width)
minimumHeight = context.getDimenPixels(R.dimen.fast_scroll_popup_min_height)
TextViewCompat.setTextAppearance(this, R.style.TextAppearance_Auxio_HeadlineLarge)
setTextColor(context.getAttrColorCompat(R.attr.colorOnSecondary))
@ -57,7 +57,7 @@ constructor(context: Context, attrs: AttributeSet? = null, defStyleRes: Int = 0)
includeFontPadding = false
alpha = 0f
elevation = context.getDimenSize(R.dimen.elevation_normal).toFloat()
elevation = context.getDimenPixels(R.dimen.elevation_normal).toFloat()
background = FastScrollPopupDrawable(context)
}
@ -72,8 +72,8 @@ constructor(context: Context, attrs: AttributeSet? = null, defStyleRes: Int = 0)
private val path = Path()
private val matrix = Matrix()
private val paddingStart = context.getDimenSize(R.dimen.fast_scroll_popup_padding_start)
private val paddingEnd = context.getDimenSize(R.dimen.fast_scroll_popup_padding_end)
private val paddingStart = context.getDimenPixels(R.dimen.fast_scroll_popup_padding_start)
private val paddingEnd = context.getDimenPixels(R.dimen.fast_scroll_popup_padding_end)
override fun draw(canvas: Canvas) {
canvas.drawPath(path, paint)

View file

@ -19,6 +19,7 @@ package org.oxycblt.auxio.home.fastscroll
import android.content.Context
import android.graphics.Canvas
import android.graphics.PointF
import android.graphics.Rect
import android.util.AttributeSet
import android.view.Gravity
@ -36,11 +37,7 @@ import androidx.recyclerview.widget.RecyclerView
import kotlin.math.abs
import org.oxycblt.auxio.R
import org.oxycblt.auxio.list.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
import org.oxycblt.auxio.util.*
/**
* A [RecyclerView] that enables better fast-scrolling. This is fundamentally a implementation of
@ -126,7 +123,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.getDimenSize(R.dimen.spacing_small)
marginEnd = context.getDimenPixels(R.dimen.spacing_small)
}
}
@ -134,7 +131,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
// Touch
private val minTouchTargetSize =
context.getDimenSize(R.dimen.fast_scroll_thumb_touch_target_size)
context.getDimenPixels(R.dimen.fast_scroll_thumb_touch_target_size)
private val touchSlop = ViewConfiguration.get(context).scaledTouchSlop
private var downX = 0f
@ -474,7 +471,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
view
.animate()
.alpha(1f)
.setDuration(context.resources.getInteger(R.integer.anim_fade_enter_duration).toLong())
.setDuration(context.getInteger(R.integer.anim_fade_enter_duration).toLong())
.start()
}
@ -482,7 +479,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
view
.animate()
.alpha(0f)
.setDuration(context.resources.getInteger(R.integer.anim_fade_exit_duration).toLong())
.setDuration(context.getInteger(R.integer.anim_fade_exit_duration).toLong())
.start()
}

View file

@ -35,7 +35,8 @@ import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.util.getAttrColorCompat
import org.oxycblt.auxio.util.getColorCompat
import org.oxycblt.auxio.util.getDimenSize
import org.oxycblt.auxio.util.getDimenPixels
import org.oxycblt.auxio.util.getInteger
/**
* A super-charged [StyledImageView]. This class enables the following features in addition
@ -114,7 +115,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
// Override the layout params of the indicator so that it's in the
// bottom left corner.
gravity = Gravity.BOTTOM or Gravity.END
val spacing = context.getDimenSize(R.dimen.spacing_tiny)
val spacing = context.getDimenPixels(R.dimen.spacing_tiny)
updateMarginsRelative(bottom = spacing, end = spacing)
})
}
@ -225,12 +226,12 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
// Activated -> Show selection indicator
targetAlpha = 1f
targetDuration =
context.resources.getInteger(R.integer.anim_fade_enter_duration).toLong()
context.getInteger(R.integer.anim_fade_enter_duration).toLong()
} else {
// Activated -> Hide selection indicator.
targetAlpha = 0f
targetDuration =
context.resources.getInteger(R.integer.anim_fade_exit_duration).toLong()
context.getInteger(R.integer.anim_fade_exit_duration).toLong()
}
if (selectionIndicatorView.alpha == targetAlpha) {

View file

@ -28,7 +28,7 @@ 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.getDimenSize
import org.oxycblt.auxio.util.getDimenPixels
/**
* A [RecyclerView] intended for use in Dialogs, adding features such as:
@ -55,7 +55,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
private val topDivider = MaterialDivider(context)
private val bottomDivider = MaterialDivider(context)
private val spacingMedium = context.getDimenSize(R.dimen.spacing_medium)
private val spacingMedium = context.getDimenPixels(R.dimen.spacing_medium)
init {
// Apply top padding to give enough room to the dialog title, assuming that this view

View file

@ -26,6 +26,7 @@ import androidx.appcompat.widget.Toolbar.OnMenuItemClickListener
import androidx.core.view.isInvisible
import com.google.android.material.appbar.MaterialToolbar
import org.oxycblt.auxio.R
import org.oxycblt.auxio.util.getInteger
import org.oxycblt.auxio.util.logD
/**
@ -116,12 +117,12 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
targetInnerAlpha = 0f
targetSelectionAlpha = 1f
targetDuration =
context.resources.getInteger(R.integer.anim_fade_enter_duration).toLong()
context.getInteger(R.integer.anim_fade_enter_duration).toLong()
} else {
targetInnerAlpha = 1f
targetSelectionAlpha = 0f
targetDuration =
context.resources.getInteger(R.integer.anim_fade_exit_duration).toLong()
context.getInteger(R.integer.anim_fade_exit_duration).toLong()
}
if (innerToolbar.alpha == targetInnerAlpha &&

View file

@ -23,7 +23,7 @@ import android.provider.OpenableColumns
import org.oxycblt.auxio.music.MusicStore.Callback
import org.oxycblt.auxio.music.MusicStore.Library
import org.oxycblt.auxio.music.storage.useQuery
import org.oxycblt.auxio.util.contentResolverSafe
import org.oxycblt.auxio.music.storage.contentResolverSafe
/**
* A repository granting access to the music library..

View file

@ -35,7 +35,7 @@ import org.oxycblt.auxio.music.storage.safeQuery
import org.oxycblt.auxio.music.storage.storageVolumesCompat
import org.oxycblt.auxio.music.storage.useQuery
import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.util.contentResolverSafe
import org.oxycblt.auxio.music.storage.contentResolverSafe
import org.oxycblt.auxio.util.getSystemServiceCompat
import org.oxycblt.auxio.util.logD

View file

@ -33,6 +33,13 @@ import org.oxycblt.auxio.util.lazyReflectedMethod
// --- MEDIASTORE UTILITIES ---
/**
* Get a content resolver that will not mangle MediaStore queries on certain devices.
* See https://github.com/OxygenCobalt/Auxio/issues/50 for more info.
*/
val Context.contentResolverSafe: ContentResolver
get() = applicationContext.contentResolver
/**
* A shortcut for querying the [ContentResolver] database.
* @param uri The [Uri] of content to retrieve.

View file

@ -33,10 +33,10 @@ import kotlinx.coroutines.launch
import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.R
import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.music.storage.contentResolverSafe
import org.oxycblt.auxio.playback.state.PlaybackStateManager
import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.shared.ForegroundManager
import org.oxycblt.auxio.util.contentResolverSafe
import org.oxycblt.auxio.util.getSystemServiceCompat
import org.oxycblt.auxio.util.logD

View file

@ -31,7 +31,7 @@ import org.oxycblt.auxio.playback.state.PlaybackStateDatabase
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.application
import org.oxycblt.auxio.util.context
/**
* The ViewModel that provides a UI frontend for [PlaybackStateManager].
@ -281,7 +281,7 @@ class PlaybackViewModel(application: Application) :
*/
fun savePlaybackState(onDone: (Boolean) -> Unit) {
viewModelScope.launch {
val saved = playbackManager.saveState(PlaybackStateDatabase.getInstance(application))
val saved = playbackManager.saveState(PlaybackStateDatabase.getInstance(context))
onDone(saved)
}
}
@ -292,7 +292,7 @@ class PlaybackViewModel(application: Application) :
*/
fun wipePlaybackState(onDone: (Boolean) -> Unit) {
viewModelScope.launch {
val wiped = playbackManager.wipeState(PlaybackStateDatabase.getInstance(application))
val wiped = playbackManager.wipeState(PlaybackStateDatabase.getInstance(context))
onDone(wiped)
}
}
@ -305,7 +305,7 @@ class PlaybackViewModel(application: Application) :
fun tryRestorePlaybackState(onDone: (Boolean) -> Unit) {
viewModelScope.launch {
val restored =
playbackManager.restoreState(PlaybackStateDatabase.getInstance(application), true)
playbackManager.restoreState(PlaybackStateDatabase.getInstance(context), true)
onDone(restored)
}
}

View file

@ -27,7 +27,7 @@ import org.oxycblt.auxio.R
import org.oxycblt.auxio.shared.AuxioBottomSheetBehavior
import org.oxycblt.auxio.util.getAttrColorCompat
import org.oxycblt.auxio.util.getDimen
import org.oxycblt.auxio.util.getDimenSize
import org.oxycblt.auxio.util.getDimenPixels
import org.oxycblt.auxio.util.replaceSystemBarInsetsCompat
import org.oxycblt.auxio.util.systemBarInsetsCompat
@ -38,7 +38,7 @@ import org.oxycblt.auxio.util.systemBarInsetsCompat
class QueueBottomSheetBehavior<V : View>(context: Context, attributeSet: AttributeSet?) :
AuxioBottomSheetBehavior<V>(context, attributeSet) {
private var barHeight = 0
private var barSpacing = context.getDimenSize(R.dimen.spacing_small)
private var barSpacing = context.getDimenPixels(R.dimen.spacing_small)
init {
isHideable = false

View file

@ -22,6 +22,7 @@ import android.content.Context
import android.util.AttributeSet
import com.google.android.material.button.MaterialButton
import org.oxycblt.auxio.R
import org.oxycblt.auxio.util.getInteger
/**
* A [MaterialButton] that automatically morphs from a circle to a squircle shape appearance when it
@ -47,7 +48,7 @@ constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
animator?.cancel()
animator =
ValueAnimator.ofFloat(currentCornerRadiusRatio, target).apply {
duration = context.resources.getInteger(R.integer.anim_fade_enter_duration).toLong()
duration = context.getInteger(R.integer.anim_fade_enter_duration).toLong()
addUpdateListener { updateCornerRadiusRatio(animatedValue as Float) }
start()
}

View file

@ -41,7 +41,7 @@ import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.Sort
import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.util.application
import org.oxycblt.auxio.util.context
import org.oxycblt.auxio.util.logD
/**
@ -168,7 +168,7 @@ class SearchViewModel(application: Application) :
// would just want to leverage CollationKey, but that is not designed for a contains
// algorithm. If that fails, filter impls have fallback values, primarily around
// sort tags or file names.
it.resolveNameNormalized(application).contains(value, ignoreCase = true) ||
it.resolveNameNormalized(context).contains(value, ignoreCase = true) ||
fallback(it)
}
.ifEmpty { null }

View file

@ -72,19 +72,14 @@ class AboutFragment : ViewBindingFragment<FragmentAboutBinding>() {
}
private fun updateStatistics(statistics: MusicViewModel.Statistics?) {
val binding = requireBinding()
binding.aboutSongCount.text = getString(R.string.fmt_lib_song_count, statistics?.songs ?: 0)
requireBinding().aboutAlbumCount.text =
getString(R.string.fmt_lib_album_count, statistics?.albums ?: 0)
requireBinding().aboutArtistCount.text =
getString(R.string.fmt_lib_artist_count, statistics?.artists ?: 0)
requireBinding().aboutGenreCount.text =
getString(R.string.fmt_lib_genre_count, statistics?.genres ?: 0)
binding.aboutTotalDuration.text =
getString(
R.string.fmt_lib_total_duration,

View file

@ -23,7 +23,7 @@ import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kotlin.math.max
import org.oxycblt.auxio.R
import org.oxycblt.auxio.util.getDimenSize
import org.oxycblt.auxio.util.getDimenPixels
/**
* A sub-class of [GridLayoutManager] that automatically sets the spans so that they fit the width
@ -38,7 +38,7 @@ class AccentGridLayoutManager(
) : GridLayoutManager(context, attrs, defStyleAttr, defStyleRes) {
// 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.getDimenSize(R.dimen.size_accent_item)
private var columnWidth = context.getDimenPixels(R.dimen.size_accent_item)
private var lastWidth = -1
private var lastHeight = -1

View file

@ -28,6 +28,7 @@ import androidx.preference.Preference
import androidx.preference.PreferenceViewHolder
import java.lang.reflect.Field
import org.oxycblt.auxio.R
import org.oxycblt.auxio.util.getInteger
import org.oxycblt.auxio.util.lazyReflectedField
/**
@ -71,7 +72,7 @@ constructor(
// Additional values: offValue defines an "off" position
val offValueId = prefAttrs.getResourceId(R.styleable.IntListPreference_offValue, -1)
if (offValueId > -1) {
offValue = context.resources.getInteger(offValueId)
offValue = context.getInteger(offValueId)
}
val iconsId = prefAttrs.getResourceId(R.styleable.IntListPreference_entryIcons, -1)

View file

@ -18,12 +18,10 @@
package org.oxycblt.auxio.util
import android.app.PendingIntent
import android.content.ContentResolver
import android.content.Context
import android.content.Intent
import android.content.res.ColorStateList
import android.content.res.Configuration
import android.graphics.drawable.Drawable
import android.os.Build
import android.util.TypedValue
import android.view.LayoutInflater
@ -42,65 +40,62 @@ import kotlin.reflect.KClass
import org.oxycblt.auxio.IntegerTable
import org.oxycblt.auxio.MainActivity
/** Shortcut to get a [LayoutInflater] from a [Context] */
/**
* Get a [LayoutInflater] instance from this [Context].
* @see LayoutInflater.from
*/
val Context.inflater: LayoutInflater
get() = LayoutInflater.from(this)
/**
* Returns whether the current UI is in night mode or not. This will work if the theme is automatic
* as well.
* Whether the device is in night mode or not.
*/
val Context.isNight
get() =
resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK ==
Configuration.UI_MODE_NIGHT_YES
/** Returns if this device is in landscape. */
/**
* Whether the device is in landscape mode or not.
*/
val Context.isLandscape
get() = resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
/**
* Gets a content resolver in a way that does not mangle metadata on certain OEM skins. See
* https://github.com/OxygenCobalt/Auxio/issues/50 for more info.
*/
val Context.contentResolverSafe: ContentResolver
get() = applicationContext.contentResolver
/**
* @brief Convenience method for getting a plural.
* @param pluralRes Resource ID for the plural
* @brief Get a plural resource.
* @param pluralRes A plural resource ID.
* @param value Int value for the plural.
* @return The formatted string requested
* @return The formatted string requested.
*/
fun Context.getPlural(@PluralsRes pluralRes: Int, value: Int) =
resources.getQuantityString(pluralRes, value, value)
/**
* @brief Convenience method for obtaining an integer resource
* @param integerRes Resource ID for the integer
* @return The integer resource requested
* @brief Get an integer resource.
* @param integerRes An integer resource ID.
* @return The integer resource requested.
*/
fun Context.getInteger(@IntegerRes integerRes: Int) = resources.getInteger(integerRes)
/**
* Convenience method for getting a [ColorStateList] resource safely.
* @param color The color resource
* @return The [ColorStateList] requested
* Get a [ColorStateList] resource.
* @param colorRes A color resource ID.
* @return The [ColorStateList] requested.
*/
fun Context.getColorCompat(@ColorRes color: Int) =
requireNotNull(ContextCompat.getColorStateList(this, color)) {
fun Context.getColorCompat(@ColorRes colorRes: Int) =
requireNotNull(ContextCompat.getColorStateList(this, colorRes)) {
"Invalid resource: State list was null"
}
/**
* Convenience method for getting a color attribute safely.
* @param attr The color attribute
* @return The attribute requested
* Get a [ColorStateList] pointed to by an attribute.
* @param attrRes An attribute resource ID.
* @return The [ColorStateList] the requested attribute points to.
*/
fun Context.getAttrColorCompat(@AttrRes attr: Int): ColorStateList {
fun Context.getAttrColorCompat(@AttrRes attrRes: Int): ColorStateList {
// First resolve the attribute into its ID
val resolvedAttr = TypedValue()
theme.resolveAttribute(attr, resolvedAttr, true)
theme.resolveAttribute(attrRes, resolvedAttr, true)
// Then convert it to a proper color
val color =
@ -114,31 +109,31 @@ fun Context.getAttrColorCompat(@AttrRes attr: Int): ColorStateList {
}
/**
* Convenience method for getting a [Drawable] safely.
* @param drawable The drawable resource
* @return The drawable requested
* Get a Drawable.
* @param drawableRes The Drawable resource ID.
* @return The Drawable requested.
*/
fun Context.getDrawableCompat(@DrawableRes drawable: Int) =
requireNotNull(ContextCompat.getDrawable(this, drawable)) {
fun Context.getDrawableCompat(@DrawableRes drawableRes: Int) =
requireNotNull(ContextCompat.getDrawable(this, drawableRes)) {
"Invalid resource: Drawable was null"
}
/**
* Convenience method for getting a dimension safely.
* @param dimen The dimension resource
* @return The dimension requested
* Get the complex (i.e DP) size of a dimension.
* @param dimenRes The dimension resource.
* @return The size of the dimension requested, in complex units.
*/
@Dimension fun Context.getDimen(@DimenRes dimen: Int) = resources.getDimension(dimen)
@Dimension fun Context.getDimen(@DimenRes dimenRes: Int) = resources.getDimension(dimenRes)
/**
* Convenience method for getting a dimension pixel size safely.
* @param dimen The dimension resource
* @return The dimension requested, in pixels
* Get the pixel size of a dimension.
* @param dimenRes The dimension resource
* @return The size of the dimension requested, in pixels
*/
@Px fun Context.getDimenSize(@DimenRes dimen: Int) = resources.getDimensionPixelSize(dimen)
@Px fun Context.getDimenPixels(@DimenRes dimenRes: Int) = resources.getDimensionPixelSize(dimenRes)
/**
* Convenience method for getting a system service without nullability issues.
* Get an instance of the requested system service.
* @param T The system service in question.
* @param serviceClass The service's kotlin class [Java class will be used in function call]
* @return The system service
@ -149,12 +144,17 @@ fun <T : Any> Context.getSystemServiceCompat(serviceClass: KClass<T>) =
"System service ${serviceClass.simpleName} could not be instantiated"
}
/** 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 short-length [Toast] with text from the specified string resource.
* @param stringRes The resource to the string to use in the toast.
*/
fun Context.showToast(@StringRes stringRes: Int) {
Toast.makeText(applicationContext, getString(stringRes), Toast.LENGTH_SHORT).show()
}
/** Create a [PendingIntent] that leads to Auxio's [MainActivity] */
/**
* Create a [PendingIntent] that will launch the app activity when launched.
*/
fun Context.newMainPendingIntent(): PendingIntent =
PendingIntent.getActivity(
this,
@ -162,10 +162,13 @@ fun Context.newMainPendingIntent(): PendingIntent =
Intent(this, MainActivity::class.java),
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) PendingIntent.FLAG_IMMUTABLE else 0)
/** Create a broadcast [PendingIntent] */
fun Context.newBroadcastPendingIntent(what: String): PendingIntent =
/**
* Create a [PendingIntent] that will broadcast the specified command when launched.
* @param action The action to broadcast when the [PendingIntent] is launched.
*/
fun Context.newBroadcastPendingIntent(action: String): PendingIntent =
PendingIntent.getBroadcast(
this,
IntegerTable.REQUEST_CODE,
Intent(what).setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY),
Intent(action).setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY),
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) PendingIntent.FLAG_IMMUTABLE else 0)

View file

@ -14,13 +14,13 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.util
import android.app.Application
import android.content.Context
import android.database.Cursor
import android.database.sqlite.SQLiteDatabase
import android.graphics.PointF
import android.graphics.drawable.Drawable
import android.os.Build
import android.view.View
@ -47,13 +47,28 @@ import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch
/**
* Determines if the point given by [x] and [y] falls within this view.
* @param minTouchTargetSize The minimum touch size, independent of the view's size (Optional)
* Get if this [View] contains the given [PointF], with optional leeway.
* @param x The x value of the point to check.
* @param y The y value of the point to check.
* @param minTouchTargetSize A minimum size to use when checking the value.
* This can be used to extend the range where a point is considered "contained"
* by the [View] beyond it's actual size.
* @return true if the [PointF] is contained by the view, false otherwise.
* Adapted from AndroidFastScroll: https://github.com/zhanghai/AndroidFastScroll
*/
fun View.isUnder(x: Float, y: Float, minTouchTargetSize: Int = 0) =
isUnderImpl(x, left, right, (parent as View).width, minTouchTargetSize) &&
isUnderImpl(y, top, bottom, (parent as View).height, minTouchTargetSize)
/**
* Internal implementation of [isUnder].
* @param position The position to check.
* @param viewStart The start of the view bounds, on the same axis as [position].
* @param viewEnd The end of the view bounds, on the same axis as [position]
* @param parentEnd The end of the parent bounds, on the same axis as [position].
* @param minTouchTargetSize The minimum size to use when checking if the value is
* in range.
*/
private fun isUnderImpl(
position: Float,
viewStart: Int,
@ -62,12 +77,11 @@ private fun isUnderImpl(
minTouchTargetSize: Int
): Boolean {
val viewSize = viewEnd - viewStart
if (viewSize >= minTouchTargetSize) {
return position >= viewStart && position < viewEnd
}
var touchTargetStart = viewStart - (minTouchTargetSize - viewSize) / 2
var touchTargetStart = viewStart - (minTouchTargetSize - viewSize) / 2
if (touchTargetStart < 0) {
touchTargetStart = 0
}
@ -76,7 +90,6 @@ private fun isUnderImpl(
if (touchTargetEnd > parentEnd) {
touchTargetEnd = parentEnd
touchTargetStart = touchTargetEnd - minTouchTargetSize
if (touchTargetStart < 0) {
touchTargetStart = 0
}
@ -85,66 +98,88 @@ private fun isUnderImpl(
return position >= touchTargetStart && position < touchTargetEnd
}
/** Returns if this view is RTL in a compatible manner. */
/**
* Whether this [View] is using an RTL layout direction.
*/
val View.isRtl: Boolean
get() = layoutDirection == View.LAYOUT_DIRECTION_RTL
/** Returns if this drawable is RTL in a compatible manner.] */
/**
* Whether this [Drawable] is using an RTL layout direction.
*/
val Drawable.isRtl: Boolean
get() = DrawableCompat.getLayoutDirection(this) == View.LAYOUT_DIRECTION_RTL
/** Shortcut to get a context from a ViewBinding */
/**
* Get a [Context] from a [ViewBinding]'s root [View].
*/
val ViewBinding.context: Context
get() = root.context
/** Returns whether a recyclerview can scroll. */
/**
* Compute if this [RecyclerView] can scroll through their items, or if the items can all fit on
* one screen.
*/
fun RecyclerView.canScroll() = computeVerticalScrollRange() > height
/**
* Shortcut to obtain the CoordinatorLayout behavior of a view. Null if not from a coordinator
* layout or if no behavior is present.
* Get the [CoordinatorLayout.Behavior] of a [View], or null if the [View] is not part of a
* [CoordinatorLayout] or does not have a [CoordinatorLayout.Behavior].
*/
val View.coordinatorLayoutBehavior: CoordinatorLayout.Behavior<View>?
get() = (layoutParams as? CoordinatorLayout.LayoutParams)?.behavior
/**
* Collect a [stateFlow] into [block] eventually.
*
* This does have an initializing call, but it usually occurs ~100ms into draw-time, which might not
* be ideal for some views. This should be used in cases where the state only needs to be updated
* during runtime.
* Collect a [StateFlow] into [block] in a lifecycle-aware manner *eventually.* Due to co-routine
* launching, the initializing call will occur ~100ms after draw time. If this is not desirable,
* use [collectImmediately].
* @param stateFlow The [StateFlow] to collect.
* @param block The code to run when the [StateFlow] updates.
*/
fun <T> Fragment.collect(stateFlow: StateFlow<T>, block: (T) -> Unit) {
launch { stateFlow.collect(block) }
}
/**
* Collect a [stateFlow] into [block] immediately.
*
* This method automatically calls [block] when initially starting to ensure UI state consistency at
* soon as the view is visible. This does nominally mean that there are two initializing
* collections, but this is considered okay. [block] should be a function pointer in order to ensure
* lifecycle consistency.
*
* This should be used if the state absolutely needs to be shown at draw-time.
* Collect a [StateFlow] into a [block] in a lifecycle-aware manner *immediately.* This will
* immediately run an initializing call to ensure the UI is set up before draw-time. Note
* that this will result in two initializing calls.
* @param stateFlow The [StateFlow] to collect.
* @param block The code to run when the [StateFlow] updates.
*/
fun <T> Fragment.collectImmediately(stateFlow: StateFlow<T>, block: (T) -> Unit) {
block(stateFlow.value)
launch { stateFlow.collect(block) }
}
/** Like [collectImmediately], but with two [StateFlow] values. */
/**
* Like [collectImmediately], but with two [StateFlow] instances that are collected
* with the same block.
* @param a The first [StateFlow] to collect.
* @param b The second [StateFlow] to collect.
* @param block The code to run when either [StateFlow] updates.
*/
fun <T1, T2> Fragment.collectImmediately(
a: StateFlow<T1>,
b: StateFlow<T2>,
block: (T1, T2) -> Unit
) {
block(a.value, b.value)
// We can combine flows, but only if we transform them into one flow output.
// Thus, we have to first combine the two flow values into a Pair, and then
// decompose it when we collect the values.
val combine = a.combine(b) { first, second -> Pair(first, second) }
launch { combine.collect { block(it.first, it.second) } }
}
/** Like [collectImmediately], but with three [StateFlow] values. */
/**
* Like [collectImmediately], but with three [StateFlow] instances that are collected
* with the same block.
* @param a The first [StateFlow] to collect.
* @param b The second [StateFlow] to collect.
* @param c The third [StateFlow] to collect.
* @param block The code to run when any of the [StateFlow]s update.
*/
fun <T1, T2, T3> Fragment.collectImmediately(
a: StateFlow<T1>,
b: StateFlow<T2>,
@ -157,9 +192,12 @@ fun <T1, T2, T3> Fragment.collectImmediately(
}
/**
* Launches [block] in a lifecycle-aware coroutine once [state] is reached. This is primarily a
* shortcut intended to correctly launch a co-routine on a fragment in a way that won't cause
* miscellaneous coroutine insanity.
* Launch a [Fragment] co-routine whenever the [Lifecycle] hits the given [Lifecycle.State].
* This should always been used when launching [Fragment] co-routines was it will not result
* in unexpected behavior.
* @param state The [Lifecycle.State] to launch the co-routine in.
* @param block The block to run in the co-routine.
* @see repeatOnLifecycle
*/
private fun Fragment.launch(
state: Lifecycle.State = Lifecycle.State.STARTED,
@ -169,77 +207,108 @@ private fun Fragment.launch(
}
/**
* Shortcut to generate a AndroidViewModel [T] without having to specify the bloated factory syntax.
* An extension to [viewModels] that automatically provides an
* [ViewModelProvider.AndroidViewModelFactory]. Use whenever an [AndroidViewModel]
* is used.
*/
inline fun <reified T : AndroidViewModel> Fragment.androidViewModels() =
viewModels<T> { ViewModelProvider.AndroidViewModelFactory(requireActivity().application) }
/**
* Shortcut to generate a AndroidViewModel [T] without having to specify the bloated factory syntax.
* An extension to [viewModels] that automatically provides an
* [ViewModelProvider.AndroidViewModelFactory]. Use whenever an [AndroidViewModel]
* is used. Note that this implementation is for an [AppCompatActivity], and thus
* makes this functionally equivalent in scope to [androidActivityViewModels].
*/
inline fun <reified T : AndroidViewModel> AppCompatActivity.androidViewModels() =
viewModels<T> { ViewModelProvider.AndroidViewModelFactory(application) }
/**
* Shortcut to generate a AndroidViewModel [T] without having to specify the bloated factory syntax.
* An extension to [activityViewModels] that automatically provides an
* [ViewModelProvider.AndroidViewModelFactory]. Use whenever an [AndroidViewModel]
* is used.
*/
inline fun <reified T : AndroidViewModel> Fragment.androidActivityViewModels() =
activityViewModels<T> {
ViewModelProvider.AndroidViewModelFactory(requireActivity().application)
}
/** Shortcut to get the [Application] from an [AndroidViewModel] */
val AndroidViewModel.application: Application
/**
* The [Context] provided to an [AndroidViewModel].
*/
inline val AndroidViewModel.context: Context
get() = getApplication()
/**
* Shortcut for querying all items in a database and running [block] with the cursor returned. Will
* not run if the cursor is null.
* Query all columns in the given [SQLiteDatabase] table, running the block when the [Cursor]
* is loaded. The block will be called with [use], allowing for automatic cleanup of [Cursor]
* resources.
* @param tableName The name of the table to query all columns in.
* @param block The code block to run with the loaded [Cursor].
*/
fun <R> SQLiteDatabase.queryAll(tableName: String, block: (Cursor) -> R) =
inline fun <R> SQLiteDatabase.queryAll(tableName: String, block: (Cursor) -> R) =
query(tableName, null, null, null, null, null, null)?.use(block)
// Note: WindowInsetsCompat and it's related methods cause too many issues.
// Use our own compat methods instead.
/**
* Resolve system bar insets in a version-aware manner. This can be used to apply padding to a view
* that properly follows all the changes that were made between Android 8-11.
* Get the "System Bar" [Insets] in this [WindowInsets] instance in a version-compatible manner
* This can be used to prevent [View] elements from intersecting with the navigation bars.
*/
val WindowInsets.systemBarInsetsCompat: Insets
get() =
when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> {
// API 30+, use window inset map.
getCompatInsets(WindowInsets.Type.systemBars())
}
// API 21+, use window inset fields.
else -> getSystemWindowCompatInsets()
}
/**
* Resolve gesture insets in a version-aware manner. This can be used to apply padding to a view
* that properly follows all the changes that were made between Android 8-11. Note that if the
* gesture insets are not present (i.e zeroed), the bar insets will be used instead.
* Get the "System Gesture" [Insets] in this [WindowInsets] instance in a version-compatible manner
* This can be used to prevent [View] elements from intersecting with the navigation bars and
* their extended gesture hit-boxes. Note that "System Bar" insets will be used if the system
* does not provide gesture insets.
*/
val WindowInsets.systemGestureInsetsCompat: Insets
get() =
// The reasoning for why we take a larger inset is because gesture insets are seemingly
// not present on some android versions. To prevent the app from appearing below the
// system bars, we fall back to the bar insets. This is guaranteed not to fire in any
// context but the gesture insets being invalid, as gesture insets are intended to
// be larger than bar insets.
// Some android versions seemingly don't provide gesture insets, setting them to zero.
// To resolve this, we take the maximum between the system bar and system gesture
// insets. Since system gesture insets should extend further than system bar insets,
// this should allow this code to fall back to system bar insets easily if the system
// does not provide system gesture insets. This does require androidx Insets to allow
// us to use the max method on all versions however, so we will want to convert the
// system-provided insets to such..
when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> {
// API 30+, use window inset map.
Insets.max(
getCompatInsets(WindowInsets.Type.systemGestures()),
getCompatInsets(WindowInsets.Type.systemBars()))
}
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> {
@Suppress("DEPRECATION")
// API 29, use window inset fields.
Insets.max(getSystemGestureCompatInsets(), getSystemWindowCompatInsets())
}
// API 21+ do not support gesture insets, as they don't have gesture navigation.
// Just use system bar insets.
else -> getSystemWindowCompatInsets()
}
/**
* Returns the given [Insets] based on the to the API 30+ [WindowInsets] convention.
* @param typeMask The type of [Insets] to obtain.
* @return Compat [Insets] corresponding to the given type.
* @see WindowInsets.getInsets
*/
@RequiresApi(Build.VERSION_CODES.R)
private fun WindowInsets.getCompatInsets(typeMask: Int) = Insets.toCompatInsets(getInsets(typeMask))
/**
* Returns "System Bar" [Insets] based on the API 21+ [WindowInsets] convention.
* @return Compat [Insets] consisting of the [WindowInsets] "System Bar" [Insets] field.
* @see WindowInsets.getSystemWindowInsets
*/
@Suppress("DEPRECATION")
private fun WindowInsets.getSystemWindowCompatInsets() =
Insets.of(
@ -248,16 +317,22 @@ private fun WindowInsets.getSystemWindowCompatInsets() =
systemWindowInsetRight,
systemWindowInsetBottom)
/**
* Returns "System Bar" [Insets] based on the API 29 [WindowInsets] convention.
* @return Compat [Insets] consisting of the [WindowInsets] "System Gesture" [Insets] fields.
* @see WindowInsets.getSystemGestureInsets
*/
@Suppress("DEPRECATION")
@RequiresApi(Build.VERSION_CODES.Q)
private fun WindowInsets.getSystemGestureCompatInsets() = Insets.toCompatInsets(systemGestureInsets)
@RequiresApi(Build.VERSION_CODES.R)
private fun WindowInsets.getCompatInsets(typeMask: Int) = Insets.toCompatInsets(getInsets(typeMask))
/**
* Replaces the system bar insets in a version-aware manner. This can be used to modify the insets
* for child views in a way that follows all of the changes that were made between 8-11.
* Replace the "System Bar" [Insets] in [WindowInsets] with a new set of [Insets].
* @param left The new left inset.
* @param top The new top inset.
* @param right The new right inset.
* @param bottom The new bottom inset.
* @return A new [WindowInsets] with the given "System Bar" inset values applied.
*/
fun WindowInsets.replaceSystemBarInsetsCompat(
left: Int,
@ -267,6 +342,7 @@ fun WindowInsets.replaceSystemBarInsetsCompat(
): WindowInsets {
return when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> {
// API 30+, use a Builder to create a new instance.
WindowInsets.Builder(this)
.setInsets(
WindowInsets.Type.systemBars(),
@ -274,6 +350,7 @@ fun WindowInsets.replaceSystemBarInsetsCompat(
.build()
}
else -> {
// API 21+, replace the system bar inset fields.
@Suppress("DEPRECATION") replaceSystemWindowInsets(left, top, right, bottom)
}
}

View file

@ -18,21 +18,13 @@
package org.oxycblt.auxio.util
import android.os.Looper
import java.lang.reflect.Field
import java.lang.reflect.Method
import kotlin.reflect.KClass
import org.oxycblt.auxio.BuildConfig
/** Assert that we are on a background thread. */
fun requireBackgroundThread() {
check(Looper.myLooper() != Looper.getMainLooper()) {
"This operation must be ran on a background thread"
}
}
/**
* Sanitizes a nullable value that is not likely to be null. On debug builds, requireNotNull is
* used, while on release builds, the unsafe assertion operator [!!] ]is used
* Sanitizes a value that is unlikely to be null. On debug builds, this aliases to [requireNotNull],
* otherwise, it aliases to the unchecked dereference operator (!!). This can be used as a minor
* optimization in certain cases.
*/
fun <T> unlikelyToBeNull(value: T?) =
if (BuildConfig.DEBUG) {
@ -41,21 +33,52 @@ fun <T> unlikelyToBeNull(value: T?) =
value!!
}
/** Returns null if this value is 0. */
/**
* Aliases a check to ensure that the given number is non-zero.
* @return The given number if it's non-zero, null otherwise.
*/
fun Int.nonZeroOrNull() = if (this > 0) this else null
/** Returns null if this value is 0. */
/**
* Aliases a check to ensure that the given number is non-zero.
* @return The same number if it's non-zero, null otherwise.
*/
fun Long.nonZeroOrNull() = if (this > 0) this else null
/** Returns null if this value is not in [range]. */
/**
* Aliases a check to ensure a given value is in a specified range.
* @param range The valid range of values for this number.
* @return The same number if it is in the range, null otherwise.
*/
fun Int.inRangeOrNull(range: IntRange) = if (range.contains(this)) this else null
/** Lazily reflect to retrieve a [Field]. */
/**
* Lazily set up a reflected field. Automatically handles visibility changes.
* Adapted from Material Files: https://github.com/zhanghai/MaterialFiles
* @param clazz The [KClass] to reflect into.
* @param field The name of the field to obtain.
*/
fun lazyReflectedField(clazz: KClass<*>, field: String) = lazy {
clazz.java.getDeclaredField(field).also { it.isAccessible = true }
}
/** Lazily reflect to retrieve a [Method]. */
/**
* Lazily set up a reflected method. Automatically handles visibility changes.
* Adapted from Material Files: https://github.com/zhanghai/MaterialFiles
* @param clazz The [KClass] to reflect into.
* @param field The name of the method to obtain.
*/
fun lazyReflectedMethod(clazz: KClass<*>, method: String) = lazy {
clazz.java.getDeclaredMethod(method).also { it.isAccessible = true }
}
/**
* Assert that the execution is currently on a background thread. This is helpful for
* functions that don't necessarily require suspend, but still want to ensure that they
* are being called with a co-routine.
* @throws IllegalStateException If the execution is not on a background thread.
*/
fun requireBackgroundThread() {
check(Looper.myLooper() != Looper.getMainLooper()) {
"This operation must be ran on a background thread"
}
}

View file

@ -24,14 +24,14 @@ import org.oxycblt.auxio.BuildConfig
// Yes, I know timber exists but this does what I need.
/**
* Shortcut method for logging a non-string [obj] to debug. Should only be used for debug
* preferably.
* Log an object to the debug channel. Automatically handles tags.
* @param obj The object to log.
*/
fun Any.logD(obj: Any?) = logD("$obj")
/**
* Shortcut method for logging [msg] to the debug console. Handles debug builds and anonymous
* objects
* Log a string message to the debug channel. Automatically handles tags.
* @param msg The message to log.
*/
fun Any.logD(msg: String) {
if (BuildConfig.DEBUG && !copyleftNotice()) {
@ -39,19 +39,28 @@ fun Any.logD(msg: String) {
}
}
/** Shortcut method for logging [msg] as a warning to the console. Handles anonymous objects */
/**
* Log a string message to the warning channel. Automatically handles tags.
* @param msg The message to log.
*/
fun Any.logW(msg: String) = Log.w(autoTag, msg)
/** Shortcut method for logging [msg] as an error to the console. Handles anonymous objects */
/**
* Log a string message to the error channel. Automatically handles tags.
* @param msg The message to log.
*/
fun Any.logE(msg: String) = Log.e(autoTag, msg)
/** Automatically creates a tag that identifies the object currently logging. */
/**
* The LogCat-suitable tag for this string. Consists of the object's name, or "Anonymous Object"
* if the object does not exist.
*/
private val Any.autoTag: String
get() = "Auxio.${this::class.simpleName ?: "Anonymous Object"}"
/**
* Please don't plagiarize Auxio! You are free to remove this as long as you continue to keep your
* source open.
* Please don't plagiarize Auxio!
* You are free to remove this as long as you continue to keep your source open.
*/
@Suppress("KotlinConstantConditions")
private fun copyleftNotice(): Boolean {
@ -61,9 +70,7 @@ private fun copyleftNotice(): Boolean {
"Auxio Project",
"Friendly reminder: Auxio is licensed under the " +
"GPLv3 and all derivative apps must be made open source!")
return true
}
return false
}

View file

@ -32,7 +32,7 @@ import org.oxycblt.auxio.playback.state.InternalPlayer
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.getDimenSize
import org.oxycblt.auxio.util.getDimenPixels
import org.oxycblt.auxio.util.logD
/**
@ -84,10 +84,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.getDimenSize(android.R.dimen.system_app_widget_inner_radius)
context.getDimenPixels(android.R.dimen.system_app_widget_inner_radius)
} else if (settings.roundMode) {
// < Android 12, but the user still enabled round mode.
context.getDimenSize(R.dimen.size_corners_medium)
context.getDimenPixels(R.dimen.size_corners_medium)
} else {
// User did not enable round mode.
0