From f014a2a48ddeaef86ebb394abc255cf77937f4e6 Mon Sep 17 00:00:00 2001 From: OxygenCobalt Date: Wed, 13 Jul 2022 14:20:35 -0600 Subject: [PATCH] home: add last added sorting [#181] Add a "Last Added" sorting option to the home UI's song list. I don't know if there is any demand for last added in other contexts. That will be resolved later. --- .../java/org/oxycblt/auxio/IntegerTable.kt | 2 + .../java/org/oxycblt/auxio/music/Music.kt | 2 + .../auxio/music/system/MediaStoreBackend.kt | 57 +++++++++---------- .../main/java/org/oxycblt/auxio/ui/Sort.kt | 16 ++++++ .../org/oxycblt/auxio/util/PrimitiveUtil.kt | 3 +- app/src/main/res/menu/menu_home.xml | 3 + app/src/main/res/values/strings.xml | 3 +- 7 files changed, 54 insertions(+), 32 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/IntegerTable.kt b/app/src/main/java/org/oxycblt/auxio/IntegerTable.kt index 5f2afb8c1..f43bce062 100644 --- a/app/src/main/java/org/oxycblt/auxio/IntegerTable.kt +++ b/app/src/main/java/org/oxycblt/auxio/IntegerTable.kt @@ -105,6 +105,8 @@ object IntegerTable { const val SORT_BY_DISC = 0xA116 /** Sort.ByTrack */ const val SORT_BY_TRACK = 0xA117 + /** Sort.ByDateAdded */ + const val SORT_BY_DATE_ADDED = 0xA118 /** ReplayGainMode.Off */ const val REPLAY_GAIN_MODE_OFF = 0xA110 diff --git a/app/src/main/java/org/oxycblt/auxio/music/Music.kt b/app/src/main/java/org/oxycblt/auxio/music/Music.kt index 2c4d47b46..3950b0607 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/Music.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/Music.kt @@ -67,6 +67,8 @@ data class Song( val mimeType: MimeType, /** The size of this song (in bytes) */ val size: Long, + /** The datetime at which this media item was added, represented as a unix timestamp. */ + val dateAdded: Long, /** The total duration of this song, in millis. */ val durationMs: Long, /** The track number of this song, null if there isn't any. */ diff --git a/app/src/main/java/org/oxycblt/auxio/music/system/MediaStoreBackend.kt b/app/src/main/java/org/oxycblt/auxio/music/system/MediaStoreBackend.kt index e0439ca92..c2e9b1a51 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/system/MediaStoreBackend.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/system/MediaStoreBackend.kt @@ -116,6 +116,7 @@ abstract class MediaStoreBackend : Indexer.Backend { private var displayNameIndex = -1 private var mimeTypeIndex = -1 private var sizeIndex = -1 + private var dateAddedIndex = -1 private var durationIndex = -1 private var yearIndex = -1 private var albumIndex = -1 @@ -235,7 +236,20 @@ abstract class MediaStoreBackend : Indexer.Backend { * implementation. */ open val projection: Array - get() = BASE_PROJECTION + get() = + arrayOf( + MediaStore.Audio.AudioColumns._ID, + MediaStore.Audio.AudioColumns.TITLE, + MediaStore.Audio.AudioColumns.DISPLAY_NAME, + MediaStore.Audio.AudioColumns.MIME_TYPE, + MediaStore.Audio.AudioColumns.SIZE, + MediaStore.Audio.AudioColumns.DATE_ADDED, + MediaStore.Audio.AudioColumns.DURATION, + MediaStore.Audio.AudioColumns.YEAR, + MediaStore.Audio.AudioColumns.ALBUM, + MediaStore.Audio.AudioColumns.ALBUM_ID, + MediaStore.Audio.AudioColumns.ARTIST, + AUDIO_COLUMN_ALBUM_ARTIST) abstract val dirSelector: String abstract fun addDirToSelectorArgs(dir: Directory, args: MutableList): Boolean @@ -254,6 +268,7 @@ abstract class MediaStoreBackend : Indexer.Backend { cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DISPLAY_NAME) mimeTypeIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.MIME_TYPE) sizeIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.SIZE) + dateAddedIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DATE_ADDED) durationIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DURATION) yearIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.YEAR) albumIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.ALBUM) @@ -269,6 +284,7 @@ abstract class MediaStoreBackend : Indexer.Backend { audio.extensionMimeType = cursor.getString(mimeTypeIndex) audio.size = cursor.getLong(sizeIndex) + audio.dateAdded = cursor.getLong(dateAddedIndex) // Try to use the DISPLAY_NAME field to obtain a (probably sane) file name // from the android system. @@ -316,6 +332,7 @@ abstract class MediaStoreBackend : Indexer.Backend { var extensionMimeType: String? = null, var formatMimeType: String? = null, var size: Long? = null, + var dateAdded: Long? = null, var duration: Long? = null, var track: Int? = null, var disc: Int? = null, @@ -326,8 +343,8 @@ abstract class MediaStoreBackend : Indexer.Backend { var albumArtist: String? = null, var genre: String? = null ) { - fun toSong(): Song { - return Song( + fun toSong() = + Song( // Assert that the fields that should always exist are present. I can't confirm that // every device provides these fields, but it seems likely that they do. rawName = requireNotNull(title) { "Malformed audio: No title" }, @@ -342,6 +359,7 @@ abstract class MediaStoreBackend : Indexer.Backend { requireNotNull(extensionMimeType) { "Malformed audio: No mime type" }, fromFormat = formatMimeType), size = requireNotNull(size) { "Malformed audio: No size" }, + dateAdded = requireNotNull(dateAdded) { "Malformed audio: No date added" }, durationMs = requireNotNull(duration) { "Malformed audio: No duration" }, track = track, disc = disc, @@ -352,7 +370,6 @@ abstract class MediaStoreBackend : Indexer.Backend { _artistName = artist, _albumArtistName = albumArtist, _genreName = genre) - } } companion object { @@ -371,24 +388,6 @@ abstract class MediaStoreBackend : Indexer.Backend { */ @Suppress("InlinedApi") private const val VOLUME_EXTERNAL = MediaStore.VOLUME_EXTERNAL - /** - * The basic projection that works across all versions of android. Is incomplete, hence why - * sub-implementations should be used instead. - */ - private val BASE_PROJECTION = - arrayOf( - MediaStore.Audio.AudioColumns._ID, - MediaStore.Audio.AudioColumns.TITLE, - MediaStore.Audio.AudioColumns.DISPLAY_NAME, - MediaStore.Audio.AudioColumns.MIME_TYPE, - MediaStore.Audio.AudioColumns.SIZE, - MediaStore.Audio.AudioColumns.DURATION, - MediaStore.Audio.AudioColumns.YEAR, - MediaStore.Audio.AudioColumns.ALBUM, - MediaStore.Audio.AudioColumns.ALBUM_ID, - MediaStore.Audio.AudioColumns.ARTIST, - AUDIO_COLUMN_ALBUM_ARTIST) - /** * The base selector that works across all versions of android. Does not exclude * directories. @@ -418,6 +417,7 @@ class Api21MediaStoreBackend : MediaStoreBackend() { get() = "${MediaStore.Audio.Media.DATA} LIKE ?" override fun addDirToSelectorArgs(dir: Directory, args: MutableList): Boolean { + // Generate an equivalent DATA value from the volume directory and the relative path. args.add("${dir.volume.directoryCompat ?: return false}/${dir.relativePath}%") return true } @@ -434,8 +434,9 @@ class Api21MediaStoreBackend : MediaStoreBackend() { val data = cursor.getString(dataIndex) // On some OEM devices below API 29, DISPLAY_NAME may not be present. I assume - // that this only applies to below API 29, as that would completely break the - // scoped storage system. Fill it in with DATA if it's not available. + // that this only applies to below API 29, as beyond API 29, this field not being + // present would completely break the scoped storage system. Fill it in with DATA + // if it's not available. if (audio.displayName == null) { audio.displayName = data.substringAfterLast(File.separatorChar, "").ifEmpty { null } } @@ -454,11 +455,7 @@ class Api21MediaStoreBackend : MediaStoreBackend() { val rawTrack = cursor.getIntOrNull(trackIndex) if (rawTrack != null) { - logD(rawTrack) - rawTrack.packedTrackNo?.let { - logD(it) - audio.track = it - } + rawTrack.packedTrackNo?.let { audio.track = it } rawTrack.packedDiscNo?.let { audio.disc = it } } @@ -541,7 +538,7 @@ open class Api29MediaStoreBackend : BaseApi29MediaStoreBackend() { } // This backend is volume-aware, but does not support the modern track fields. - // Use the packed utilities instead. + // Use the old field instead. val rawTrack = cursor.getIntOrNull(trackIndex) if (rawTrack != null) { rawTrack.packedTrackNo?.let { audio.track = it } diff --git a/app/src/main/java/org/oxycblt/auxio/ui/Sort.kt b/app/src/main/java/org/oxycblt/auxio/ui/Sort.kt index 4fe015972..a83f23f42 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/Sort.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/Sort.kt @@ -252,6 +252,7 @@ data class Sort(val mode: Mode, val isAscending: Boolean) { override val itemId: Int get() = R.id.option_sort_disc + override fun getSongComparator(ascending: Boolean): Comparator = MultiComparator( compareByDynamic(ascending, NULLABLE_INT_COMPARATOR) { it.disc }, @@ -259,6 +260,19 @@ data class Sort(val mode: Mode, val isAscending: Boolean) { compareBy(BasicComparator.SONG)) } + /** Sort by the time the item was added. Only supported by [Song] */ + object ByDateAdded : Mode() { + override val intCode: Int + get() = IntegerTable.SORT_BY_DATE_ADDED + + override val itemId: Int + get() = R.id.option_sort_date_added + + override fun getSongComparator(ascending: Boolean): Comparator = + MultiComparator( + compareByDynamic(ascending) { it.dateAdded }, compareBy(BasicComparator.SONG)) + } + /** * Sort by the disc, and then track number of an item. Only supported by [Song]. Do not use * this in a main sorting view, as it is not assigned to a particular item ID @@ -374,6 +388,7 @@ data class Sort(val mode: Mode, val isAscending: Boolean) { ByCount.itemId -> ByCount ByDisc.itemId -> ByDisc ByTrack.itemId -> ByTrack + ByDateAdded.itemId -> ByDateAdded else -> null } } @@ -398,6 +413,7 @@ data class Sort(val mode: Mode, val isAscending: Boolean) { Mode.ByCount.intCode -> Mode.ByCount Mode.ByDisc.intCode -> Mode.ByDisc Mode.ByTrack.intCode -> Mode.ByTrack + Mode.ByDateAdded.intCode -> Mode.ByDateAdded else -> return null } diff --git a/app/src/main/java/org/oxycblt/auxio/util/PrimitiveUtil.kt b/app/src/main/java/org/oxycblt/auxio/util/PrimitiveUtil.kt index 52b269be4..9bc7b079b 100644 --- a/app/src/main/java/org/oxycblt/auxio/util/PrimitiveUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/util/PrimitiveUtil.kt @@ -77,7 +77,8 @@ fun lazyReflectedMethod(clazz: KClass<*>, method: String) = lazy { /** * An abstraction that allows cheap cooperative multi-threading in shared object contexts. Every new * task should call [newHandle], while every running task should call [check] or [yield] depending - * on the context to determine if it should continue. + * on the situation to determine if it should continue. Failure to follow the expectations of this + * class will result in bugs. * * @author OxygenCobalt */ diff --git a/app/src/main/res/menu/menu_home.xml b/app/src/main/res/menu/menu_home.xml index c1b0561f2..273f558de 100644 --- a/app/src/main/res/menu/menu_home.xml +++ b/app/src/main/res/menu/menu_home.xml @@ -33,6 +33,9 @@ + Album Year Duration - Song Count + Song count Disc Track + Date added Ascending Now Playing