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. # hide the original source file name.
#-renamesourcefileattribute SourceFile #-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 -dontobfuscate

View file

@ -33,15 +33,14 @@ import org.oxycblt.auxio.image.extractor.MusicKeyer
import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.settings.Settings
/** /**
* Auxio. * Auxio: A simple, rational music player for android.
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
class AuxioApp : Application(), ImageLoaderFactory { class AuxioApp : Application(), ImageLoaderFactory {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
// Migrate any settings that may have changed in an app update.
Settings(this).migrate() Settings(this).migrate()
// Adding static shortcuts in a dynamic manner is better than declaring them // Adding static shortcuts in a dynamic manner is better than declaring them
// manually, as it will properly handle the difference between debug and release // manually, as it will properly handle the difference between debug and release
// Auxio instances. // Auxio instances.
@ -75,7 +74,14 @@ class AuxioApp : Application(), ImageLoaderFactory {
.build() .build()
companion object { companion object {
/**
* The ID of the "Shuffle All" shortcut.
*/
const val SHORTCUT_SHUFFLE_ID = "shortcut_shuffle" 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" 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 navModel: NavigationViewModel by activityViewModels()
private val callback = DynamicBackPressedCallback() private val callback = DynamicBackPressedCallback()
private var lastInsets: WindowInsets? = null private var lastInsets: WindowInsets? = null
private val elevationNormal: Float by lifecycleObject { binding -> private val elevationNormal: Float by lifecycleObject { binding ->
binding.context.getDimen(R.dimen.elevation_normal) binding.context.getDimen(R.dimen.elevation_normal)
} }
@ -72,7 +71,8 @@ class MainFragment :
override fun onBindingCreated(binding: FragmentMainBinding, savedInstanceState: Bundle?) { override fun onBindingCreated(binding: FragmentMainBinding, savedInstanceState: Bundle?) {
// --- UI SETUP --- // --- UI SETUP ---
val context = requireActivity() 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) context.onBackPressedDispatcher.addCallback(viewLifecycleOwner, callback)
binding.root.setOnApplyWindowInsetsListener { _, insets -> binding.root.setOnApplyWindowInsetsListener { _, insets ->
@ -89,24 +89,26 @@ class MainFragment :
val queueSheetBehavior = val queueSheetBehavior =
binding.queueSheet.coordinatorLayoutBehavior as QueueBottomSheetBehavior? binding.queueSheet.coordinatorLayoutBehavior as QueueBottomSheetBehavior?
if (queueSheetBehavior != null) { if (queueSheetBehavior != null) {
// Bottom sheet mode, set up click listeners.
val playbackSheetBehavior = val playbackSheetBehavior =
binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior
unlikelyToBeNull(binding.handleWrapper).setOnClickListener { unlikelyToBeNull(binding.handleWrapper).setOnClickListener {
if (playbackSheetBehavior.state == NeoBottomSheetBehavior.STATE_EXPANDED && if (playbackSheetBehavior.state == NeoBottomSheetBehavior.STATE_EXPANDED &&
queueSheetBehavior.state == NeoBottomSheetBehavior.STATE_COLLAPSED) { queueSheetBehavior.state == NeoBottomSheetBehavior.STATE_COLLAPSED) {
// Playback sheet is expanded and queue sheet is collapsed, we can expand it.
queueSheetBehavior.state = NeoBottomSheetBehavior.STATE_EXPANDED queueSheetBehavior.state = NeoBottomSheetBehavior.STATE_EXPANDED
} }
} }
} else { } else {
// Dual-pane mode, color/pad the queue sheet manually. // Dual-pane mode, manually style the static queue sheet.
binding.queueSheet.apply { binding.queueSheet.apply {
// Emulate the elevated bottom sheet style.
background = background =
MaterialShapeDrawable.createWithElevationOverlay(context).apply { MaterialShapeDrawable.createWithElevationOverlay(context).apply {
fillColor = context.getAttrColorCompat(R.attr.colorSurface) fillColor = context.getAttrColorCompat(R.attr.colorSurface)
elevation = context.getDimen(R.dimen.elevation_normal) elevation = context.getDimen(R.dimen.elevation_normal)
} }
// Apply bar insets for the queue's RecyclerView to usee.
setOnApplyWindowInsetsListener { v, insets -> setOnApplyWindowInsetsListener { v, insets ->
v.updatePadding(top = insets.systemBarInsetsCompat.top) v.updatePadding(top = insets.systemBarInsetsCompat.top)
insets insets
@ -115,7 +117,6 @@ class MainFragment :
} }
// --- VIEWMODEL SETUP --- // --- VIEWMODEL SETUP ---
collect(navModel.mainNavigationAction, ::handleMainNavigation) collect(navModel.mainNavigationAction, ::handleMainNavigation)
collect(navModel.exploreNavigationItem, ::handleExploreNavigation) collect(navModel.exploreNavigationItem, ::handleExploreNavigation)
collect(navModel.exploreNavigationArtists, ::handleExplorePicker) collect(navModel.exploreNavigationArtists, ::handleExplorePicker)
@ -125,7 +126,6 @@ class MainFragment :
override fun onStart() { override fun onStart() {
super.onStart() super.onStart()
// Callback could still reasonably fire even if we clear the binding, attach/detach // Callback could still reasonably fire even if we clear the binding, attach/detach
// our pre-draw listener our listener in onStart/onStop respectively. // our pre-draw listener our listener in onStart/onStop respectively.
requireBinding().playbackSheet.viewTreeObserver.addOnPreDrawListener(this) requireBinding().playbackSheet.viewTreeObserver.addOnPreDrawListener(this)
@ -139,33 +139,33 @@ class MainFragment :
override fun onPreDraw(): Boolean { override fun onPreDraw(): Boolean {
// We overload CoordinatorLayout far too much to rely on any of it's typical // We overload CoordinatorLayout far too much to rely on any of it's typical
// callback functionality. Just update all transitions before every draw. Should // callback functionality. Just update all transitions before every draw. Should
// probably be cheap *enough.* // probably be cheap enough.
val binding = requireBinding() val binding = requireBinding()
val playbackSheetBehavior = val playbackSheetBehavior =
binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior
val queueSheetBehavior =
binding.queueSheet.coordinatorLayoutBehavior as QueueBottomSheetBehavior?
val playbackRatio = max(playbackSheetBehavior.calculateSlideOffset(), 0f) val playbackRatio = max(playbackSheetBehavior.calculateSlideOffset(), 0f)
val outPlaybackRatio = 1 - playbackRatio val outPlaybackRatio = 1 - playbackRatio
val halfOutRatio = min(playbackRatio * 2, 1f) val halfOutRatio = min(playbackRatio * 2, 1f)
val halfInPlaybackRatio = max(playbackRatio - 0.5f, 0f) * 2 val halfInPlaybackRatio = max(playbackRatio - 0.5f, 0f) * 2
val queueSheetBehavior =
binding.queueSheet.coordinatorLayoutBehavior as QueueBottomSheetBehavior?
if (queueSheetBehavior != null) { if (queueSheetBehavior != null) {
// Queue sheet, take queue into account so the playback bar is shown and the playback // Queue sheet available, the normal transition applies, but it now much be combined
// panel is hidden when the queue sheet is expanded. // 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 queueRatio = max(queueSheetBehavior.calculateSlideOffset(), 0f)
val halfOutQueueRatio = min(queueRatio * 2, 1f) val halfOutQueueRatio = min(queueRatio * 2, 1f)
val halfInQueueRatio = max(queueRatio - 0.5f, 0f) * 2 val halfInQueueRatio = max(queueRatio - 0.5f, 0f) * 2
binding.playbackBarFragment.alpha = max(1 - halfOutRatio, halfInQueueRatio) binding.playbackBarFragment.alpha = max(1 - halfOutRatio, halfInQueueRatio)
binding.playbackPanelFragment.alpha = min(halfInPlaybackRatio, 1 - halfOutQueueRatio) binding.playbackPanelFragment.alpha = min(halfInPlaybackRatio, 1 - halfOutQueueRatio)
binding.queueFragment.alpha = queueRatio binding.queueFragment.alpha = queueRatio
if (playbackModel.song.value != null) { 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 = playbackSheetBehavior.isDraggable =
queueSheetBehavior.state == NeoBottomSheetBehavior.STATE_COLLAPSED queueSheetBehavior.state == NeoBottomSheetBehavior.STATE_COLLAPSED
} }
@ -175,26 +175,42 @@ class MainFragment :
binding.playbackPanelFragment.alpha = halfInPlaybackRatio binding.playbackPanelFragment.alpha = halfInPlaybackRatio
} }
// Fade out the content as the playback panel expands.
// TODO: Replace with shadow?
binding.exploreNavHost.apply { binding.exploreNavHost.apply {
alpha = outPlaybackRatio alpha = outPlaybackRatio
// Prevent interactions when the content fully fades out.
isInvisible = alpha == 0f 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 binding.playbackSheet.translationZ = elevationNormal * outPlaybackRatio
playbackSheetBehavior.sheetBackgroundDrawable.alpha = (outPlaybackRatio * 255).toInt() playbackSheetBehavior.sheetBackgroundDrawable.alpha = (outPlaybackRatio * 255).toInt()
// Fade out the playback bar as the panel expands.
binding.playbackBarFragment.apply { binding.playbackBarFragment.apply {
// Prevent interactions when the playback bar fully fades out.
isInvisible = alpha == 0f 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 } lastInsets?.let { translationY = it.systemBarInsetsCompat.top * halfOutRatio }
} }
// Prevent interactions when the playback panell fully fades out.
binding.playbackPanelFragment.isInvisible = binding.playbackPanelFragment.alpha == 0f binding.playbackPanelFragment.isInvisible = binding.playbackPanelFragment.alpha == 0f
binding.queueSheet.apply { binding.queueSheet.apply {
// Queue sheet (not queue content) should fade out with the playback panel.
alpha = halfInPlaybackRatio alpha = halfInPlaybackRatio
// Prevent interactions when the queue sheet fully fades out.
binding.queueSheet.isInvisible = alpha == 0f binding.queueSheet.isInvisible = alpha == 0f
} }
// Prevent interactions when the queue content fully fades out.
binding.queueFragment.isInvisible = binding.queueFragment.alpha == 0f binding.queueFragment.isInvisible = binding.queueFragment.alpha == 0f
if (playbackModel.song.value == null) { if (playbackModel.song.value == null) {
@ -216,8 +232,7 @@ class MainFragment :
when (action) { when (action) {
is MainNavigationAction.Expand -> tryExpandAll() is MainNavigationAction.Expand -> tryExpandAll()
is MainNavigationAction.Collapse -> tryCollapseAll() is MainNavigationAction.Collapse -> tryCollapseAll()
// No need to reset selection despite navigating to another screen, as the // TODO: Figure out how to clear out the selections as one moves between screens.
// main fragment destinations don't have selections
is MainNavigationAction.Directions -> findNavController().navigate(action.directions) 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 java.lang.reflect.Field
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.shared.AuxioAppBarLayout import org.oxycblt.auxio.shared.AuxioAppBarLayout
import org.oxycblt.auxio.util.getInteger
import org.oxycblt.auxio.util.lazyReflectedField 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 } addUpdateListener { titleView.alpha = it.animatedValue as Float }
duration = duration =
if (titleShown == true) { if (titleShown == true) {
context.resources.getInteger(R.integer.anim_fade_enter_duration).toLong() context.getInteger(R.integer.anim_fade_enter_duration).toLong()
} else { } else {
context.resources.getInteger(R.integer.anim_fade_exit_duration).toLong() context.getInteger(R.integer.anim_fade_exit_duration).toLong()
} }
start() start()
} }

View file

@ -295,7 +295,7 @@ class DetailViewModel(application: Application) :
val extractor = MediaExtractor() val extractor = MediaExtractor()
try { try {
extractor.setDataSource(application, song.uri, emptyMap()) extractor.setDataSource(context, song.uri, emptyMap())
} catch (e: Exception) { } catch (e: Exception) {
// Can feasibly fail with invalid file formats. Note that this isn't considered // 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 // 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.Song
import org.oxycblt.auxio.music.Sort import org.oxycblt.auxio.music.Sort
import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.util.application import org.oxycblt.auxio.util.context
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
/** /**
@ -131,13 +131,13 @@ class HomeViewModel(application: Application) :
override fun onSettingChanged(key: String) { override fun onSettingChanged(key: String) {
when (key) { 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. // Tabs changed, update the current tabs and set up a re-create event.
currentTabModes = getVisibleTabModes() currentTabModes = getVisibleTabModes()
_shouldRecreate.value = true _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 // Changes in the hide collaborator setting will change the artist contents
// of the library, consider it a library update. // of the library, consider it a library update.
onLibraryChanged(musicStore.library) onLibraryChanged(musicStore.library)

View file

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

View file

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

View file

@ -35,7 +35,8 @@ import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.util.getAttrColorCompat import org.oxycblt.auxio.util.getAttrColorCompat
import org.oxycblt.auxio.util.getColorCompat 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 * 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 // Override the layout params of the indicator so that it's in the
// bottom left corner. // bottom left corner.
gravity = Gravity.BOTTOM or Gravity.END 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) updateMarginsRelative(bottom = spacing, end = spacing)
}) })
} }
@ -225,12 +226,12 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
// Activated -> Show selection indicator // Activated -> Show selection indicator
targetAlpha = 1f targetAlpha = 1f
targetDuration = targetDuration =
context.resources.getInteger(R.integer.anim_fade_enter_duration).toLong() context.getInteger(R.integer.anim_fade_enter_duration).toLong()
} else { } else {
// Activated -> Hide selection indicator. // Activated -> Hide selection indicator.
targetAlpha = 0f targetAlpha = 0f
targetDuration = targetDuration =
context.resources.getInteger(R.integer.anim_fade_exit_duration).toLong() context.getInteger(R.integer.anim_fade_exit_duration).toLong()
} }
if (selectionIndicatorView.alpha == targetAlpha) { if (selectionIndicatorView.alpha == targetAlpha) {

View file

@ -28,7 +28,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.divider.MaterialDivider import com.google.android.material.divider.MaterialDivider
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.util.getDimenSize import org.oxycblt.auxio.util.getDimenPixels
/** /**
* A [RecyclerView] intended for use in Dialogs, adding features such as: * 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 topDivider = MaterialDivider(context)
private val bottomDivider = 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 { init {
// Apply top padding to give enough room to the dialog title, assuming that this view // 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 androidx.core.view.isInvisible
import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.appbar.MaterialToolbar
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.util.getInteger
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
/** /**
@ -116,12 +117,12 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
targetInnerAlpha = 0f targetInnerAlpha = 0f
targetSelectionAlpha = 1f targetSelectionAlpha = 1f
targetDuration = targetDuration =
context.resources.getInteger(R.integer.anim_fade_enter_duration).toLong() context.getInteger(R.integer.anim_fade_enter_duration).toLong()
} else { } else {
targetInnerAlpha = 1f targetInnerAlpha = 1f
targetSelectionAlpha = 0f targetSelectionAlpha = 0f
targetDuration = targetDuration =
context.resources.getInteger(R.integer.anim_fade_exit_duration).toLong() context.getInteger(R.integer.anim_fade_exit_duration).toLong()
} }
if (innerToolbar.alpha == targetInnerAlpha && 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.Callback
import org.oxycblt.auxio.music.MusicStore.Library import org.oxycblt.auxio.music.MusicStore.Library
import org.oxycblt.auxio.music.storage.useQuery 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.. * 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.storageVolumesCompat
import org.oxycblt.auxio.music.storage.useQuery import org.oxycblt.auxio.music.storage.useQuery
import org.oxycblt.auxio.settings.Settings 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.getSystemServiceCompat
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD

View file

@ -33,6 +33,13 @@ import org.oxycblt.auxio.util.lazyReflectedMethod
// --- MEDIASTORE UTILITIES --- // --- 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. * A shortcut for querying the [ContentResolver] database.
* @param uri The [Uri] of content to retrieve. * @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.BuildConfig
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.music.MusicStore import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.music.storage.contentResolverSafe
import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.playback.state.PlaybackStateManager
import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.shared.ForegroundManager import org.oxycblt.auxio.shared.ForegroundManager
import org.oxycblt.auxio.util.contentResolverSafe
import org.oxycblt.auxio.util.getSystemServiceCompat import org.oxycblt.auxio.util.getSystemServiceCompat
import org.oxycblt.auxio.util.logD 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.PlaybackStateManager
import org.oxycblt.auxio.playback.state.RepeatMode import org.oxycblt.auxio.playback.state.RepeatMode
import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.util.application import org.oxycblt.auxio.util.context
/** /**
* The ViewModel that provides a UI frontend for [PlaybackStateManager]. * The ViewModel that provides a UI frontend for [PlaybackStateManager].
@ -281,7 +281,7 @@ class PlaybackViewModel(application: Application) :
*/ */
fun savePlaybackState(onDone: (Boolean) -> Unit) { fun savePlaybackState(onDone: (Boolean) -> Unit) {
viewModelScope.launch { viewModelScope.launch {
val saved = playbackManager.saveState(PlaybackStateDatabase.getInstance(application)) val saved = playbackManager.saveState(PlaybackStateDatabase.getInstance(context))
onDone(saved) onDone(saved)
} }
} }
@ -292,7 +292,7 @@ class PlaybackViewModel(application: Application) :
*/ */
fun wipePlaybackState(onDone: (Boolean) -> Unit) { fun wipePlaybackState(onDone: (Boolean) -> Unit) {
viewModelScope.launch { viewModelScope.launch {
val wiped = playbackManager.wipeState(PlaybackStateDatabase.getInstance(application)) val wiped = playbackManager.wipeState(PlaybackStateDatabase.getInstance(context))
onDone(wiped) onDone(wiped)
} }
} }
@ -305,7 +305,7 @@ class PlaybackViewModel(application: Application) :
fun tryRestorePlaybackState(onDone: (Boolean) -> Unit) { fun tryRestorePlaybackState(onDone: (Boolean) -> Unit) {
viewModelScope.launch { viewModelScope.launch {
val restored = val restored =
playbackManager.restoreState(PlaybackStateDatabase.getInstance(application), true) playbackManager.restoreState(PlaybackStateDatabase.getInstance(context), true)
onDone(restored) onDone(restored)
} }
} }

View file

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

View file

@ -22,6 +22,7 @@ import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import com.google.android.material.button.MaterialButton import com.google.android.material.button.MaterialButton
import org.oxycblt.auxio.R 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 * 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?.cancel()
animator = animator =
ValueAnimator.ofFloat(currentCornerRadiusRatio, target).apply { 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) } addUpdateListener { updateCornerRadiusRatio(animatedValue as Float) }
start() start()
} }

View file

@ -41,7 +41,7 @@ import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.Sort import org.oxycblt.auxio.music.Sort
import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.util.application import org.oxycblt.auxio.util.context
import org.oxycblt.auxio.util.logD 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 // 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 // algorithm. If that fails, filter impls have fallback values, primarily around
// sort tags or file names. // sort tags or file names.
it.resolveNameNormalized(application).contains(value, ignoreCase = true) || it.resolveNameNormalized(context).contains(value, ignoreCase = true) ||
fallback(it) fallback(it)
} }
.ifEmpty { null } .ifEmpty { null }

View file

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

View file

@ -23,7 +23,7 @@ import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import kotlin.math.max import kotlin.math.max
import org.oxycblt.auxio.R 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 * 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) { ) : GridLayoutManager(context, attrs, defStyleAttr, defStyleRes) {
// We use 56dp here since that's the rough size of the accent item. // We use 56dp here since that's the rough size of the accent item.
// This will need to be modified if this is used beyond the accent dialog. // This will need to be modified if this is used beyond the accent dialog.
private var columnWidth = context.getDimenSize(R.dimen.size_accent_item) private var columnWidth = context.getDimenPixels(R.dimen.size_accent_item)
private var lastWidth = -1 private var lastWidth = -1
private var lastHeight = -1 private var lastHeight = -1

View file

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

View file

@ -18,12 +18,10 @@
package org.oxycblt.auxio.util package org.oxycblt.auxio.util
import android.app.PendingIntent import android.app.PendingIntent
import android.content.ContentResolver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.res.ColorStateList import android.content.res.ColorStateList
import android.content.res.Configuration import android.content.res.Configuration
import android.graphics.drawable.Drawable
import android.os.Build import android.os.Build
import android.util.TypedValue import android.util.TypedValue
import android.view.LayoutInflater import android.view.LayoutInflater
@ -42,65 +40,62 @@ import kotlin.reflect.KClass
import org.oxycblt.auxio.IntegerTable import org.oxycblt.auxio.IntegerTable
import org.oxycblt.auxio.MainActivity 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 val Context.inflater: LayoutInflater
get() = LayoutInflater.from(this) get() = LayoutInflater.from(this)
/** /**
* Returns whether the current UI is in night mode or not. This will work if the theme is automatic * Whether the device is in night mode or not.
* as well.
*/ */
val Context.isNight val Context.isNight
get() = get() =
resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK ==
Configuration.UI_MODE_NIGHT_YES Configuration.UI_MODE_NIGHT_YES
/** Returns if this device is in landscape. */ /**
* Whether the device is in landscape mode or not.
*/
val Context.isLandscape val Context.isLandscape
get() = resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE get() = resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
/** /**
* Gets a content resolver in a way that does not mangle metadata on certain OEM skins. See * @brief Get a plural resource.
* https://github.com/OxygenCobalt/Auxio/issues/50 for more info. * @param pluralRes A plural resource ID.
*/
val Context.contentResolverSafe: ContentResolver
get() = applicationContext.contentResolver
/**
* @brief Convenience method for getting a plural.
* @param pluralRes Resource ID for the plural
* @param value Int value for the plural. * @param value Int value for the plural.
* @return The formatted string requested * @return The formatted string requested.
*/ */
fun Context.getPlural(@PluralsRes pluralRes: Int, value: Int) = fun Context.getPlural(@PluralsRes pluralRes: Int, value: Int) =
resources.getQuantityString(pluralRes, value, value) resources.getQuantityString(pluralRes, value, value)
/** /**
* @brief Convenience method for obtaining an integer resource * @brief Get an integer resource.
* @param integerRes Resource ID for the integer * @param integerRes An integer resource ID.
* @return The integer resource requested * @return The integer resource requested.
*/ */
fun Context.getInteger(@IntegerRes integerRes: Int) = resources.getInteger(integerRes) fun Context.getInteger(@IntegerRes integerRes: Int) = resources.getInteger(integerRes)
/** /**
* Convenience method for getting a [ColorStateList] resource safely. * Get a [ColorStateList] resource.
* @param color The color resource * @param colorRes A color resource ID.
* @return The [ColorStateList] requested * @return The [ColorStateList] requested.
*/ */
fun Context.getColorCompat(@ColorRes color: Int) = fun Context.getColorCompat(@ColorRes colorRes: Int) =
requireNotNull(ContextCompat.getColorStateList(this, color)) { requireNotNull(ContextCompat.getColorStateList(this, colorRes)) {
"Invalid resource: State list was null" "Invalid resource: State list was null"
} }
/** /**
* Convenience method for getting a color attribute safely. * Get a [ColorStateList] pointed to by an attribute.
* @param attr The color attribute * @param attrRes An attribute resource ID.
* @return The attribute requested * @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 // First resolve the attribute into its ID
val resolvedAttr = TypedValue() val resolvedAttr = TypedValue()
theme.resolveAttribute(attr, resolvedAttr, true) theme.resolveAttribute(attrRes, resolvedAttr, true)
// Then convert it to a proper color // Then convert it to a proper color
val color = val color =
@ -114,31 +109,31 @@ fun Context.getAttrColorCompat(@AttrRes attr: Int): ColorStateList {
} }
/** /**
* Convenience method for getting a [Drawable] safely. * Get a Drawable.
* @param drawable The drawable resource * @param drawableRes The Drawable resource ID.
* @return The drawable requested * @return The Drawable requested.
*/ */
fun Context.getDrawableCompat(@DrawableRes drawable: Int) = fun Context.getDrawableCompat(@DrawableRes drawableRes: Int) =
requireNotNull(ContextCompat.getDrawable(this, drawable)) { requireNotNull(ContextCompat.getDrawable(this, drawableRes)) {
"Invalid resource: Drawable was null" "Invalid resource: Drawable was null"
} }
/** /**
* Convenience method for getting a dimension safely. * Get the complex (i.e DP) size of a dimension.
* @param dimen The dimension resource * @param dimenRes The dimension resource.
* @return The dimension requested * @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. * Get the pixel size of a dimension.
* @param dimen The dimension resource * @param dimenRes The dimension resource
* @return The dimension requested, in pixels * @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 T The system service in question.
* @param serviceClass The service's kotlin class [Java class will be used in function call] * @param serviceClass The service's kotlin class [Java class will be used in function call]
* @return The system service * @return The system service
@ -149,12 +144,17 @@ fun <T : Any> Context.getSystemServiceCompat(serviceClass: KClass<T>) =
"System service ${serviceClass.simpleName} could not be instantiated" "System service ${serviceClass.simpleName} could not be instantiated"
} }
/** Create a toast using the provided string resource. */ /**
fun Context.showToast(@StringRes str: Int) { * Create a short-length [Toast] with text from the specified string resource.
Toast.makeText(applicationContext, getString(str), Toast.LENGTH_SHORT).show() * @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 = fun Context.newMainPendingIntent(): PendingIntent =
PendingIntent.getActivity( PendingIntent.getActivity(
this, this,
@ -162,10 +162,13 @@ fun Context.newMainPendingIntent(): PendingIntent =
Intent(this, MainActivity::class.java), Intent(this, MainActivity::class.java),
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) PendingIntent.FLAG_IMMUTABLE else 0) 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( PendingIntent.getBroadcast(
this, this,
IntegerTable.REQUEST_CODE, 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) 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 * You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.oxycblt.auxio.util package org.oxycblt.auxio.util
import android.app.Application
import android.content.Context import android.content.Context
import android.database.Cursor import android.database.Cursor
import android.database.sqlite.SQLiteDatabase import android.database.sqlite.SQLiteDatabase
import android.graphics.PointF
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.os.Build import android.os.Build
import android.view.View import android.view.View
@ -47,13 +47,28 @@ import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
/** /**
* Determines if the point given by [x] and [y] falls within this view. * Get if this [View] contains the given [PointF], with optional leeway.
* @param minTouchTargetSize The minimum touch size, independent of the view's size (Optional) * @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) = fun View.isUnder(x: Float, y: Float, minTouchTargetSize: Int = 0) =
isUnderImpl(x, left, right, (parent as View).width, minTouchTargetSize) && isUnderImpl(x, left, right, (parent as View).width, minTouchTargetSize) &&
isUnderImpl(y, top, bottom, (parent as View).height, 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( private fun isUnderImpl(
position: Float, position: Float,
viewStart: Int, viewStart: Int,
@ -62,12 +77,11 @@ private fun isUnderImpl(
minTouchTargetSize: Int minTouchTargetSize: Int
): Boolean { ): Boolean {
val viewSize = viewEnd - viewStart val viewSize = viewEnd - viewStart
if (viewSize >= minTouchTargetSize) { if (viewSize >= minTouchTargetSize) {
return position >= viewStart && position < viewEnd return position >= viewStart && position < viewEnd
} }
var touchTargetStart = viewStart - (minTouchTargetSize - viewSize) / 2
var touchTargetStart = viewStart - (minTouchTargetSize - viewSize) / 2
if (touchTargetStart < 0) { if (touchTargetStart < 0) {
touchTargetStart = 0 touchTargetStart = 0
} }
@ -76,7 +90,6 @@ private fun isUnderImpl(
if (touchTargetEnd > parentEnd) { if (touchTargetEnd > parentEnd) {
touchTargetEnd = parentEnd touchTargetEnd = parentEnd
touchTargetStart = touchTargetEnd - minTouchTargetSize touchTargetStart = touchTargetEnd - minTouchTargetSize
if (touchTargetStart < 0) { if (touchTargetStart < 0) {
touchTargetStart = 0 touchTargetStart = 0
} }
@ -85,66 +98,88 @@ private fun isUnderImpl(
return position >= touchTargetStart && position < touchTargetEnd 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 val View.isRtl: Boolean
get() = layoutDirection == View.LAYOUT_DIRECTION_RTL 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 val Drawable.isRtl: Boolean
get() = DrawableCompat.getLayoutDirection(this) == View.LAYOUT_DIRECTION_RTL 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 val ViewBinding.context: Context
get() = root.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 fun RecyclerView.canScroll() = computeVerticalScrollRange() > height
/** /**
* Shortcut to obtain the CoordinatorLayout behavior of a view. Null if not from a coordinator * Get the [CoordinatorLayout.Behavior] of a [View], or null if the [View] is not part of a
* layout or if no behavior is present. * [CoordinatorLayout] or does not have a [CoordinatorLayout.Behavior].
*/ */
val View.coordinatorLayoutBehavior: CoordinatorLayout.Behavior<View>? val View.coordinatorLayoutBehavior: CoordinatorLayout.Behavior<View>?
get() = (layoutParams as? CoordinatorLayout.LayoutParams)?.behavior get() = (layoutParams as? CoordinatorLayout.LayoutParams)?.behavior
/** /**
* Collect a [stateFlow] into [block] eventually. * 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,
* This does have an initializing call, but it usually occurs ~100ms into draw-time, which might not * use [collectImmediately].
* be ideal for some views. This should be used in cases where the state only needs to be updated * @param stateFlow The [StateFlow] to collect.
* during runtime. * @param block The code to run when the [StateFlow] updates.
*/ */
fun <T> Fragment.collect(stateFlow: StateFlow<T>, block: (T) -> Unit) { fun <T> Fragment.collect(stateFlow: StateFlow<T>, block: (T) -> Unit) {
launch { stateFlow.collect(block) } launch { stateFlow.collect(block) }
} }
/** /**
* Collect a [stateFlow] into [block] immediately. * 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
* This method automatically calls [block] when initially starting to ensure UI state consistency at * that this will result in two initializing calls.
* soon as the view is visible. This does nominally mean that there are two initializing * @param stateFlow The [StateFlow] to collect.
* collections, but this is considered okay. [block] should be a function pointer in order to ensure * @param block The code to run when the [StateFlow] updates.
* lifecycle consistency.
*
* This should be used if the state absolutely needs to be shown at draw-time.
*/ */
fun <T> Fragment.collectImmediately(stateFlow: StateFlow<T>, block: (T) -> Unit) { fun <T> Fragment.collectImmediately(stateFlow: StateFlow<T>, block: (T) -> Unit) {
block(stateFlow.value) block(stateFlow.value)
launch { stateFlow.collect(block) } 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( fun <T1, T2> Fragment.collectImmediately(
a: StateFlow<T1>, a: StateFlow<T1>,
b: StateFlow<T2>, b: StateFlow<T2>,
block: (T1, T2) -> Unit block: (T1, T2) -> Unit
) { ) {
block(a.value, b.value) 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) } val combine = a.combine(b) { first, second -> Pair(first, second) }
launch { combine.collect { block(it.first, it.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( fun <T1, T2, T3> Fragment.collectImmediately(
a: StateFlow<T1>, a: StateFlow<T1>,
b: StateFlow<T2>, 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 * Launch a [Fragment] co-routine whenever the [Lifecycle] hits the given [Lifecycle.State].
* shortcut intended to correctly launch a co-routine on a fragment in a way that won't cause * This should always been used when launching [Fragment] co-routines was it will not result
* miscellaneous coroutine insanity. * 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( private fun Fragment.launch(
state: Lifecycle.State = Lifecycle.State.STARTED, 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() = inline fun <reified T : AndroidViewModel> Fragment.androidViewModels() =
viewModels<T> { ViewModelProvider.AndroidViewModelFactory(requireActivity().application) } 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() = inline fun <reified T : AndroidViewModel> AppCompatActivity.androidViewModels() =
viewModels<T> { ViewModelProvider.AndroidViewModelFactory(application) } 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() = inline fun <reified T : AndroidViewModel> Fragment.androidActivityViewModels() =
activityViewModels<T> { activityViewModels<T> {
ViewModelProvider.AndroidViewModelFactory(requireActivity().application) 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() get() = getApplication()
/** /**
* Shortcut for querying all items in a database and running [block] with the cursor returned. Will * Query all columns in the given [SQLiteDatabase] table, running the block when the [Cursor]
* not run if the cursor is null. * 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) 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 * Get the "System Bar" [Insets] in this [WindowInsets] instance in a version-compatible manner
* that properly follows all the changes that were made between Android 8-11. * This can be used to prevent [View] elements from intersecting with the navigation bars.
*/ */
val WindowInsets.systemBarInsetsCompat: Insets val WindowInsets.systemBarInsetsCompat: Insets
get() = get() =
when { when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> { Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> {
// API 30+, use window inset map.
getCompatInsets(WindowInsets.Type.systemBars()) getCompatInsets(WindowInsets.Type.systemBars())
} }
// API 21+, use window inset fields.
else -> getSystemWindowCompatInsets() else -> getSystemWindowCompatInsets()
} }
/** /**
* Resolve gesture insets in a version-aware manner. This can be used to apply padding to a view * Get the "System Gesture" [Insets] in this [WindowInsets] instance in a version-compatible manner
* that properly follows all the changes that were made between Android 8-11. Note that if the * This can be used to prevent [View] elements from intersecting with the navigation bars and
* gesture insets are not present (i.e zeroed), the bar insets will be used instead. * 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 val WindowInsets.systemGestureInsetsCompat: Insets
get() = get() =
// The reasoning for why we take a larger inset is because gesture insets are seemingly // Some android versions seemingly don't provide gesture insets, setting them to zero.
// not present on some android versions. To prevent the app from appearing below the // To resolve this, we take the maximum between the system bar and system gesture
// system bars, we fall back to the bar insets. This is guaranteed not to fire in any // insets. Since system gesture insets should extend further than system bar insets,
// context but the gesture insets being invalid, as gesture insets are intended to // this should allow this code to fall back to system bar insets easily if the system
// be larger than bar insets. // 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 { when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> { Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> {
// API 30+, use window inset map.
Insets.max( Insets.max(
getCompatInsets(WindowInsets.Type.systemGestures()), getCompatInsets(WindowInsets.Type.systemGestures()),
getCompatInsets(WindowInsets.Type.systemBars())) getCompatInsets(WindowInsets.Type.systemBars()))
} }
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> { Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> {
@Suppress("DEPRECATION") // API 29, use window inset fields.
Insets.max(getSystemGestureCompatInsets(), getSystemWindowCompatInsets()) 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() 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") @Suppress("DEPRECATION")
private fun WindowInsets.getSystemWindowCompatInsets() = private fun WindowInsets.getSystemWindowCompatInsets() =
Insets.of( Insets.of(
@ -248,16 +317,22 @@ private fun WindowInsets.getSystemWindowCompatInsets() =
systemWindowInsetRight, systemWindowInsetRight,
systemWindowInsetBottom) 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") @Suppress("DEPRECATION")
@RequiresApi(Build.VERSION_CODES.Q) @RequiresApi(Build.VERSION_CODES.Q)
private fun WindowInsets.getSystemGestureCompatInsets() = Insets.toCompatInsets(systemGestureInsets) 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 * Replace the "System Bar" [Insets] in [WindowInsets] with a new set of [Insets].
* for child views in a way that follows all of the changes that were made between 8-11. * @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( fun WindowInsets.replaceSystemBarInsetsCompat(
left: Int, left: Int,
@ -267,6 +342,7 @@ fun WindowInsets.replaceSystemBarInsetsCompat(
): WindowInsets { ): WindowInsets {
return when { return when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> { Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> {
// API 30+, use a Builder to create a new instance.
WindowInsets.Builder(this) WindowInsets.Builder(this)
.setInsets( .setInsets(
WindowInsets.Type.systemBars(), WindowInsets.Type.systemBars(),
@ -274,6 +350,7 @@ fun WindowInsets.replaceSystemBarInsetsCompat(
.build() .build()
} }
else -> { else -> {
// API 21+, replace the system bar inset fields.
@Suppress("DEPRECATION") replaceSystemWindowInsets(left, top, right, bottom) @Suppress("DEPRECATION") replaceSystemWindowInsets(left, top, right, bottom)
} }
} }

View file

@ -18,21 +18,13 @@
package org.oxycblt.auxio.util package org.oxycblt.auxio.util
import android.os.Looper import android.os.Looper
import java.lang.reflect.Field
import java.lang.reflect.Method
import kotlin.reflect.KClass import kotlin.reflect.KClass
import org.oxycblt.auxio.BuildConfig 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 * Sanitizes a value that is unlikely to be null. On debug builds, this aliases to [requireNotNull],
* used, while on release builds, the unsafe assertion operator [!!] ]is used * otherwise, it aliases to the unchecked dereference operator (!!). This can be used as a minor
* optimization in certain cases.
*/ */
fun <T> unlikelyToBeNull(value: T?) = fun <T> unlikelyToBeNull(value: T?) =
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
@ -41,21 +33,52 @@ fun <T> unlikelyToBeNull(value: T?) =
value!! 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 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 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 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 { fun lazyReflectedField(clazz: KClass<*>, field: String) = lazy {
clazz.java.getDeclaredField(field).also { it.isAccessible = true } 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 { fun lazyReflectedMethod(clazz: KClass<*>, method: String) = lazy {
clazz.java.getDeclaredMethod(method).also { it.isAccessible = true } 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. // 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 * Log an object to the debug channel. Automatically handles tags.
* preferably. * @param obj The object to log.
*/ */
fun Any.logD(obj: Any?) = logD("$obj") fun Any.logD(obj: Any?) = logD("$obj")
/** /**
* Shortcut method for logging [msg] to the debug console. Handles debug builds and anonymous * Log a string message to the debug channel. Automatically handles tags.
* objects * @param msg The message to log.
*/ */
fun Any.logD(msg: String) { fun Any.logD(msg: String) {
if (BuildConfig.DEBUG && !copyleftNotice()) { 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) 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) 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 private val Any.autoTag: String
get() = "Auxio.${this::class.simpleName ?: "Anonymous Object"}" 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 * Please don't plagiarize Auxio!
* source open. * You are free to remove this as long as you continue to keep your source open.
*/ */
@Suppress("KotlinConstantConditions") @Suppress("KotlinConstantConditions")
private fun copyleftNotice(): Boolean { private fun copyleftNotice(): Boolean {
@ -61,9 +70,7 @@ private fun copyleftNotice(): Boolean {
"Auxio Project", "Auxio Project",
"Friendly reminder: Auxio is licensed under the " + "Friendly reminder: Auxio is licensed under the " +
"GPLv3 and all derivative apps must be made open source!") "GPLv3 and all derivative apps must be made open source!")
return true return true
} }
return false 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.PlaybackStateManager
import org.oxycblt.auxio.playback.state.RepeatMode import org.oxycblt.auxio.playback.state.RepeatMode
import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.util.getDimenSize import org.oxycblt.auxio.util.getDimenPixels
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
/** /**
@ -84,10 +84,10 @@ class WidgetComponent(private val context: Context) :
val cornerRadius = val cornerRadius =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
// Android 12, always round the cover with the app widget's inner radius // Android 12, always round the cover with the app widget's inner radius
context.getDimenSize(android.R.dimen.system_app_widget_inner_radius) context.getDimenPixels(android.R.dimen.system_app_widget_inner_radius)
} else if (settings.roundMode) { } else if (settings.roundMode) {
// < Android 12, but the user still enabled round mode. // < Android 12, but the user still enabled round mode.
context.getDimenSize(R.dimen.size_corners_medium) context.getDimenPixels(R.dimen.size_corners_medium)
} else { } else {
// User did not enable round mode. // User did not enable round mode.
0 0