diff --git a/CHANGELOG.md b/CHANGELOG.md index c5ed2bee1..70c754a45 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ - Added a new view for song properties (Such as Bitrate) - The playback bar now has a new design, with an improved progress indicator and a skip action -- When playing, covers now shows an animated indicator +- When playing, covers now show an indicator #### What's Improved - The toolbar in the home UI now collapses when scrolling 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 9a8bae0cb..3394d33c1 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailAppBarLayout.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailAppBarLayout.kt @@ -153,6 +153,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr } companion object { - private val TOOLBAR_TITLE_TEXT_FIELD: Field by lazyReflectedField("mTitleTextView") + private val TOOLBAR_TITLE_TEXT_FIELD: Field by + lazyReflectedField(Toolbar::class, "mTitleTextView") } } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/recycler/DetailAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/recycler/DetailAdapter.kt index 234b496d8..c3e1314ab 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/recycler/DetailAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/recycler/DetailAdapter.kt @@ -95,7 +95,6 @@ abstract class DetailAdapter( } } - // TODO: Pause indicator animation when not playing viewHolder.itemView.isActivated = shouldHighlightViewHolder(item) } diff --git a/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt index c10c726c7..e1785b6a3 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt @@ -388,18 +388,9 @@ class HomeFragment : ViewBindingFragment(), Toolbar.OnMenuI */ private fun ViewPager2.reduceSensitivity(by: Int) { try { - val recycler = - ViewPager2::class.java.getDeclaredField("mRecyclerView").run { - isAccessible = true - get(this@reduceSensitivity) - } - - RecyclerView::class.java.getDeclaredField("mTouchSlop").apply { - isAccessible = true - - val slop = get(recycler) as Int - set(recycler, slop * by) - } + val recycler = VIEW_PAGER_RECYCLER_FIELD.get(this@reduceSensitivity) + val slop = VIEW_PAGER_TOUCH_SLOP_FIELD.get(recycler) as Int + VIEW_PAGER_TOUCH_SLOP_FIELD.set(recycler, slop * by) } catch (e: Exception) { logE("Unable to reduce ViewPager sensitivity (likely an internal code change)") e.logTraceOrThrow() @@ -429,8 +420,8 @@ class HomeFragment : ViewBindingFragment(), Toolbar.OnMenuI companion object { private val VIEW_PAGER_RECYCLER_FIELD: Field by - lazyReflectedField("mRecyclerView") + lazyReflectedField(ViewPager2::class, "mRecyclerView") private val VIEW_PAGER_TOUCH_SLOP_FIELD: Field by - lazyReflectedField("mTouchSlop") + lazyReflectedField(RecyclerView::class, "mTouchSlop") } } 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 af7e9d8e1..905f0a3b9 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/ImageGroup.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/ImageGroup.kt @@ -31,12 +31,13 @@ import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.settings.SettingsManager import org.oxycblt.auxio.util.getColorStateListSafe +import org.oxycblt.auxio.util.getDrawableSafe /** * Effectively a super-charged [StyledImageView]. * * This class enables the following features alongside the base features pf [StyledImageView]: - * - Activation indicator with an animated icon + * - Activation indicator * - (Eventually) selection indicator * - Support for ONE custom view * @@ -53,7 +54,11 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr private val inner = BaseStyledImageView(context, attrs) private var customView: View? = null - private val indicator = ImageGroupIndicator(context) + private val indicator = + BaseStyledImageView(context).apply { + setImageDrawable( + StyledDrawable(context, context.getDrawableSafe(R.drawable.ic_equalizer))) + } init { // Android wants you to make separate attributes for each view type, but will diff --git a/app/src/main/java/org/oxycblt/auxio/image/ImageGroupIndicator.kt b/app/src/main/java/org/oxycblt/auxio/image/ImageGroupIndicator.kt deleted file mode 100644 index e8725b3b9..000000000 --- a/app/src/main/java/org/oxycblt/auxio/image/ImageGroupIndicator.kt +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright (c) 2022 Auxio Project - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.oxycblt.auxio.image - -import android.content.Context -import android.graphics.Matrix -import android.graphics.RectF -import android.graphics.drawable.AnimationDrawable -import android.util.AttributeSet -import androidx.annotation.AttrRes -import androidx.appcompat.widget.AppCompatImageView -import com.google.android.material.shape.MaterialShapeDrawable -import org.oxycblt.auxio.R -import org.oxycblt.auxio.util.getColorStateListSafe - -/** - * Represents the animated indicator that is shown when [ImageGroup] is active. - * - * AnimationDrawable, the drawable that this view is backed by, is really finicky. Basically, it has - * to be set as the drawable of an ImageView to work correctly, and will just not draw anywhere - * else. As a result, we have to create a custom view that emulates [StyledImageView] and - * [StyledDrawable] simultaneously while also managing the animation state. - * - * @author OxygenCobalt - */ -class ImageGroupIndicator -@JvmOverloads -constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) : - AppCompatImageView(context, attrs, defStyleAttr) { - private val centerMatrix = Matrix() - private val matrixSrc = RectF() - private val matrixDst = RectF() - - init { - scaleType = ScaleType.MATRIX - setImageResource(R.drawable.ic_animated_equalizer) - imageTintList = context.getColorStateListSafe(R.color.sel_on_cover_bg) - background = - MaterialShapeDrawable().apply { - fillColor = context.getColorStateListSafe(R.color.sel_cover_bg) - } - } - - override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { - super.onMeasure(widthMeasureSpec, heightMeasureSpec) - - // Instead of using StyledDrawable (which would break the animation), we scale - // up the animated icon using a matrix. This is okay, as it won't fall victim to - // the same issues that come with using a matrix in StyledImageView - imageMatrix = - centerMatrix.apply { - reset() - drawable?.let { drawable -> - // Android is too good to allow us to set a fixed image size, so we instead need - // to define a matrix to scale an image directly. - - val iconWidth = measuredWidth / 2f - val iconHeight = measuredHeight / 2f - - // First scale the icon up to the desired size. - matrixSrc.set( - 0f, - 0f, - drawable.intrinsicWidth.toFloat(), - drawable.intrinsicHeight.toFloat()) - matrixDst.set(0f, 0f, iconWidth, iconHeight) - centerMatrix.setRectToRect(matrixSrc, matrixDst, Matrix.ScaleToFit.CENTER) - - // Then actually center it into the icon, which the previous call does not - // actually do. - centerMatrix.postTranslate( - (measuredWidth - iconWidth) / 2f, (measuredHeight - iconHeight) / 2f) - } - } - } - - override fun setActivated(activated: Boolean) { - super.setActivated(activated) - val icon = drawable as AnimationDrawable - if (activated) { - icon.start() - } else { - icon.stop() - } - } -} diff --git a/app/src/main/java/org/oxycblt/auxio/music/StorageFramework.kt b/app/src/main/java/org/oxycblt/auxio/music/StorageFramework.kt index 81a1f72f2..cad73113c 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/StorageFramework.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/StorageFramework.kt @@ -81,8 +81,8 @@ data class Directory(val volume: StorageVolume, val relativePath: String) { } private val SM_API21_GET_VOLUME_LIST_METHOD: Method by - lazyReflectedMethod("getVolumeList") -private val SV_API21_GET_PATH_METHOD: Method by lazyReflectedMethod("getPath") + lazyReflectedMethod(StorageManager::class, "getVolumeList") +private val SV_API21_GET_PATH_METHOD: Method by lazyReflectedMethod(StorageVolume::class, "getPath") /** * A list of recognized volumes, retrieved in a compatible manner. Note that these volumes may be @@ -132,12 +132,21 @@ val StorageVolume.isEmulatedCompat: Boolean val StorageVolume.isInternalCompat: Boolean get() = isPrimaryCompat && isEmulatedCompat +/** Returns the UUID of the volume in a compatible manner. */ val StorageVolume.uuidCompat: String? @SuppressLint("NewApi") get() = uuid +/* + * Returns the state of the volume in a compatible manner. + */ val StorageVolume.stateCompat: String @SuppressLint("NewApi") get() = state +/** + * Returns the name of this volume as it is used in [MediaStore]. This will be + * [MediaStore.VOLUME_EXTERNAL_PRIMARY] if it is the primary volume, and the lowercase UUID of the + * volume otherwise. + */ val StorageVolume.mediaStoreVolumeNameCompat: String? get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { diff --git a/app/src/main/java/org/oxycblt/auxio/music/backend/ExoPlayerBackend.kt b/app/src/main/java/org/oxycblt/auxio/music/backend/ExoPlayerBackend.kt index f5ed0e372..0af14cb9d 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/backend/ExoPlayerBackend.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/backend/ExoPlayerBackend.kt @@ -122,7 +122,6 @@ class ExoPlayerBackend(private val inner: MediaStoreBackend) : Indexer.Backend { /** * Wraps an ExoPlayer metadata retrieval task in a safe abstraction. Access is done with [get]. - * * @author OxygenCobalt */ class Task(context: Context, private val audio: MediaStoreBackend.Audio) { diff --git a/app/src/main/java/org/oxycblt/auxio/music/backend/MediaStoreBackend.kt b/app/src/main/java/org/oxycblt/auxio/music/backend/MediaStoreBackend.kt index 275c3c3e7..02629b1c4 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/backend/MediaStoreBackend.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/backend/MediaStoreBackend.kt @@ -160,7 +160,7 @@ abstract class MediaStoreBackend : Indexer.Backend { selector += ')' } - logD("Starting query [selector: $selector, args: $args]") + logD("Starting query [proj: ${projection.map { it }}, selector: $selector, args: $args]") return requireNotNull( context.contentResolverSafe.queryCursor( diff --git a/app/src/main/java/org/oxycblt/auxio/settings/ui/IntListPreference.kt b/app/src/main/java/org/oxycblt/auxio/settings/ui/IntListPreference.kt index 942385925..d96461ad4 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/ui/IntListPreference.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/ui/IntListPreference.kt @@ -111,6 +111,6 @@ constructor( companion object { private val PREFERENCE_DEFAULT_VALUE_FIELD: Field by - lazyReflectedField("mDefaultValue") + lazyReflectedField(Preference::class, "mDefaultValue") } } diff --git a/app/src/main/java/org/oxycblt/auxio/ui/BottomSheetLayout.kt b/app/src/main/java/org/oxycblt/auxio/ui/BottomSheetLayout.kt index 02d0aebdd..4559ed93a 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/BottomSheetLayout.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/BottomSheetLayout.kt @@ -387,7 +387,6 @@ constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) : // not apply any window insets at all, which results in scroll desynchronization on // certain views. This is considered tolerable as the other options are to convert // the playback fragments to views, which is not nice. - logD("Readjusting window insets") val bars = insets.getSystemBarInsetsCompat(this) val consumedByPanel = computePanelTopPosition(panelOffset) - measuredHeight val adjustedBottomInset = (consumedByPanel + bars.bottom).coerceAtLeast(0) @@ -500,7 +499,6 @@ constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) : private fun setPanelStateInternal(state: PanelState) { if (panelState == state) { - logD("State is already $state, not applying") return } @@ -672,7 +670,7 @@ constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) : companion object { private val INIT_PANEL_STATE = PanelState.HIDDEN private val VIEW_DRAG_HELPER_STATE_FIELD: Field by - lazyReflectedField("mDragState") + lazyReflectedField(ViewDragHelper::class, "mDragState") private const val MIN_FLING_VEL = 400 private const val KEY_PANEL_STATE = BuildConfig.APPLICATION_ID + ".key.PANEL_STATE" diff --git a/app/src/main/java/org/oxycblt/auxio/ui/MenuFragment.kt b/app/src/main/java/org/oxycblt/auxio/ui/MenuFragment.kt index 208ab28a5..ce53bcd00 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/MenuFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/MenuFragment.kt @@ -33,12 +33,21 @@ import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logEOrThrow import org.oxycblt.auxio.util.showToast +/** + * A fragment capable of creating menus. Automatically keeps track of and disposes of menus, + * preventing UI issues and memory leaks. + * @author OxygenCobalt + */ abstract class MenuFragment : ViewBindingFragment() { private var currentMenu: PopupMenu? = null protected val playbackModel: PlaybackViewModel by activityViewModels() protected val navModel: NavigationViewModel by activityViewModels() + /** + * Opens the given menu in context of [song]. Assumes that the menu is only composed of common + * [Song] options. + */ protected fun musicMenu(anchor: View, @MenuRes menuRes: Int, song: Song) { logD("Launching new song menu: ${song.rawName}") @@ -71,6 +80,10 @@ abstract class MenuFragment : ViewBindingFragment() { } } + /** + * Opens the given menu in context of [album]. Assumes that the menu is only composed of common + * [Album] options. + */ protected fun musicMenu(anchor: View, @MenuRes menuRes: Int, album: Album) { logD("Launching new album menu: ${album.rawName}") @@ -103,6 +116,10 @@ abstract class MenuFragment : ViewBindingFragment() { } } + /** + * Opens the given menu in context of [artist]. Assumes that the menu is only composed of common + * [Artist] options. + */ protected fun musicMenu(anchor: View, @MenuRes menuRes: Int, artist: Artist) { logD("Launching new artist menu: ${artist.rawName}") @@ -132,6 +149,10 @@ abstract class MenuFragment : ViewBindingFragment() { } } + /** + * Opens the given menu in context of [genre]. Assumes that the menu is only composed of common + * [Genre] options. + */ protected fun musicMenu(anchor: View, @MenuRes menuRes: Int, genre: Genre) { logD("Launching new genre menu: ${genre.rawName}") @@ -173,6 +194,10 @@ abstract class MenuFragment : ViewBindingFragment() { } } + /** + * Open a generic menu with configuration in [block]. If a menu is already opened, then this + * function is a no-op. + */ protected fun menu(anchor: View, @MenuRes menuRes: Int, block: PopupMenu.() -> Unit) { if (currentMenu != null) { logD("Menu already present, not launching") diff --git a/app/src/main/java/org/oxycblt/auxio/util/PrimitiveUtil.kt b/app/src/main/java/org/oxycblt/auxio/util/PrimitiveUtil.kt index f2d708eac..d26ae2b3b 100644 --- a/app/src/main/java/org/oxycblt/auxio/util/PrimitiveUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/util/PrimitiveUtil.kt @@ -22,6 +22,7 @@ import android.text.format.DateUtils import androidx.core.math.MathUtils 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. */ @@ -68,17 +69,11 @@ fun Long.formatDuration(isElapsed: Boolean): String { } /** Lazily reflect to retrieve a [Field]. */ -inline fun lazyReflectedField(field: String): Lazy = lazy { - T::class.java.getDeclaredField(field).also { it.isAccessible = true } +fun lazyReflectedField(clazz: KClass<*>, field: String) = lazy { + clazz.java.getDeclaredField(field).also { it.isAccessible = true } } /** Lazily reflect to retrieve a [Method]. */ -inline fun lazyReflectedMethod( - methodName: String, - vararg parameterTypes: Any -): Lazy = lazy { - T::class - .java - .getDeclaredMethod(methodName, *parameterTypes.map { it::class.java }.toTypedArray()) - .also { it.isAccessible = true } +fun lazyReflectedMethod(clazz: KClass<*>, method: String) = lazy { + clazz.java.getDeclaredMethod(method).also { it.isAccessible = true } } diff --git a/app/src/main/res/drawable/ic_animated_equalizer.xml b/app/src/main/res/drawable/ic_animated_equalizer.xml deleted file mode 100644 index a6531fd52..000000000 --- a/app/src/main/res/drawable/ic_animated_equalizer.xml +++ /dev/null @@ -1,491 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/ic_equalizer.xml b/app/src/main/res/drawable/ic_equalizer.xml new file mode 100644 index 000000000..c131a0cea --- /dev/null +++ b/app/src/main/res/drawable/ic_equalizer.xml @@ -0,0 +1,13 @@ + + + + +