util: add reflection utilities

Add lazyReflectedMethod and lazyReflectedField, utility methods that
make handling reflection a bit easier.

This is mostly a readability change.
This commit is contained in:
OxygenCobalt 2022-06-15 11:24:47 -06:00
parent 3d8d2e0975
commit 90976e318d
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
9 changed files with 64 additions and 23 deletions

View file

@ -30,8 +30,10 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.AppBarLayout
import java.lang.Exception import java.lang.Exception
import java.lang.reflect.Field
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.ui.EdgeAppBarLayout import org.oxycblt.auxio.ui.EdgeAppBarLayout
import org.oxycblt.auxio.util.lazyReflectedField
import org.oxycblt.auxio.util.logE import org.oxycblt.auxio.util.logE
import org.oxycblt.auxio.util.logTraceOrThrow 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 // Reflect to get the actual title view to do transformations on
val newTitleView = val newTitleView =
try { try {
Toolbar::class.java.getDeclaredField("mTitleTextView").run { TOOLBAR_TITLE_TEXT_FIELD.get(toolbar) as AppCompatTextView
isAccessible = true
get(toolbar) as AppCompatTextView
}
} catch (e: Exception) { } catch (e: Exception) {
logE("Could not get toolbar title view (likely an internal code change)") logE("Could not get toolbar title view (likely an internal code change)")
e.logTraceOrThrow() e.logTraceOrThrow()
@ -152,4 +151,8 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
appBar.setTitleVisibility(showTitle) appBar.setTitleVisibility(showTitle)
} }
} }
companion object {
private val TOOLBAR_TITLE_TEXT_FIELD: Field by lazyReflectedField<Toolbar>("mTitleTextView")
}
} }

View file

@ -35,6 +35,7 @@ import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.adapter.FragmentStateAdapter import androidx.viewpager2.adapter.FragmentStateAdapter
import androidx.viewpager2.widget.ViewPager2 import androidx.viewpager2.widget.ViewPager2
import com.google.android.material.tabs.TabLayoutMediator import com.google.android.material.tabs.TabLayoutMediator
import java.lang.reflect.Field
import kotlin.math.abs import kotlin.math.abs
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentHomeBinding 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.getColorStateListSafe
import org.oxycblt.auxio.util.getSystemBarInsetsCompat import org.oxycblt.auxio.util.getSystemBarInsetsCompat
import org.oxycblt.auxio.util.launch import org.oxycblt.auxio.util.launch
import org.oxycblt.auxio.util.lazyReflectedField
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logE import org.oxycblt.auxio.util.logE
import org.oxycblt.auxio.util.logTraceOrThrow import org.oxycblt.auxio.util.logTraceOrThrow
@ -424,4 +426,11 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
} }
} }
} }
companion object {
private val VIEW_PAGER_RECYCLER_FIELD: Field by
lazyReflectedField<ViewPager2>("mRecyclerView")
private val VIEW_PAGER_TOUCH_SLOP_FIELD: Field by
lazyReflectedField<ViewPager2>("mTouchSlop")
}
} }

View file

@ -31,7 +31,6 @@ import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.settings.SettingsManager import org.oxycblt.auxio.settings.SettingsManager
import org.oxycblt.auxio.util.getColorStateListSafe import org.oxycblt.auxio.util.getColorStateListSafe
import org.oxycblt.auxio.util.logD
/** /**
* Effectively a super-charged [StyledImageView]. * Effectively a super-charged [StyledImageView].

View file

@ -23,7 +23,6 @@ import android.database.Cursor
import android.net.Uri import android.net.Uri
import android.provider.MediaStore import android.provider.MediaStore
import androidx.core.text.isDigitsOnly import androidx.core.text.isDigitsOnly
import org.oxycblt.auxio.util.logD
/** Shortcut for making a [ContentResolver] query with less superfluous arguments. */ /** Shortcut for making a [ContentResolver] query with less superfluous arguments. */
fun ContentResolver.queryCursor( fun ContentResolver.queryCursor(

View file

@ -27,7 +27,9 @@ import android.provider.MediaStore
import android.webkit.MimeTypeMap import android.webkit.MimeTypeMap
import com.google.android.exoplayer2.util.MimeTypes import com.google.android.exoplayer2.util.MimeTypes
import java.io.File import java.io.File
import java.lang.reflect.Method
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.util.lazyReflectedMethod
import org.oxycblt.auxio.util.logEOrThrow import org.oxycblt.auxio.util.logEOrThrow
data class Path(val name: String, val parent: Directory) 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. */ /** Converts this dir into an opaque document URI in the form of VOLUME:PATH. */
fun toDocumentUri(): String? { fun toDocumentUri(): String? {
// "primary" actually corresponds to the primary *emulated* storage. External storage // "primary" actually corresponds to the internal storage, not the primary volume.
// can also be the primary storage, but is represented as a document ID using the UUID. // Removable storage is represented with the UUID.
return if (volume.isPrimaryCompat && volume.isEmulatedCompat) { return if (volume.isInternalCompat) {
"${DOCUMENT_URI_PRIMARY_NAME}:${relativePath}" "${DOCUMENT_URI_PRIMARY_NAME}:${relativePath}"
} else { } else {
"${(volume.uuidCompat ?: return null).uppercase()}:${relativePath}" "${(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<StorageManager>("getVolumeList")
private val SV_API21_GET_PATH_METHOD: Method by lazyReflectedMethod<StorageVolume>("getPath")
/** /**
* A list of recognized volumes, retrieved in a compatible manner. Note that these volumes may be * A list of recognized volumes, retrieved in a compatible manner. Note that these volumes may be
* mounted or unmounted. * mounted or unmounted.
@ -88,9 +94,7 @@ val StorageManager.storageVolumesCompat: List<StorageVolume>
storageVolumes.toList() storageVolumes.toList()
} else { } else {
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
(StorageManager::class.java.getDeclaredMethod("getVolumeList").invoke(this) (SM_API21_GET_VOLUME_LIST_METHOD.invoke(this) as Array<StorageVolume>).toList()
as Array<StorageVolume>)
.toList()
} }
/** Returns the absolute path to a particular volume in a compatible manner. */ /** Returns the absolute path to a particular volume in a compatible manner. */
@ -104,7 +108,7 @@ val StorageVolume.directoryCompat: String?
when (stateCompat) { when (stateCompat) {
Environment.MEDIA_MOUNTED, Environment.MEDIA_MOUNTED,
Environment.MEDIA_MOUNTED_READ_ONLY -> 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 else -> null
} }
} }
@ -121,6 +125,13 @@ val StorageVolume.isPrimaryCompat: Boolean
val StorageVolume.isEmulatedCompat: Boolean val StorageVolume.isEmulatedCompat: Boolean
@SuppressLint("NewApi") get() = isEmulated @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? val StorageVolume.uuidCompat: String?
@SuppressLint("NewApi") get() = uuid @SuppressLint("NewApi") get() = uuid

View file

@ -27,8 +27,7 @@ import android.util.Log
import androidx.core.content.edit import androidx.core.content.edit
import org.oxycblt.auxio.music.Directory import org.oxycblt.auxio.music.Directory
import org.oxycblt.auxio.music.directoryCompat import org.oxycblt.auxio.music.directoryCompat
import org.oxycblt.auxio.music.isEmulatedCompat import org.oxycblt.auxio.music.isInternalCompat
import org.oxycblt.auxio.music.isPrimaryCompat
import org.oxycblt.auxio.music.storageVolumesCompat import org.oxycblt.auxio.music.storageVolumesCompat
import org.oxycblt.auxio.ui.accent.Accent import org.oxycblt.auxio.ui.accent.Accent
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
@ -103,8 +102,7 @@ fun handleExcludedCompat(context: Context, storageManager: StorageManager): List
val db = LegacyExcludedDatabase(context) val db = LegacyExcludedDatabase(context)
// /storage/emulated/0 (the old path prefix) should correspond to primary *emulated* storage. // /storage/emulated/0 (the old path prefix) should correspond to primary *emulated* storage.
val primaryVolume = val primaryVolume =
storageManager.storageVolumesCompat.find { it.isPrimaryCompat && it.isEmulatedCompat } storageManager.storageVolumesCompat.find { it.isInternalCompat } ?: return emptyList()
?: return emptyList()
val primaryDirectory = primaryVolume.directoryCompat ?: return emptyList() val primaryDirectory = primaryVolume.directoryCompat ?: return emptyList()
return db.readPaths().map { path -> return db.readPaths().map { path ->
val relativePath = path.removePrefix(primaryDirectory) val relativePath = path.removePrefix(primaryDirectory)

View file

@ -22,7 +22,9 @@ import android.content.res.TypedArray
import android.util.AttributeSet import android.util.AttributeSet
import androidx.preference.DialogPreference import androidx.preference.DialogPreference
import androidx.preference.Preference import androidx.preference.Preference
import java.lang.reflect.Field
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.util.lazyReflectedField
class IntListPreference class IntListPreference
@JvmOverloads @JvmOverloads
@ -38,7 +40,7 @@ constructor(
// Reflect into Preference to get the (normally inaccessible) default value. // Reflect into Preference to get the (normally inaccessible) default value.
private val defValue: Int private val defValue: Int
get() = defValueField.get(this) as Int get() = PREFERENCE_DEFAULT_VALUE_FIELD.get(this) as Int
init { init {
val prefAttrs = val prefAttrs =
@ -108,7 +110,7 @@ constructor(
} }
companion object { companion object {
private val defValueField = private val PREFERENCE_DEFAULT_VALUE_FIELD: Field by
Preference::class.java.getDeclaredField("mDefaultValue").apply { isAccessible = true } lazyReflectedField<Preference>("mDefaultValue")
} }
} }

View file

@ -34,6 +34,7 @@ import android.widget.FrameLayout
import androidx.core.view.isInvisible import androidx.core.view.isInvisible
import androidx.customview.widget.ViewDragHelper import androidx.customview.widget.ViewDragHelper
import com.google.android.material.shape.MaterialShapeDrawable import com.google.android.material.shape.MaterialShapeDrawable
import java.lang.reflect.Field
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.max import kotlin.math.max
import kotlin.math.min 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.getDrawableSafe
import org.oxycblt.auxio.util.getSystemBarInsetsCompat import org.oxycblt.auxio.util.getSystemBarInsetsCompat
import org.oxycblt.auxio.util.isUnder import org.oxycblt.auxio.util.isUnder
import org.oxycblt.auxio.util.lazyReflectedField
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.pxOfDp import org.oxycblt.auxio.util.pxOfDp
import org.oxycblt.auxio.util.replaceSystemBarInsetsCompat 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. // want to vendor ViewDragHelper so I just do reflection instead.
val state = val state =
try { try {
DRAG_STATE_FIELD.get(this) VIEW_DRAG_HELPER_STATE_FIELD.get(this)
} catch (e: Exception) { } catch (e: Exception) {
ViewDragHelper.STATE_IDLE ViewDragHelper.STATE_IDLE
} }
@ -669,8 +671,8 @@ constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) :
companion object { companion object {
private val INIT_PANEL_STATE = PanelState.HIDDEN private val INIT_PANEL_STATE = PanelState.HIDDEN
private val DRAG_STATE_FIELD = private val VIEW_DRAG_HELPER_STATE_FIELD: Field by
ViewDragHelper::class.java.getDeclaredField("mDragState").apply { isAccessible = true } lazyReflectedField<ViewDragHelper>("mDragState")
private const val MIN_FLING_VEL = 400 private const val MIN_FLING_VEL = 400
private const val KEY_PANEL_STATE = BuildConfig.APPLICATION_ID + ".key.PANEL_STATE" private const val KEY_PANEL_STATE = BuildConfig.APPLICATION_ID + ".key.PANEL_STATE"

View file

@ -20,6 +20,8 @@ package org.oxycblt.auxio.util
import android.os.Looper import android.os.Looper
import android.text.format.DateUtils import android.text.format.DateUtils
import androidx.core.math.MathUtils import androidx.core.math.MathUtils
import java.lang.reflect.Field
import java.lang.reflect.Method
import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.BuildConfig
/** Assert that we are on a background thread. */ /** Assert that we are on a background thread. */
@ -64,3 +66,19 @@ fun Long.formatDuration(isElapsed: Boolean): String {
return durationString return durationString
} }
/** Lazily reflect to retrieve a [Field]. */
inline fun <reified T : Any> lazyReflectedField(field: String): Lazy<Field> = lazy {
T::class.java.getDeclaredField(field).also { it.isAccessible = true }
}
/** Lazily reflect to retrieve a [Method]. */
inline fun <reified T : Any> lazyReflectedMethod(
methodName: String,
vararg parameterTypes: Any
): Lazy<Method> = lazy {
T::class
.java
.getDeclaredMethod(methodName, *parameterTypes.map { it::class.java }.toTypedArray())
.also { it.isAccessible = true }
}