util: redocument
Redocument the util module. This should make the purpose of the utilities used in this app clearer.
This commit is contained in:
parent
e92b69e399
commit
7415c28e2d
27 changed files with 346 additions and 210 deletions
3
app/proguard-rules.pro
vendored
3
app/proguard-rules.pro
vendored
|
@ -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
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 &&
|
||||
|
|
|
@ -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..
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue