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 c39cb1682..9a8bae0cb 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailAppBarLayout.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailAppBarLayout.kt @@ -30,8 +30,10 @@ import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.google.android.material.appbar.AppBarLayout import java.lang.Exception +import java.lang.reflect.Field import org.oxycblt.auxio.R import org.oxycblt.auxio.ui.EdgeAppBarLayout +import org.oxycblt.auxio.util.lazyReflectedField import org.oxycblt.auxio.util.logE import org.oxycblt.auxio.util.logTraceOrThrow @@ -68,10 +70,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr // Reflect to get the actual title view to do transformations on val newTitleView = try { - Toolbar::class.java.getDeclaredField("mTitleTextView").run { - isAccessible = true - get(toolbar) as AppCompatTextView - } + TOOLBAR_TITLE_TEXT_FIELD.get(toolbar) as AppCompatTextView } catch (e: Exception) { logE("Could not get toolbar title view (likely an internal code change)") e.logTraceOrThrow() @@ -152,4 +151,8 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr appBar.setTitleVisibility(showTitle) } } + + companion object { + private val TOOLBAR_TITLE_TEXT_FIELD: Field by lazyReflectedField("mTitleTextView") + } } 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 11ec8e0b8..c10c726c7 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt @@ -35,6 +35,7 @@ import androidx.recyclerview.widget.RecyclerView import androidx.viewpager2.adapter.FragmentStateAdapter import androidx.viewpager2.widget.ViewPager2 import com.google.android.material.tabs.TabLayoutMediator +import java.lang.reflect.Field import kotlin.math.abs import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.FragmentHomeBinding @@ -57,6 +58,7 @@ import org.oxycblt.auxio.ui.ViewBindingFragment import org.oxycblt.auxio.util.getColorStateListSafe import org.oxycblt.auxio.util.getSystemBarInsetsCompat import org.oxycblt.auxio.util.launch +import org.oxycblt.auxio.util.lazyReflectedField import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logE import org.oxycblt.auxio.util.logTraceOrThrow @@ -424,4 +426,11 @@ class HomeFragment : ViewBindingFragment(), Toolbar.OnMenuI } } } + + companion object { + private val VIEW_PAGER_RECYCLER_FIELD: Field by + lazyReflectedField("mRecyclerView") + private val VIEW_PAGER_TOUCH_SLOP_FIELD: Field by + lazyReflectedField("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 3c36fcac5..af7e9d8e1 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/ImageGroup.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/ImageGroup.kt @@ -31,7 +31,6 @@ 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.logD /** * Effectively a super-charged [StyledImageView]. diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicUtil.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicUtil.kt index 75c91765c..bb6a907ef 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicUtil.kt @@ -23,7 +23,6 @@ import android.database.Cursor import android.net.Uri import android.provider.MediaStore import androidx.core.text.isDigitsOnly -import org.oxycblt.auxio.util.logD /** Shortcut for making a [ContentResolver] query with less superfluous arguments. */ fun ContentResolver.queryCursor( 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 d570caffe..81a1f72f2 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/StorageFramework.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/StorageFramework.kt @@ -27,7 +27,9 @@ import android.provider.MediaStore import android.webkit.MimeTypeMap import com.google.android.exoplayer2.util.MimeTypes import java.io.File +import java.lang.reflect.Method import org.oxycblt.auxio.R +import org.oxycblt.auxio.util.lazyReflectedMethod import org.oxycblt.auxio.util.logEOrThrow data class Path(val name: String, val parent: Directory) @@ -45,9 +47,9 @@ data class Directory(val volume: StorageVolume, val relativePath: String) { /** Converts this dir into an opaque document URI in the form of VOLUME:PATH. */ fun toDocumentUri(): String? { - // "primary" actually corresponds to the primary *emulated* storage. External storage - // can also be the primary storage, but is represented as a document ID using the UUID. - return if (volume.isPrimaryCompat && volume.isEmulatedCompat) { + // "primary" actually corresponds to the internal storage, not the primary volume. + // Removable storage is represented with the UUID. + return if (volume.isInternalCompat) { "${DOCUMENT_URI_PRIMARY_NAME}:${relativePath}" } else { "${(volume.uuidCompat ?: return null).uppercase()}:${relativePath}" @@ -78,6 +80,10 @@ 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") + /** * A list of recognized volumes, retrieved in a compatible manner. Note that these volumes may be * mounted or unmounted. @@ -88,9 +94,7 @@ val StorageManager.storageVolumesCompat: List storageVolumes.toList() } else { @Suppress("UNCHECKED_CAST") - (StorageManager::class.java.getDeclaredMethod("getVolumeList").invoke(this) - as Array) - .toList() + (SM_API21_GET_VOLUME_LIST_METHOD.invoke(this) as Array).toList() } /** Returns the absolute path to a particular volume in a compatible manner. */ @@ -104,7 +108,7 @@ val StorageVolume.directoryCompat: String? when (stateCompat) { Environment.MEDIA_MOUNTED, Environment.MEDIA_MOUNTED_READ_ONLY -> - StorageVolume::class.java.getDeclaredMethod("getPath").invoke(this) as String + SV_API21_GET_PATH_METHOD.invoke(this) as String else -> null } } @@ -121,6 +125,13 @@ val StorageVolume.isPrimaryCompat: Boolean val StorageVolume.isEmulatedCompat: Boolean @SuppressLint("NewApi") get() = isEmulated +/** + * If this volume corresponds to "Internal shared storage", represented in document URIs as + * "primary". These volumes are primary volumes, but are also non-removable and emulated. + */ +val StorageVolume.isInternalCompat: Boolean + get() = isPrimaryCompat && isEmulatedCompat + val StorageVolume.uuidCompat: String? @SuppressLint("NewApi") get() = uuid diff --git a/app/src/main/java/org/oxycblt/auxio/settings/SettingsCompat.kt b/app/src/main/java/org/oxycblt/auxio/settings/SettingsCompat.kt index a804bbf3d..c1c2eed06 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/SettingsCompat.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/SettingsCompat.kt @@ -27,8 +27,7 @@ import android.util.Log import androidx.core.content.edit import org.oxycblt.auxio.music.Directory import org.oxycblt.auxio.music.directoryCompat -import org.oxycblt.auxio.music.isEmulatedCompat -import org.oxycblt.auxio.music.isPrimaryCompat +import org.oxycblt.auxio.music.isInternalCompat import org.oxycblt.auxio.music.storageVolumesCompat import org.oxycblt.auxio.ui.accent.Accent import org.oxycblt.auxio.util.logD @@ -103,8 +102,7 @@ fun handleExcludedCompat(context: Context, storageManager: StorageManager): List val db = LegacyExcludedDatabase(context) // /storage/emulated/0 (the old path prefix) should correspond to primary *emulated* storage. val primaryVolume = - storageManager.storageVolumesCompat.find { it.isPrimaryCompat && it.isEmulatedCompat } - ?: return emptyList() + storageManager.storageVolumesCompat.find { it.isInternalCompat } ?: return emptyList() val primaryDirectory = primaryVolume.directoryCompat ?: return emptyList() return db.readPaths().map { path -> val relativePath = path.removePrefix(primaryDirectory) 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 f0a0fa23d..942385925 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 @@ -22,7 +22,9 @@ import android.content.res.TypedArray import android.util.AttributeSet import androidx.preference.DialogPreference import androidx.preference.Preference +import java.lang.reflect.Field import org.oxycblt.auxio.R +import org.oxycblt.auxio.util.lazyReflectedField class IntListPreference @JvmOverloads @@ -38,7 +40,7 @@ constructor( // Reflect into Preference to get the (normally inaccessible) default value. private val defValue: Int - get() = defValueField.get(this) as Int + get() = PREFERENCE_DEFAULT_VALUE_FIELD.get(this) as Int init { val prefAttrs = @@ -108,7 +110,7 @@ constructor( } companion object { - private val defValueField = - Preference::class.java.getDeclaredField("mDefaultValue").apply { isAccessible = true } + private val PREFERENCE_DEFAULT_VALUE_FIELD: Field by + lazyReflectedField("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 433f06e28..02d0aebdd 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/BottomSheetLayout.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/BottomSheetLayout.kt @@ -34,6 +34,7 @@ import android.widget.FrameLayout import androidx.core.view.isInvisible import androidx.customview.widget.ViewDragHelper import com.google.android.material.shape.MaterialShapeDrawable +import java.lang.reflect.Field import kotlin.math.abs import kotlin.math.max import kotlin.math.min @@ -45,6 +46,7 @@ import org.oxycblt.auxio.util.getDimenSafe import org.oxycblt.auxio.util.getDrawableSafe import org.oxycblt.auxio.util.getSystemBarInsetsCompat import org.oxycblt.auxio.util.isUnder +import org.oxycblt.auxio.util.lazyReflectedField import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.pxOfDp import org.oxycblt.auxio.util.replaceSystemBarInsetsCompat @@ -488,7 +490,7 @@ constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) : // want to vendor ViewDragHelper so I just do reflection instead. val state = try { - DRAG_STATE_FIELD.get(this) + VIEW_DRAG_HELPER_STATE_FIELD.get(this) } catch (e: Exception) { ViewDragHelper.STATE_IDLE } @@ -669,8 +671,8 @@ constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) : companion object { private val INIT_PANEL_STATE = PanelState.HIDDEN - private val DRAG_STATE_FIELD = - ViewDragHelper::class.java.getDeclaredField("mDragState").apply { isAccessible = true } + private val VIEW_DRAG_HELPER_STATE_FIELD: Field by + lazyReflectedField("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/util/PrimitiveUtil.kt b/app/src/main/java/org/oxycblt/auxio/util/PrimitiveUtil.kt index c8e0ea91a..f2d708eac 100644 --- a/app/src/main/java/org/oxycblt/auxio/util/PrimitiveUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/util/PrimitiveUtil.kt @@ -20,6 +20,8 @@ package org.oxycblt.auxio.util import android.os.Looper import android.text.format.DateUtils import androidx.core.math.MathUtils +import java.lang.reflect.Field +import java.lang.reflect.Method import org.oxycblt.auxio.BuildConfig /** Assert that we are on a background thread. */ @@ -64,3 +66,19 @@ fun Long.formatDuration(isElapsed: Boolean): String { return durationString } + +/** Lazily reflect to retrieve a [Field]. */ +inline fun lazyReflectedField(field: String): Lazy = lazy { + T::class.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 } +}