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:
parent
3d8d2e0975
commit
90976e318d
9 changed files with 64 additions and 23 deletions
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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].
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 }
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue