From 7415c28e2dee7ab0a9d78dfd4afadc33385b6349 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Thu, 22 Dec 2022 20:19:59 -0700 Subject: [PATCH] util: redocument Redocument the util module. This should make the purpose of the utilities used in this app clearer. --- app/proguard-rules.pro | 3 +- .../main/java/org/oxycblt/auxio/AuxioApp.kt | 12 +- .../java/org/oxycblt/auxio/MainFragment.kt | 51 +++-- .../auxio/detail/DetailAppBarLayout.kt | 5 +- .../oxycblt/auxio/detail/DetailViewModel.kt | 2 +- .../org/oxycblt/auxio/home/HomeViewModel.kt | 6 +- .../home/fastscroll/FastScrollPopupView.kt | 12 +- .../home/fastscroll/FastScrollRecyclerView.kt | 15 +- .../org/oxycblt/auxio/image/ImageGroup.kt | 9 +- .../auxio/list/recycler/DialogRecyclerView.kt | 4 +- .../list/selection/SelectionToolbarOverlay.kt | 5 +- .../org/oxycblt/auxio/music/MusicStore.kt | 2 +- .../music/extractor/MediaStoreExtractor.kt | 2 +- .../auxio/music/storage/StorageUtil.kt | 7 + .../auxio/music/system/IndexerService.kt | 2 +- .../auxio/playback/PlaybackViewModel.kt | 8 +- .../queue/QueueBottomSheetBehavior.kt | 4 +- .../playback/ui/AnimatedMaterialButton.kt | 3 +- .../oxycblt/auxio/search/SearchViewModel.kt | 4 +- .../oxycblt/auxio/settings/AboutFragment.kt | 5 - .../accent/AccentGridLayoutManager.kt | 4 +- .../auxio/settings/prefs/IntListPreference.kt | 3 +- .../org/oxycblt/auxio/util/ContextUtil.kt | 103 +++++----- .../org/oxycblt/auxio/util/FrameworkUtil.kt | 193 ++++++++++++------ .../util/{PrimitiveUtil.kt => LangUtil.kt} | 57 ++++-- .../java/org/oxycblt/auxio/util/LogUtil.kt | 29 ++- .../oxycblt/auxio/widgets/WidgetComponent.kt | 6 +- 27 files changed, 346 insertions(+), 210 deletions(-) rename app/src/main/java/org/oxycblt/auxio/util/{PrimitiveUtil.kt => LangUtil.kt} (51%) diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 73e1b85ef..f3c0f68db 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -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 \ No newline at end of file diff --git a/app/src/main/java/org/oxycblt/auxio/AuxioApp.kt b/app/src/main/java/org/oxycblt/auxio/AuxioApp.kt index 6b246989a..4fe121e41 100644 --- a/app/src/main/java/org/oxycblt/auxio/AuxioApp.kt +++ b/app/src/main/java/org/oxycblt/auxio/AuxioApp.kt @@ -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" } } diff --git a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt index be191f211..318d36dfc 100644 --- a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt @@ -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) } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/DetailAppBarLayout.kt b/app/src/main/java/org/oxycblt/auxio/detail/DetailAppBarLayout.kt index 967699cd1..7584ae05d 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailAppBarLayout.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailAppBarLayout.kt @@ -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() } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt index a669e8983..30c1e3a96 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt @@ -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 diff --git a/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt b/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt index 31ef0b82c..03936b5a9 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt @@ -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) diff --git a/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollPopupView.kt b/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollPopupView.kt index 31a7f6495..ba1de4483 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollPopupView.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollPopupView.kt @@ -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) diff --git a/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollRecyclerView.kt b/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollRecyclerView.kt index a77fe8691..ab59bee7d 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollRecyclerView.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollRecyclerView.kt @@ -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() } diff --git a/app/src/main/java/org/oxycblt/auxio/image/ImageGroup.kt b/app/src/main/java/org/oxycblt/auxio/image/ImageGroup.kt index 09d8a2fdf..1ac3d953f 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/ImageGroup.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/ImageGroup.kt @@ -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) { diff --git a/app/src/main/java/org/oxycblt/auxio/list/recycler/DialogRecyclerView.kt b/app/src/main/java/org/oxycblt/auxio/list/recycler/DialogRecyclerView.kt index db098b803..f71e08917 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/recycler/DialogRecyclerView.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/recycler/DialogRecyclerView.kt @@ -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 diff --git a/app/src/main/java/org/oxycblt/auxio/list/selection/SelectionToolbarOverlay.kt b/app/src/main/java/org/oxycblt/auxio/list/selection/SelectionToolbarOverlay.kt index 04433583b..8a9500296 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/selection/SelectionToolbarOverlay.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/selection/SelectionToolbarOverlay.kt @@ -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 && diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicStore.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicStore.kt index 608c67aeb..432a37de5 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicStore.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicStore.kt @@ -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.. diff --git a/app/src/main/java/org/oxycblt/auxio/music/extractor/MediaStoreExtractor.kt b/app/src/main/java/org/oxycblt/auxio/music/extractor/MediaStoreExtractor.kt index ee16148e2..7374d4024 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/extractor/MediaStoreExtractor.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/extractor/MediaStoreExtractor.kt @@ -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 diff --git a/app/src/main/java/org/oxycblt/auxio/music/storage/StorageUtil.kt b/app/src/main/java/org/oxycblt/auxio/music/storage/StorageUtil.kt index 73c602ff8..5aae60d5a 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/storage/StorageUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/storage/StorageUtil.kt @@ -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. diff --git a/app/src/main/java/org/oxycblt/auxio/music/system/IndexerService.kt b/app/src/main/java/org/oxycblt/auxio/music/system/IndexerService.kt index 470d8f2d2..9a6a4573a 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/system/IndexerService.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/system/IndexerService.kt @@ -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 diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt index 7b6ecff37..70b95edd5 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt @@ -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) } } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueBottomSheetBehavior.kt b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueBottomSheetBehavior.kt index a73eaf2da..16004fe1d 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueBottomSheetBehavior.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueBottomSheetBehavior.kt @@ -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(context: Context, attributeSet: AttributeSet?) : AuxioBottomSheetBehavior(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 diff --git a/app/src/main/java/org/oxycblt/auxio/playback/ui/AnimatedMaterialButton.kt b/app/src/main/java/org/oxycblt/auxio/playback/ui/AnimatedMaterialButton.kt index 029c41e9f..97dfb8eef 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/ui/AnimatedMaterialButton.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/ui/AnimatedMaterialButton.kt @@ -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() } diff --git a/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt b/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt index 682314a66..6b6b09d95 100644 --- a/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt @@ -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 } diff --git a/app/src/main/java/org/oxycblt/auxio/settings/AboutFragment.kt b/app/src/main/java/org/oxycblt/auxio/settings/AboutFragment.kt index e4945458c..162505c58 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/AboutFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/AboutFragment.kt @@ -72,19 +72,14 @@ class AboutFragment : ViewBindingFragment() { } 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, diff --git a/app/src/main/java/org/oxycblt/auxio/settings/accent/AccentGridLayoutManager.kt b/app/src/main/java/org/oxycblt/auxio/settings/accent/AccentGridLayoutManager.kt index e9666b1b2..171e364e9 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/accent/AccentGridLayoutManager.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/accent/AccentGridLayoutManager.kt @@ -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 diff --git a/app/src/main/java/org/oxycblt/auxio/settings/prefs/IntListPreference.kt b/app/src/main/java/org/oxycblt/auxio/settings/prefs/IntListPreference.kt index ed05fee31..0cbe414e3 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/prefs/IntListPreference.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/prefs/IntListPreference.kt @@ -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) diff --git a/app/src/main/java/org/oxycblt/auxio/util/ContextUtil.kt b/app/src/main/java/org/oxycblt/auxio/util/ContextUtil.kt index bb32dc891..af34af6b1 100644 --- a/app/src/main/java/org/oxycblt/auxio/util/ContextUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/util/ContextUtil.kt @@ -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 Context.getSystemServiceCompat(serviceClass: KClass) = "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) diff --git a/app/src/main/java/org/oxycblt/auxio/util/FrameworkUtil.kt b/app/src/main/java/org/oxycblt/auxio/util/FrameworkUtil.kt index 610ba4847..5611f6b61 100644 --- a/app/src/main/java/org/oxycblt/auxio/util/FrameworkUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/util/FrameworkUtil.kt @@ -14,13 +14,13 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - + 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? 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 Fragment.collect(stateFlow: StateFlow, 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 Fragment.collectImmediately(stateFlow: StateFlow, 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 Fragment.collectImmediately( a: StateFlow, b: StateFlow, 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 Fragment.collectImmediately( a: StateFlow, b: StateFlow, @@ -157,9 +192,12 @@ fun 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 Fragment.androidViewModels() = viewModels { 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 AppCompatActivity.androidViewModels() = viewModels { 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 Fragment.androidActivityViewModels() = activityViewModels { 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 SQLiteDatabase.queryAll(tableName: String, block: (Cursor) -> R) = +inline fun 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) } } diff --git a/app/src/main/java/org/oxycblt/auxio/util/PrimitiveUtil.kt b/app/src/main/java/org/oxycblt/auxio/util/LangUtil.kt similarity index 51% rename from app/src/main/java/org/oxycblt/auxio/util/PrimitiveUtil.kt rename to app/src/main/java/org/oxycblt/auxio/util/LangUtil.kt index 198921ef7..53f6f8ccf 100644 --- a/app/src/main/java/org/oxycblt/auxio/util/PrimitiveUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/util/LangUtil.kt @@ -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 unlikelyToBeNull(value: T?) = if (BuildConfig.DEBUG) { @@ -41,21 +33,52 @@ fun 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" + } +} diff --git a/app/src/main/java/org/oxycblt/auxio/util/LogUtil.kt b/app/src/main/java/org/oxycblt/auxio/util/LogUtil.kt index 4bd83c697..846cc2390 100644 --- a/app/src/main/java/org/oxycblt/auxio/util/LogUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/util/LogUtil.kt @@ -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 } diff --git a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetComponent.kt b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetComponent.kt index ae403a004..ebf14fc9f 100644 --- a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetComponent.kt +++ b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetComponent.kt @@ -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