music: move utils around

Move some miscellanious utils around.
This commit is contained in:
Alexander Capehart 2022-09-08 19:01:21 -06:00
parent 4c954e83b0
commit 2e71342e1c
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
17 changed files with 97 additions and 104 deletions

View file

@ -29,7 +29,7 @@ import org.oxycblt.auxio.databinding.DialogSongDetailBinding
import org.oxycblt.auxio.ui.fragment.ViewBindingDialogFragment
import org.oxycblt.auxio.util.androidActivityViewModels
import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.formatDurationMs
import org.oxycblt.auxio.music.formatDurationMs
/**
* A dialog displayed when "View properties" is selected on a song, showing more information about

View file

@ -34,7 +34,7 @@ import org.oxycblt.auxio.ui.recycler.Item
import org.oxycblt.auxio.ui.recycler.MenuItemListener
import org.oxycblt.auxio.ui.recycler.SimpleItemCallback
import org.oxycblt.auxio.util.context
import org.oxycblt.auxio.util.formatDurationMs
import org.oxycblt.auxio.music.formatDurationMs
import org.oxycblt.auxio.util.getPlural
import org.oxycblt.auxio.util.inflater

View file

@ -29,7 +29,7 @@ import org.oxycblt.auxio.ui.recycler.Item
import org.oxycblt.auxio.ui.recycler.SimpleItemCallback
import org.oxycblt.auxio.ui.recycler.SongViewHolder
import org.oxycblt.auxio.util.context
import org.oxycblt.auxio.util.formatDurationMs
import org.oxycblt.auxio.music.formatDurationMs
import org.oxycblt.auxio.util.getPlural
import org.oxycblt.auxio.util.inflater

View file

@ -34,8 +34,8 @@ import org.oxycblt.auxio.ui.recycler.Item
import org.oxycblt.auxio.ui.recycler.MenuItemListener
import org.oxycblt.auxio.ui.recycler.SyncListDiffer
import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.formatDurationMs
import org.oxycblt.auxio.util.secsToMs
import org.oxycblt.auxio.music.formatDurationMs
import org.oxycblt.auxio.music.secsToMs
/**
* A [HomeListFragment] for showing a list of [Album]s.

View file

@ -32,7 +32,7 @@ import org.oxycblt.auxio.ui.recycler.Item
import org.oxycblt.auxio.ui.recycler.MenuItemListener
import org.oxycblt.auxio.ui.recycler.SyncListDiffer
import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.formatDurationMs
import org.oxycblt.auxio.music.formatDurationMs
/**
* A [HomeListFragment] for showing a list of [Artist]s.

View file

@ -32,7 +32,7 @@ import org.oxycblt.auxio.ui.recycler.Item
import org.oxycblt.auxio.ui.recycler.MenuItemListener
import org.oxycblt.auxio.ui.recycler.SyncListDiffer
import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.formatDurationMs
import org.oxycblt.auxio.music.formatDurationMs
/**
* A [HomeListFragment] for showing a list of [Genre]s.

View file

@ -36,8 +36,8 @@ import org.oxycblt.auxio.ui.recycler.SongViewHolder
import org.oxycblt.auxio.ui.recycler.SyncListDiffer
import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.context
import org.oxycblt.auxio.util.formatDurationMs
import org.oxycblt.auxio.util.secsToMs
import org.oxycblt.auxio.music.formatDurationMs
import org.oxycblt.auxio.music.secsToMs
/**
* A [HomeListFragment] for showing a list of [Song]s.

View file

@ -23,18 +23,12 @@ import android.content.Context
import android.database.Cursor
import android.net.Uri
import android.provider.MediaStore
import android.text.format.DateUtils
import androidx.core.text.isDigitsOnly
import org.oxycblt.auxio.R
import org.oxycblt.auxio.util.nonZeroOrNull
import org.oxycblt.auxio.util.logD
import java.util.UUID
/** Shortcut to resolve a year from a nullable date. Will return "No Date" if it is null. */
fun Date?.resolveYear(context: Context) =
this?.resolveYear(context) ?: context.getString(R.string.def_date)
/** Converts this string to a UUID, or returns null if it is not valid. */
fun String.toUuid() = try { UUID.fromString(this) } catch (e: IllegalArgumentException) { null }
/** Shortcut for making a [ContentResolver] query with less superfluous arguments. */
fun ContentResolver.queryCursor(
uri: Uri,
@ -65,3 +59,61 @@ val Long.audioUri: Uri
/** Converts a [Long] Album ID into a URI pointing to MediaStore-cached album art. */
val Long.albumCoverUri: Uri
get() = ContentUris.withAppendedId(EXTERNAL_ALBUM_ART_URI, this)
/** Shortcut to resolve a year from a nullable date. Will return "No Date" if it is null. */
fun Date?.resolveYear(context: Context) =
this?.resolveYear(context) ?: context.getString(R.string.def_date)
/** Converts this string to a UUID, or returns null if it is not valid. */
fun String.toUuid() = try { UUID.fromString(this) } catch (e: IllegalArgumentException) { null }
/** Converts a long in milliseconds to a long in deci-seconds */
fun Long.msToDs() = floorDiv(100)
/** Converts a long in milliseconds to a long in seconds */
fun Long.msToSecs() = floorDiv(1000)
/** Converts a long in deci-seconds to a long in milliseconds. */
fun Long.dsToMs() = times(100)
/** Converts a long in deci-seconds to a long in seconds. */
fun Long.dsToSecs() = floorDiv(10)
/** Converts a long in seconds to a long in milliseconds. */
fun Long.secsToMs() = times(1000)
/**
* Convert a [Long] of milliseconds into a string duration.
* @param isElapsed Whether this duration is represents elapsed time. If this is false, then --:--
* will be returned if the second value is 0.
*/
fun Long.formatDurationMs(isElapsed: Boolean) = msToSecs().formatDurationSecs(isElapsed)
/**
* Convert a [Long] of deci-seconds into a string duration.
* @param isElapsed Whether this duration is represents elapsed time. If this is false, then --:--
* will be returned if the second value is 0.
*/
fun Long.formatDurationDs(isElapsed: Boolean) = dsToSecs().formatDurationSecs(isElapsed)
/**
* Convert a [Long] of seconds into a string duration.
* @param isElapsed Whether this duration is represents elapsed time. If this is false, then --:--
* will be returned if the second value is 0.
*/
fun Long.formatDurationSecs(isElapsed: Boolean): String {
if (!isElapsed && this == 0L) {
logD("Non-elapsed duration is zero, using --:--")
return "--:--"
}
var durationString = DateUtils.formatElapsedTime(this)
// If the duration begins with a excess zero [e.g 01:42], then cut it off.
if (durationString[0] == '0') {
durationString = durationString.slice(1 until durationString.length)
}
return durationString
}

View file

@ -45,15 +45,15 @@ import org.oxycblt.auxio.util.logW
*
* @author OxygenCobalt
*/
class ExoPlayerBackend(private val inner: MediaStoreBackend) : Indexer.Backend {
class ExoPlayerBackend(private val context: Context, private val inner: MediaStoreBackend) : Indexer.Backend {
private val settings = Settings(context)
private val taskPool: Array<Task?> = arrayOfNulls(TASK_CAPACITY)
// No need to implement our own query logic, as this backend is still reliant on
// MediaStore.
override fun query(context: Context) = inner.query(context)
override fun query() = inner.query()
override fun buildSongs(
context: Context,
cursor: Cursor,
emitIndexing: (Indexer.Indexing) -> Unit
): List<Song> {
@ -82,11 +82,11 @@ class ExoPlayerBackend(private val inner: MediaStoreBackend) : Indexer.Backend {
if (song != null) {
songs.add(song)
emitIndexing(Indexer.Indexing.Songs(songs.size, total))
taskPool[i] = Task(context, raw)
taskPool[i] = Task(context, settings, raw)
break@spin
}
} else {
taskPool[i] = Task(context, raw)
taskPool[i] = Task(context, settings, raw)
break@spin
}
}
@ -122,8 +122,7 @@ 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 raw: Song.Raw) {
private val settings = Settings(context)
class Task(context: Context, private val settings: Settings, private val raw: Song.Raw) {
private val future =
MetadataRetriever.retrieveMetadata(
context,

View file

@ -128,8 +128,6 @@ class Indexer {
* complete, a new completion state will be pushed to each callback.
*/
suspend fun index(context: Context) {
requireBackgroundThread()
val handle = guard.newHandle()
val notGranted =
@ -203,20 +201,20 @@ class Indexer {
val mediaStoreBackend =
when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> Api30MediaStoreBackend()
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> Api29MediaStoreBackend()
else -> Api21MediaStoreBackend()
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> Api30MediaStoreBackend(context)
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> Api29MediaStoreBackend(context)
else -> Api21MediaStoreBackend(context)
}
val settings = Settings(context)
val backend =
if (settings.useQualityTags) {
ExoPlayerBackend(mediaStoreBackend)
ExoPlayerBackend(context, mediaStoreBackend)
} else {
mediaStoreBackend
}
val songs = buildSongs(context, backend, handle)
val songs = buildSongs(backend, handle)
if (songs.isEmpty()) {
return null
}
@ -243,16 +241,16 @@ class Indexer {
* [buildGenres] functions must be called with the returned list so that all songs are properly
* linked up.
*/
private fun buildSongs(context: Context, backend: Backend, handle: Long): List<Song> {
private fun buildSongs(backend: Backend, handle: Long): List<Song> {
val start = System.currentTimeMillis()
var songs =
backend.query(context).use { cursor ->
backend.query().use { cursor ->
logD(
"Successfully queried media database " +
"in ${System.currentTimeMillis() - start}ms")
backend.buildSongs(context, cursor) { emitIndexing(it, handle) }
backend.buildSongs(cursor) { emitIndexing(it, handle) }
}
// Deduplicate songs to prevent (most) deformed music clones
@ -425,11 +423,10 @@ class Indexer {
/** Represents a backend that metadata can be extracted from. */
interface Backend {
/** Query the media database for a basic cursor. */
fun query(context: Context): Cursor
fun query(): Cursor
/** Create a list of songs from the [Cursor] queried in [query]. */
fun buildSongs(
context: Context,
cursor: Cursor,
emitIndexing: (Indexing) -> Unit
): List<Song>

View file

@ -91,7 +91,6 @@ import org.oxycblt.auxio.util.logD
* I wish I was born in the neolithic.
*/
// TODO: Make context a member var to cache Settings
// TODO: Move duration util to MusicUtil
/**
@ -99,7 +98,7 @@ import org.oxycblt.auxio.util.logD
* not a fully-featured class by itself, and it's API-specific derivatives should be used instead.
* @author OxygenCobalt
*/
abstract class MediaStoreBackend : Indexer.Backend {
abstract class MediaStoreBackend(private val context: Context) : Indexer.Backend {
private var idIndex = -1
private var titleIndex = -1
private var displayNameIndex = -1
@ -113,10 +112,10 @@ abstract class MediaStoreBackend : Indexer.Backend {
private var artistIndex = -1
private var albumArtistIndex = -1
private val settings = Settings(context)
protected val volumes = mutableListOf<StorageVolume>()
override fun query(context: Context): Cursor {
val settings = Settings(context)
override fun query(): Cursor {
val storageManager = context.getSystemServiceCompat(StorageManager::class)
volumes.addAll(storageManager.storageVolumesCompat)
val dirs = settings.getMusicDirs(storageManager)
@ -162,12 +161,9 @@ abstract class MediaStoreBackend : Indexer.Backend {
}
override fun buildSongs(
context: Context,
cursor: Cursor,
emitIndexing: (Indexer.Indexing) -> Unit
): List<Song> {
val settings = Settings(context)
val rawSongs = mutableListOf<Song.Raw>()
while (cursor.moveToNext()) {
rawSongs.add(buildRawSong(context, cursor))
@ -255,8 +251,6 @@ abstract class MediaStoreBackend : Indexer.Backend {
* outlined in [projection].
*/
open fun buildRawSong(context: Context, cursor: Cursor): Song.Raw {
val settings = Settings(context)
// Initialize our cursor indices if we haven't already.
if (idIndex == -1) {
idIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns._ID)
@ -349,7 +343,7 @@ abstract class MediaStoreBackend : Indexer.Backend {
* A [MediaStoreBackend] that completes the music loading process in a way compatible from
* @author OxygenCobalt
*/
class Api21MediaStoreBackend : MediaStoreBackend() {
class Api21MediaStoreBackend(context: Context) : MediaStoreBackend(context) {
private var trackIndex = -1
private var dataIndex = -1
@ -414,7 +408,7 @@ class Api21MediaStoreBackend : MediaStoreBackend() {
* @author OxygenCobalt
*/
@RequiresApi(Build.VERSION_CODES.Q)
open class BaseApi29MediaStoreBackend : MediaStoreBackend() {
open class BaseApi29MediaStoreBackend(context: Context) : MediaStoreBackend(context) {
private var volumeIndex = -1
private var relativePathIndex = -1
@ -466,7 +460,7 @@ open class BaseApi29MediaStoreBackend : MediaStoreBackend() {
* @author OxygenCobalt
*/
@RequiresApi(Build.VERSION_CODES.Q)
open class Api29MediaStoreBackend : BaseApi29MediaStoreBackend() {
open class Api29MediaStoreBackend(context: Context) : BaseApi29MediaStoreBackend(context) {
private var trackIndex = -1
override val projection: Array<String>
@ -497,7 +491,7 @@ open class Api29MediaStoreBackend : BaseApi29MediaStoreBackend() {
* @author OxygenCobalt
*/
@RequiresApi(Build.VERSION_CODES.R)
class Api30MediaStoreBackend : BaseApi29MediaStoreBackend() {
class Api30MediaStoreBackend(context: Context) : BaseApi29MediaStoreBackend(context) {
private var trackIndex: Int = -1
private var discIndex: Int = -1

View file

@ -34,7 +34,7 @@ import org.oxycblt.auxio.util.androidActivityViewModels
import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.getAttrColorCompat
import org.oxycblt.auxio.util.getColorCompat
import org.oxycblt.auxio.util.msToDs
import org.oxycblt.auxio.music.msToDs
/**
* A fragment showing the current playback state in a compact manner. Used as the bar for the

View file

@ -35,7 +35,7 @@ import org.oxycblt.auxio.playback.state.RepeatMode
import org.oxycblt.auxio.ui.MainNavigationAction
import org.oxycblt.auxio.ui.fragment.MenuFragment
import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.msToDs
import org.oxycblt.auxio.music.msToDs
import org.oxycblt.auxio.util.showToast
import org.oxycblt.auxio.util.systemBarInsetsCompat

View file

@ -36,9 +36,9 @@ import org.oxycblt.auxio.playback.state.PlaybackStateManager
import org.oxycblt.auxio.playback.state.RepeatMode
import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.util.application
import org.oxycblt.auxio.util.dsToMs
import org.oxycblt.auxio.music.dsToMs
import org.oxycblt.auxio.util.logE
import org.oxycblt.auxio.util.msToDs
import org.oxycblt.auxio.music.msToDs
/**
* The ViewModel that provides a UI frontend for [PlaybackStateManager].

View file

@ -22,7 +22,7 @@ import android.util.AttributeSet
import com.google.android.material.slider.Slider
import kotlin.math.max
import org.oxycblt.auxio.databinding.ViewSeekBarBinding
import org.oxycblt.auxio.util.formatDurationDs
import org.oxycblt.auxio.music.formatDurationDs
import org.oxycblt.auxio.util.inflater
import org.oxycblt.auxio.util.logD

View file

@ -39,7 +39,7 @@ import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.ui.fragment.ViewBindingFragment
import org.oxycblt.auxio.util.androidActivityViewModels
import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.formatDurationMs
import org.oxycblt.auxio.music.formatDurationMs
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.showToast
import org.oxycblt.auxio.util.systemBarInsetsCompat

View file

@ -50,55 +50,6 @@ fun Int.nonZeroOrNull() = if (this > 0) this else null
/** Returns null if this value is not in [range]. */
fun Int.inRangeOrNull(range: IntRange) = if (range.contains(this)) this else null
/** Converts a long in milliseconds to a long in deci-seconds */
fun Long.msToDs() = floorDiv(100)
/** Converts a long in milliseconds to a long in seconds */
fun Long.msToSecs() = floorDiv(1000)
/** Converts a long in deci-seconds to a long in milliseconds. */
fun Long.dsToMs() = times(100)
/** Converts a long in deci-seconds to a long in seconds. */
fun Long.dsToSecs() = floorDiv(10)
/** Converts a long in seconds to a long in milliseconds. */
fun Long.secsToMs() = times(1000)
/**
* Convert a [Long] of milliseconds into a string duration.
* @param isElapsed Whether this duration is represents elapsed time. If this is false, then --:--
* will be returned if the second value is 0.
*/
fun Long.formatDurationMs(isElapsed: Boolean) = msToSecs().formatDurationSecs(isElapsed)
/**
* Convert a [Long] of deci-seconds into a string duration.
* @param isElapsed Whether this duration is represents elapsed time. If this is false, then --:--
* will be returned if the second value is 0.
*/
fun Long.formatDurationDs(isElapsed: Boolean) = dsToSecs().formatDurationSecs(isElapsed)
/**
* Convert a [Long] of seconds into a string duration.
* @param isElapsed Whether this duration is represents elapsed time. If this is false, then --:--
* will be returned if the second value is 0.
*/
fun Long.formatDurationSecs(isElapsed: Boolean): String {
if (!isElapsed && this == 0L) {
logD("Non-elapsed duration is zero, using --:--")
return "--:--"
}
var durationString = DateUtils.formatElapsedTime(this)
// If the duration begins with a excess zero [e.g 01:42], then cut it off.
if (durationString[0] == '0') {
durationString = durationString.slice(1 until durationString.length)
}
return durationString
}
/** Lazily reflect to retrieve a [Field]. */
fun lazyReflectedField(clazz: KClass<*>, field: String) = lazy {