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 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<Toolbar>("mTitleTextView")
}
}

View file

@ -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<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.settings.SettingsManager
import org.oxycblt.auxio.util.getColorStateListSafe
import org.oxycblt.auxio.util.logD
/**
* Effectively a super-charged [StyledImageView].

View file

@ -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(

View file

@ -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<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
* mounted or unmounted.
@ -88,9 +94,7 @@ val StorageManager.storageVolumesCompat: List<StorageVolume>
storageVolumes.toList()
} else {
@Suppress("UNCHECKED_CAST")
(StorageManager::class.java.getDeclaredMethod("getVolumeList").invoke(this)
as Array<StorageVolume>)
.toList()
(SM_API21_GET_VOLUME_LIST_METHOD.invoke(this) as Array<StorageVolume>).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

View file

@ -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)

View file

@ -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<Preference>("mDefaultValue")
}
}

View file

@ -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<ViewDragHelper>("mDragState")
private const val MIN_FLING_VEL = 400
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.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 <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 }
}