music: fix uid issues
Fix some mistakes with the UID hashing process. Some more work needs to be done regarding formalizing the datatype and tagtype fields. If anything, I may just collapse them into a single "tag" field since they are only used for equality. Alternatively, I could make them integers to increase efficiency. Depends.
This commit is contained in:
parent
fe5609b447
commit
2690e8343a
8 changed files with 107 additions and 75 deletions
|
@ -9,7 +9,7 @@
|
||||||
- Fixed issue where the scroll popup would not display correctly in landscape mode [#230]
|
- Fixed issue where the scroll popup would not display correctly in landscape mode [#230]
|
||||||
- Fixed issue where the playback progress would continue in the notification even if
|
- Fixed issue where the playback progress would continue in the notification even if
|
||||||
audio focus was lost
|
audio focus was lost
|
||||||
- Fixed issue where the app would crash if a song in the genre menu was opened
|
- Fixed issue where the app would crash if a song menu in the genre UI was opened
|
||||||
|
|
||||||
#### Dev/Meta
|
#### Dev/Meta
|
||||||
- Completed migration to reactive playback system
|
- Completed migration to reactive playback system
|
||||||
|
|
|
@ -386,11 +386,9 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
|
||||||
|
|
||||||
private fun initAxisTransitions(axis: Int) {
|
private fun initAxisTransitions(axis: Int) {
|
||||||
// Sanity check
|
// Sanity check
|
||||||
if (axis != MaterialSharedAxis.X && axis != MaterialSharedAxis.Z) {
|
check(axis == MaterialSharedAxis.X || axis == MaterialSharedAxis.Z) {
|
||||||
logW("Invalid axis provided")
|
"Not expecting Y axis transition"
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enterTransition = MaterialSharedAxis(axis, true)
|
enterTransition = MaterialSharedAxis(axis, true)
|
||||||
returnTransition = MaterialSharedAxis(axis, false)
|
returnTransition = MaterialSharedAxis(axis, false)
|
||||||
exitTransition = MaterialSharedAxis(axis, true)
|
exitTransition = MaterialSharedAxis(axis, true)
|
||||||
|
|
|
@ -23,6 +23,7 @@ import android.content.Context
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
import kotlin.experimental.and
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
import kotlin.reflect.KClass
|
import kotlin.reflect.KClass
|
||||||
|
@ -77,16 +78,20 @@ sealed class Music : Item {
|
||||||
* UID enables a much cheaper and more reliable form of differentiating music, derived from
|
* UID enables a much cheaper and more reliable form of differentiating music, derived from
|
||||||
* either a hash of meaningful metadata or the MusicBrainz UUID spec. It is the default datatype
|
* either a hash of meaningful metadata or the MusicBrainz UUID spec. It is the default datatype
|
||||||
* used when comparing music, and it is also the datatype used when serializing music to
|
* used when comparing music, and it is also the datatype used when serializing music to
|
||||||
* external sources.
|
* external sources, as it can persist across app restarts and does not need to encode useless
|
||||||
|
* information about the relationships between items.
|
||||||
*
|
*
|
||||||
* TODO: Verify hash mechanism works
|
* TODO: MusizBrainz tags
|
||||||
*
|
*
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
@Parcelize
|
@Parcelize
|
||||||
class UID
|
class UID
|
||||||
private constructor(val datatype: String, val isMusicBrainz: Boolean, val uuid: UUID) :
|
private constructor(private val datatype: String, private val isMusicBrainz: Boolean, private val uuid: UUID) :
|
||||||
Parcelable {
|
Parcelable {
|
||||||
|
// TODO: Formalize datatype and isMusicBrainz more
|
||||||
|
|
||||||
|
// Cache the hashCode for speed
|
||||||
@IgnoredOnParcel private val hashCode: Int
|
@IgnoredOnParcel private val hashCode: Int
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
@ -104,9 +109,14 @@ sealed class Music : Item {
|
||||||
isMusicBrainz == other.isMusicBrainz &&
|
isMusicBrainz == other.isMusicBrainz &&
|
||||||
uuid == other.uuid
|
uuid == other.uuid
|
||||||
|
|
||||||
override fun toString() = "$datatype/${if (isMusicBrainz) "musicbrainz" else "auxio"}:$uuid"
|
override fun toString() =
|
||||||
|
"$datatype/${if (isMusicBrainz) FORMAT_MUSICBRAINZ else FORMAT_AUXIO}:$uuid"
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
const val FORMAT_AUXIO = "auxio"
|
||||||
|
const val FORMAT_MUSICBRAINZ = "musicbrainz"
|
||||||
|
|
||||||
|
/** Parse a [UID] from the string [uid]. Returns null if not valid. */
|
||||||
fun fromString(uid: String): UID? {
|
fun fromString(uid: String): UID? {
|
||||||
val split = uid.split(':', limit = 2)
|
val split = uid.split(':', limit = 2)
|
||||||
if (split.size != 2) {
|
if (split.size != 2) {
|
||||||
|
@ -122,10 +132,10 @@ sealed class Music : Item {
|
||||||
val datatype = namespace[0]
|
val datatype = namespace[0]
|
||||||
val isMusicBrainz =
|
val isMusicBrainz =
|
||||||
when (namespace[1]) {
|
when (namespace[1]) {
|
||||||
"auxio" -> false
|
FORMAT_AUXIO -> false
|
||||||
"musicbrainz" -> true
|
FORMAT_MUSICBRAINZ -> true
|
||||||
else -> {
|
else -> {
|
||||||
logE("Invalid mid: Malformed uuid type")
|
logE("Invalid uid: Malformed uuid format")
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -133,43 +143,25 @@ sealed class Music : Item {
|
||||||
val uuid =
|
val uuid =
|
||||||
try {
|
try {
|
||||||
UUID.fromString(split[1])
|
UUID.fromString(split[1])
|
||||||
} catch (e: Exception) {
|
} catch (e: IllegalArgumentException) {
|
||||||
logE("Invalid uid: Malformed UUID")
|
logE("Invalid uid: Malformed uuid")
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return UID(datatype, isMusicBrainz, uuid)
|
return UID(datatype, isMusicBrainz, uuid)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make a UUID derived from the MD5 hash of the data digested in [updates].
|
||||||
|
*
|
||||||
|
* This is considered the "auxio" uuid format.
|
||||||
|
*/
|
||||||
fun hashed(clazz: KClass<*>, updates: MessageDigest.() -> Unit): UID {
|
fun hashed(clazz: KClass<*>, updates: MessageDigest.() -> Unit): UID {
|
||||||
|
// Auxio hashes consist of the MD5 hash of the non-subjective, consistent
|
||||||
|
// tags in a music item. For easier use with MusicBrainz IDs, we
|
||||||
val digest = MessageDigest.getInstance("MD5")
|
val digest = MessageDigest.getInstance("MD5")
|
||||||
updates(digest)
|
updates(digest)
|
||||||
|
val uuid = digest.digest().toUuid()
|
||||||
// Make the MD5 hash and then bitshift it into a UUID.
|
|
||||||
val hash = digest.digest()
|
|
||||||
val uuid =
|
|
||||||
UUID(
|
|
||||||
hash[0]
|
|
||||||
.toLong()
|
|
||||||
.shl(56)
|
|
||||||
.or(hash[1].toLong().and(0xFF).shl(48))
|
|
||||||
.or(hash[2].toLong().and(0xFF).shl(40))
|
|
||||||
.or(hash[3].toLong().and(0xFF).shl(32))
|
|
||||||
.or(hash[4].toLong().and(0xFF).shl(24))
|
|
||||||
.or(hash[5].toLong().and(0xFF).shl(16))
|
|
||||||
.or(hash[6].toLong().and(0xFF).shl(8))
|
|
||||||
.or(hash[7].toLong().and(0xFF)),
|
|
||||||
hash[8]
|
|
||||||
.toLong()
|
|
||||||
.shl(56)
|
|
||||||
.or(hash[9].toLong().and(0xFF).shl(48))
|
|
||||||
.or(hash[10].toLong().and(0xFF).shl(40))
|
|
||||||
.or(hash[11].toLong().and(0xFF).shl(32))
|
|
||||||
.or(hash[12].toLong().and(0xFF).shl(24))
|
|
||||||
.or(hash[13].toLong().and(0xFF).shl(16))
|
|
||||||
.or(hash[14].toLong().and(0xFF).shl(8))
|
|
||||||
.or(hash[15].toLong().and(0xFF)))
|
|
||||||
|
|
||||||
return UID(unlikelyToBeNull(clazz.simpleName).lowercase(), false, uuid)
|
return UID(unlikelyToBeNull(clazz.simpleName).lowercase(), false, uuid)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -189,7 +181,7 @@ sealed class MusicParent : Music() {
|
||||||
* A song.
|
* A song.
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class Song(private val raw: Raw) : Music() {
|
class Song constructor(private val raw: Raw) : Music() {
|
||||||
override val uid: UID
|
override val uid: UID
|
||||||
|
|
||||||
override val rawName = requireNotNull(raw.name) { "Invalid raw: No title" }
|
override val rawName = requireNotNull(raw.name) { "Invalid raw: No title" }
|
||||||
|
@ -226,10 +218,10 @@ class Song(private val raw: Raw) : Music() {
|
||||||
val dateAdded = requireNotNull(raw.dateAdded) { "Invalid raw: No date added" }
|
val dateAdded = requireNotNull(raw.dateAdded) { "Invalid raw: No date added" }
|
||||||
|
|
||||||
/** The track number of this song in it's album.. */
|
/** The track number of this song in it's album.. */
|
||||||
val track: Int? = raw.track
|
val track = raw.track
|
||||||
|
|
||||||
/** The disc number of this song in it's album. */
|
/** The disc number of this song in it's album. */
|
||||||
val disc: Int? = raw.disc
|
val disc = raw.disc
|
||||||
|
|
||||||
private var _album: Album? = null
|
private var _album: Album? = null
|
||||||
/** The album of this song. */
|
/** The album of this song. */
|
||||||
|
@ -254,7 +246,10 @@ class Song(private val raw: Raw) : Music() {
|
||||||
raw.artistName ?: album.artist.resolveName(context)
|
raw.artistName ?: album.artist.resolveName(context)
|
||||||
|
|
||||||
private val _genres: MutableList<Genre> = mutableListOf()
|
private val _genres: MutableList<Genre> = mutableListOf()
|
||||||
/** The genre of this song. Will be an "unknown genre" if the song does not have any. */
|
/**
|
||||||
|
* The genres of this song. Most often one, but there could be multiple. There will always be at
|
||||||
|
* least one genre, even if it is an "unknown genre" instance.
|
||||||
|
*/
|
||||||
val genres: List<Genre>
|
val genres: List<Genre>
|
||||||
get() = _genres
|
get() = _genres
|
||||||
|
|
||||||
|
@ -304,11 +299,13 @@ class Song(private val raw: Raw) : Music() {
|
||||||
update(track)
|
update(track)
|
||||||
update(disc)
|
update(disc)
|
||||||
|
|
||||||
|
// Hashing by seconds makes the song more resilient to trimming
|
||||||
update(durationMs.msToSecs())
|
update(durationMs.msToSecs())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class Raw(
|
class Raw
|
||||||
|
constructor(
|
||||||
var mediaStoreId: Long? = null,
|
var mediaStoreId: Long? = null,
|
||||||
var name: String? = null,
|
var name: String? = null,
|
||||||
var sortName: String? = null,
|
var sortName: String? = null,
|
||||||
|
@ -334,8 +331,11 @@ class Song(private val raw: Raw) : Music() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The data object for an album. */
|
/**
|
||||||
class Album(raw: Raw, override val songs: List<Song>) : MusicParent() {
|
* An album.
|
||||||
|
* @author OxygenCobalt
|
||||||
|
*/
|
||||||
|
class Album constructor(raw: Raw, override val songs: List<Song>) : MusicParent() {
|
||||||
override val uid: UID
|
override val uid: UID
|
||||||
|
|
||||||
override val rawName = raw.name
|
override val rawName = raw.name
|
||||||
|
@ -410,10 +410,11 @@ class Album(raw: Raw, override val songs: List<Song>) : MusicParent() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The [MusicParent] for an *album* artist. This reflects a group of songs with the same(ish) album
|
* An artist. This is derived from the album artist first, and then the normal artist second.
|
||||||
* artist or artist field, not the individual performers of an artist.
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class Artist(
|
class Artist
|
||||||
|
constructor(
|
||||||
raw: Raw,
|
raw: Raw,
|
||||||
/** The albums of this artist. */
|
/** The albums of this artist. */
|
||||||
val albums: List<Album>
|
val albums: List<Album>
|
||||||
|
@ -454,8 +455,11 @@ class Artist(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The data object for a genre. */
|
/**
|
||||||
class Genre(raw: Raw, override val songs: List<Song>) : MusicParent() {
|
* A genre.
|
||||||
|
* @author OxygenCobalt
|
||||||
|
*/
|
||||||
|
class Genre constructor(raw: Raw, override val songs: List<Song>) : MusicParent() {
|
||||||
override val uid: UID
|
override val uid: UID
|
||||||
|
|
||||||
override val rawName = raw.name
|
override val rawName = raw.name
|
||||||
|
@ -491,21 +495,35 @@ class Genre(raw: Raw, override val songs: List<Song>) : MusicParent() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hashing extensions
|
||||||
|
|
||||||
|
/** Update the digest using the lowercase variant of a string, or don't update if null. */
|
||||||
fun MessageDigest.update(string: String?) {
|
fun MessageDigest.update(string: String?) {
|
||||||
if (string == null) return
|
if (string == null) return
|
||||||
update(string.lowercase().toByteArray())
|
update(string.lowercase().toByteArray())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Update the digest using a date. */
|
||||||
fun MessageDigest.update(date: Date?) {
|
fun MessageDigest.update(date: Date?) {
|
||||||
if (date == null) return
|
if (date == null) return
|
||||||
update(date.toString().toByteArray())
|
update(date.toString().toByteArray())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Note: All methods regarding integer bytemucking must be little-endian
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the digest using the little-endian byte representation of a byte, or do not update if
|
||||||
|
* null.
|
||||||
|
*/
|
||||||
fun MessageDigest.update(n: Int?) {
|
fun MessageDigest.update(n: Int?) {
|
||||||
if (n == null) return
|
if (n == null) return
|
||||||
update(byteArrayOf(n.toByte(), n.shr(8).toByte(), n.shr(16).toByte(), n.shr(24).toByte()))
|
update(byteArrayOf(n.toByte(), n.shr(8).toByte(), n.shr(16).toByte(), n.shr(24).toByte()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the digest using the little-endian byte representation of a long, or do not update if
|
||||||
|
* null.
|
||||||
|
*/
|
||||||
fun MessageDigest.update(n: Long?) {
|
fun MessageDigest.update(n: Long?) {
|
||||||
if (n == null) return
|
if (n == null) return
|
||||||
update(
|
update(
|
||||||
|
@ -516,8 +534,38 @@ fun MessageDigest.update(n: Long?) {
|
||||||
n.shr(24).toByte(),
|
n.shr(24).toByte(),
|
||||||
n.shr(32).toByte(),
|
n.shr(32).toByte(),
|
||||||
n.shr(40).toByte(),
|
n.shr(40).toByte(),
|
||||||
n.shr(56).toByte(),
|
n.shl(48).toByte(),
|
||||||
n.shr(64).toByte()))
|
n.shr(56).toByte()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert an array of 16 bytes to a UUID. Java is a bit strange in that it represents their UUIDs
|
||||||
|
* as two longs, however we will not assume that the given bytes represent two little endian longs.
|
||||||
|
* We will treat them as a raw sequence of bytes and serialize them as such.
|
||||||
|
*/
|
||||||
|
fun ByteArray.toUuid(): UUID {
|
||||||
|
check(size == 16)
|
||||||
|
return UUID(
|
||||||
|
get(0)
|
||||||
|
.toLong()
|
||||||
|
.shl(56)
|
||||||
|
.or(get(1).toLong().and(0xFF).shl(48))
|
||||||
|
.or(get(2).toLong().and(0xFF).shl(40))
|
||||||
|
.or(get(3).toLong().and(0xFF).shl(32))
|
||||||
|
.or(get(4).toLong().and(0xFF).shl(24))
|
||||||
|
.or(get(5).toLong().and(0xFF).shl(16))
|
||||||
|
.or(get(6).toLong().and(0xFF).shl(8))
|
||||||
|
.or(get(7).toLong().and(0xFF)),
|
||||||
|
get(8)
|
||||||
|
.toLong()
|
||||||
|
.shl(56)
|
||||||
|
.or(get(9).toLong().and(0xFF).shl(48))
|
||||||
|
.or(get(10).toLong().and(0xFF).shl(40))
|
||||||
|
.or(get(11).toLong().and(0xFF).shl(32))
|
||||||
|
.or(get(12).toLong().and(0xFF).shl(24))
|
||||||
|
.or(get(13).toLong().and(0xFF).shl(16))
|
||||||
|
.or(get(14).toLong().and(0xFF).shl(8))
|
||||||
|
.or(get(15).toLong().and(0xFF)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -532,7 +580,7 @@ fun MessageDigest.update(n: Long?) {
|
||||||
* nature of tag formats. Thus, it's better to use an analogous data structure that will not mangle
|
* nature of tag formats. Thus, it's better to use an analogous data structure that will not mangle
|
||||||
* or reject valid-ish dates.
|
* or reject valid-ish dates.
|
||||||
*
|
*
|
||||||
* Date instances are immutable and their internal implementation is hidden. To instantiate one, use
|
* Date instances are immutable and their implementation is hidden. To instantiate one, use
|
||||||
* [from]. The string representation of a Date is RFC 3339, with granular position depending on the
|
* [from]. The string representation of a Date is RFC 3339, with granular position depending on the
|
||||||
* presence of particular tokens.
|
* presence of particular tokens.
|
||||||
*
|
*
|
||||||
|
@ -755,7 +803,7 @@ sealed class ReleaseType {
|
||||||
// Compilation is the only weird secondary release type, as it could
|
// Compilation is the only weird secondary release type, as it could
|
||||||
// theoretically have additional modifiers including soundtrack, remix,
|
// theoretically have additional modifiers including soundtrack, remix,
|
||||||
// live, dj-mix, etc. However, since there is no real demand for me to
|
// live, dj-mix, etc. However, since there is no real demand for me to
|
||||||
// respond to those, I don't implement them simply for internal simplicity.
|
// respond to those, I don't implement them simply for simplicity.
|
||||||
secondary.equals("compilation", true) -> Compilation
|
secondary.equals("compilation", true) -> Compilation
|
||||||
secondary.equals("soundtrack", true) -> Soundtrack
|
secondary.equals("soundtrack", true) -> Soundtrack
|
||||||
secondary.equals("mixtape/street", true) -> Mixtape
|
secondary.equals("mixtape/street", true) -> Mixtape
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.playback.state
|
package org.oxycblt.auxio.playback
|
||||||
|
|
||||||
import org.oxycblt.auxio.IntegerTable
|
import org.oxycblt.auxio.IntegerTable
|
||||||
|
|
||||||
|
@ -33,19 +33,6 @@ enum class PlaybackMode {
|
||||||
/** Construct the queue from all songs */
|
/** Construct the queue from all songs */
|
||||||
IN_GENRE;
|
IN_GENRE;
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert the mode into an int constant, to be saved in PlaybackStateDatabase
|
|
||||||
* @return The constant for this mode,
|
|
||||||
*/
|
|
||||||
val intCode: Int
|
|
||||||
get() =
|
|
||||||
when (this) {
|
|
||||||
ALL_SONGS -> IntegerTable.PLAYBACK_MODE_ALL_SONGS
|
|
||||||
IN_ALBUM -> IntegerTable.PLAYBACK_MODE_IN_ALBUM
|
|
||||||
IN_ARTIST -> IntegerTable.PLAYBACK_MODE_IN_ARTIST
|
|
||||||
IN_GENRE -> IntegerTable.PLAYBACK_MODE_IN_GENRE
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
/**
|
/**
|
||||||
* Get a [PlaybackMode] for an int [constant]
|
* Get a [PlaybackMode] for an int [constant]
|
|
@ -31,7 +31,6 @@ import org.oxycblt.auxio.music.Genre
|
||||||
import org.oxycblt.auxio.music.MusicParent
|
import org.oxycblt.auxio.music.MusicParent
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.playback.state.InternalPlayer
|
import org.oxycblt.auxio.playback.state.InternalPlayer
|
||||||
import org.oxycblt.auxio.playback.state.PlaybackMode
|
|
||||||
import org.oxycblt.auxio.playback.state.PlaybackStateDatabase
|
import org.oxycblt.auxio.playback.state.PlaybackStateDatabase
|
||||||
import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
||||||
import org.oxycblt.auxio.playback.state.RepeatMode
|
import org.oxycblt.auxio.playback.state.RepeatMode
|
||||||
|
|
|
@ -104,8 +104,7 @@ class PlaybackStateDatabase private constructor(context: Context) :
|
||||||
|
|
||||||
// Correct the index to match up with a possibly shortened queue (file removals/changes)
|
// Correct the index to match up with a possibly shortened queue (file removals/changes)
|
||||||
var actualIndex = rawState.index
|
var actualIndex = rawState.index
|
||||||
while (queue.getOrNull(actualIndex)?.uid?.also { logD(it) } != rawState.songUid &&
|
while (queue.getOrNull(actualIndex)?.uid != rawState.songUid && actualIndex > -1) {
|
||||||
actualIndex > -1) {
|
|
||||||
actualIndex--
|
actualIndex--
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -158,7 +157,6 @@ class PlaybackStateDatabase private constructor(context: Context) :
|
||||||
if (cursor.count == 0) return@queryAll
|
if (cursor.count == 0) return@queryAll
|
||||||
val songIndex = cursor.getColumnIndexOrThrow(QueueColumns.SONG_UID)
|
val songIndex = cursor.getColumnIndexOrThrow(QueueColumns.SONG_UID)
|
||||||
while (cursor.moveToNext()) {
|
while (cursor.moveToNext()) {
|
||||||
logD(cursor.getString(songIndex))
|
|
||||||
val uid = Music.UID.fromString(cursor.getString(songIndex)) ?: continue
|
val uid = Music.UID.fromString(cursor.getString(songIndex)) ?: continue
|
||||||
val song = library.find<Song>(uid) ?: continue
|
val song = library.find<Song>(uid) ?: continue
|
||||||
queue.add(song)
|
queue.add(song)
|
||||||
|
|
|
@ -28,9 +28,9 @@ import org.oxycblt.auxio.home.tabs.Tab
|
||||||
import org.oxycblt.auxio.music.Directory
|
import org.oxycblt.auxio.music.Directory
|
||||||
import org.oxycblt.auxio.music.dirs.MusicDirs
|
import org.oxycblt.auxio.music.dirs.MusicDirs
|
||||||
import org.oxycblt.auxio.playback.BarAction
|
import org.oxycblt.auxio.playback.BarAction
|
||||||
|
import org.oxycblt.auxio.playback.PlaybackMode
|
||||||
import org.oxycblt.auxio.playback.replaygain.ReplayGainMode
|
import org.oxycblt.auxio.playback.replaygain.ReplayGainMode
|
||||||
import org.oxycblt.auxio.playback.replaygain.ReplayGainPreAmp
|
import org.oxycblt.auxio.playback.replaygain.ReplayGainPreAmp
|
||||||
import org.oxycblt.auxio.playback.state.PlaybackMode
|
|
||||||
import org.oxycblt.auxio.ui.DisplayMode
|
import org.oxycblt.auxio.ui.DisplayMode
|
||||||
import org.oxycblt.auxio.ui.Sort
|
import org.oxycblt.auxio.ui.Sort
|
||||||
import org.oxycblt.auxio.ui.accent.Accent
|
import org.oxycblt.auxio.ui.accent.Accent
|
||||||
|
|
|
@ -65,6 +65,8 @@ import org.oxycblt.auxio.util.systemBarInsetsCompat
|
||||||
* - Added drag listener
|
* - Added drag listener
|
||||||
* - Added documentation
|
* - Added documentation
|
||||||
*
|
*
|
||||||
|
* TODO: Add vibration when popup changes
|
||||||
|
*
|
||||||
* @author Hai Zhang, OxygenCobalt
|
* @author Hai Zhang, OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class FastScrollRecyclerView
|
class FastScrollRecyclerView
|
||||||
|
|
Loading…
Reference in a new issue