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.
This commit is contained in:
parent
634fcb4273
commit
f014a2a48d
7 changed files with 54 additions and 32 deletions
|
@ -105,6 +105,8 @@ object IntegerTable {
|
||||||
const val SORT_BY_DISC = 0xA116
|
const val SORT_BY_DISC = 0xA116
|
||||||
/** Sort.ByTrack */
|
/** Sort.ByTrack */
|
||||||
const val SORT_BY_TRACK = 0xA117
|
const val SORT_BY_TRACK = 0xA117
|
||||||
|
/** Sort.ByDateAdded */
|
||||||
|
const val SORT_BY_DATE_ADDED = 0xA118
|
||||||
|
|
||||||
/** ReplayGainMode.Off */
|
/** ReplayGainMode.Off */
|
||||||
const val REPLAY_GAIN_MODE_OFF = 0xA110
|
const val REPLAY_GAIN_MODE_OFF = 0xA110
|
||||||
|
|
|
@ -67,6 +67,8 @@ data class Song(
|
||||||
val mimeType: MimeType,
|
val mimeType: MimeType,
|
||||||
/** The size of this song (in bytes) */
|
/** The size of this song (in bytes) */
|
||||||
val size: Long,
|
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. */
|
/** The total duration of this song, in millis. */
|
||||||
val durationMs: Long,
|
val durationMs: Long,
|
||||||
/** The track number of this song, null if there isn't any. */
|
/** The track number of this song, null if there isn't any. */
|
||||||
|
|
|
@ -116,6 +116,7 @@ abstract class MediaStoreBackend : Indexer.Backend {
|
||||||
private var displayNameIndex = -1
|
private var displayNameIndex = -1
|
||||||
private var mimeTypeIndex = -1
|
private var mimeTypeIndex = -1
|
||||||
private var sizeIndex = -1
|
private var sizeIndex = -1
|
||||||
|
private var dateAddedIndex = -1
|
||||||
private var durationIndex = -1
|
private var durationIndex = -1
|
||||||
private var yearIndex = -1
|
private var yearIndex = -1
|
||||||
private var albumIndex = -1
|
private var albumIndex = -1
|
||||||
|
@ -235,7 +236,20 @@ abstract class MediaStoreBackend : Indexer.Backend {
|
||||||
* implementation.
|
* implementation.
|
||||||
*/
|
*/
|
||||||
open val projection: Array<String>
|
open val projection: Array<String>
|
||||||
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 val dirSelector: String
|
||||||
abstract fun addDirToSelectorArgs(dir: Directory, args: MutableList<String>): Boolean
|
abstract fun addDirToSelectorArgs(dir: Directory, args: MutableList<String>): Boolean
|
||||||
|
@ -254,6 +268,7 @@ abstract class MediaStoreBackend : Indexer.Backend {
|
||||||
cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DISPLAY_NAME)
|
cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DISPLAY_NAME)
|
||||||
mimeTypeIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.MIME_TYPE)
|
mimeTypeIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.MIME_TYPE)
|
||||||
sizeIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.SIZE)
|
sizeIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.SIZE)
|
||||||
|
dateAddedIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DATE_ADDED)
|
||||||
durationIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DURATION)
|
durationIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DURATION)
|
||||||
yearIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.YEAR)
|
yearIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.YEAR)
|
||||||
albumIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.ALBUM)
|
albumIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.ALBUM)
|
||||||
|
@ -269,6 +284,7 @@ abstract class MediaStoreBackend : Indexer.Backend {
|
||||||
|
|
||||||
audio.extensionMimeType = cursor.getString(mimeTypeIndex)
|
audio.extensionMimeType = cursor.getString(mimeTypeIndex)
|
||||||
audio.size = cursor.getLong(sizeIndex)
|
audio.size = cursor.getLong(sizeIndex)
|
||||||
|
audio.dateAdded = cursor.getLong(dateAddedIndex)
|
||||||
|
|
||||||
// Try to use the DISPLAY_NAME field to obtain a (probably sane) file name
|
// Try to use the DISPLAY_NAME field to obtain a (probably sane) file name
|
||||||
// from the android system.
|
// from the android system.
|
||||||
|
@ -316,6 +332,7 @@ abstract class MediaStoreBackend : Indexer.Backend {
|
||||||
var extensionMimeType: String? = null,
|
var extensionMimeType: String? = null,
|
||||||
var formatMimeType: String? = null,
|
var formatMimeType: String? = null,
|
||||||
var size: Long? = null,
|
var size: Long? = null,
|
||||||
|
var dateAdded: Long? = null,
|
||||||
var duration: Long? = null,
|
var duration: Long? = null,
|
||||||
var track: Int? = null,
|
var track: Int? = null,
|
||||||
var disc: Int? = null,
|
var disc: Int? = null,
|
||||||
|
@ -326,8 +343,8 @@ abstract class MediaStoreBackend : Indexer.Backend {
|
||||||
var albumArtist: String? = null,
|
var albumArtist: String? = null,
|
||||||
var genre: String? = null
|
var genre: String? = null
|
||||||
) {
|
) {
|
||||||
fun toSong(): Song {
|
fun toSong() =
|
||||||
return Song(
|
Song(
|
||||||
// Assert that the fields that should always exist are present. I can't confirm that
|
// 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.
|
// every device provides these fields, but it seems likely that they do.
|
||||||
rawName = requireNotNull(title) { "Malformed audio: No title" },
|
rawName = requireNotNull(title) { "Malformed audio: No title" },
|
||||||
|
@ -342,6 +359,7 @@ abstract class MediaStoreBackend : Indexer.Backend {
|
||||||
requireNotNull(extensionMimeType) { "Malformed audio: No mime type" },
|
requireNotNull(extensionMimeType) { "Malformed audio: No mime type" },
|
||||||
fromFormat = formatMimeType),
|
fromFormat = formatMimeType),
|
||||||
size = requireNotNull(size) { "Malformed audio: No size" },
|
size = requireNotNull(size) { "Malformed audio: No size" },
|
||||||
|
dateAdded = requireNotNull(dateAdded) { "Malformed audio: No date added" },
|
||||||
durationMs = requireNotNull(duration) { "Malformed audio: No duration" },
|
durationMs = requireNotNull(duration) { "Malformed audio: No duration" },
|
||||||
track = track,
|
track = track,
|
||||||
disc = disc,
|
disc = disc,
|
||||||
|
@ -352,7 +370,6 @@ abstract class MediaStoreBackend : Indexer.Backend {
|
||||||
_artistName = artist,
|
_artistName = artist,
|
||||||
_albumArtistName = albumArtist,
|
_albumArtistName = albumArtist,
|
||||||
_genreName = genre)
|
_genreName = genre)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -371,24 +388,6 @@ abstract class MediaStoreBackend : Indexer.Backend {
|
||||||
*/
|
*/
|
||||||
@Suppress("InlinedApi") private const val VOLUME_EXTERNAL = MediaStore.VOLUME_EXTERNAL
|
@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
|
* The base selector that works across all versions of android. Does not exclude
|
||||||
* directories.
|
* directories.
|
||||||
|
@ -418,6 +417,7 @@ class Api21MediaStoreBackend : MediaStoreBackend() {
|
||||||
get() = "${MediaStore.Audio.Media.DATA} LIKE ?"
|
get() = "${MediaStore.Audio.Media.DATA} LIKE ?"
|
||||||
|
|
||||||
override fun addDirToSelectorArgs(dir: Directory, args: MutableList<String>): Boolean {
|
override fun addDirToSelectorArgs(dir: Directory, args: MutableList<String>): Boolean {
|
||||||
|
// Generate an equivalent DATA value from the volume directory and the relative path.
|
||||||
args.add("${dir.volume.directoryCompat ?: return false}/${dir.relativePath}%")
|
args.add("${dir.volume.directoryCompat ?: return false}/${dir.relativePath}%")
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -434,8 +434,9 @@ class Api21MediaStoreBackend : MediaStoreBackend() {
|
||||||
val data = cursor.getString(dataIndex)
|
val data = cursor.getString(dataIndex)
|
||||||
|
|
||||||
// On some OEM devices below API 29, DISPLAY_NAME may not be present. I assume
|
// 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
|
// that this only applies to below API 29, as beyond API 29, this field not being
|
||||||
// scoped storage system. Fill it in with DATA if it's not available.
|
// present would completely break the scoped storage system. Fill it in with DATA
|
||||||
|
// if it's not available.
|
||||||
if (audio.displayName == null) {
|
if (audio.displayName == null) {
|
||||||
audio.displayName = data.substringAfterLast(File.separatorChar, "").ifEmpty { null }
|
audio.displayName = data.substringAfterLast(File.separatorChar, "").ifEmpty { null }
|
||||||
}
|
}
|
||||||
|
@ -454,11 +455,7 @@ class Api21MediaStoreBackend : MediaStoreBackend() {
|
||||||
|
|
||||||
val rawTrack = cursor.getIntOrNull(trackIndex)
|
val rawTrack = cursor.getIntOrNull(trackIndex)
|
||||||
if (rawTrack != null) {
|
if (rawTrack != null) {
|
||||||
logD(rawTrack)
|
rawTrack.packedTrackNo?.let { audio.track = it }
|
||||||
rawTrack.packedTrackNo?.let {
|
|
||||||
logD(it)
|
|
||||||
audio.track = it
|
|
||||||
}
|
|
||||||
rawTrack.packedDiscNo?.let { audio.disc = 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.
|
// 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)
|
val rawTrack = cursor.getIntOrNull(trackIndex)
|
||||||
if (rawTrack != null) {
|
if (rawTrack != null) {
|
||||||
rawTrack.packedTrackNo?.let { audio.track = it }
|
rawTrack.packedTrackNo?.let { audio.track = it }
|
||||||
|
|
|
@ -252,6 +252,7 @@ data class Sort(val mode: Mode, val isAscending: Boolean) {
|
||||||
|
|
||||||
override val itemId: Int
|
override val itemId: Int
|
||||||
get() = R.id.option_sort_disc
|
get() = R.id.option_sort_disc
|
||||||
|
|
||||||
override fun getSongComparator(ascending: Boolean): Comparator<Song> =
|
override fun getSongComparator(ascending: Boolean): Comparator<Song> =
|
||||||
MultiComparator(
|
MultiComparator(
|
||||||
compareByDynamic(ascending, NULLABLE_INT_COMPARATOR) { it.disc },
|
compareByDynamic(ascending, NULLABLE_INT_COMPARATOR) { it.disc },
|
||||||
|
@ -259,6 +260,19 @@ data class Sort(val mode: Mode, val isAscending: Boolean) {
|
||||||
compareBy(BasicComparator.SONG))
|
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<Song> =
|
||||||
|
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
|
* 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
|
* 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
|
ByCount.itemId -> ByCount
|
||||||
ByDisc.itemId -> ByDisc
|
ByDisc.itemId -> ByDisc
|
||||||
ByTrack.itemId -> ByTrack
|
ByTrack.itemId -> ByTrack
|
||||||
|
ByDateAdded.itemId -> ByDateAdded
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -398,6 +413,7 @@ data class Sort(val mode: Mode, val isAscending: Boolean) {
|
||||||
Mode.ByCount.intCode -> Mode.ByCount
|
Mode.ByCount.intCode -> Mode.ByCount
|
||||||
Mode.ByDisc.intCode -> Mode.ByDisc
|
Mode.ByDisc.intCode -> Mode.ByDisc
|
||||||
Mode.ByTrack.intCode -> Mode.ByTrack
|
Mode.ByTrack.intCode -> Mode.ByTrack
|
||||||
|
Mode.ByDateAdded.intCode -> Mode.ByDateAdded
|
||||||
else -> return null
|
else -> return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
* 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
|
* 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
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -33,6 +33,9 @@
|
||||||
<item
|
<item
|
||||||
android:id="@+id/option_sort_count"
|
android:id="@+id/option_sort_count"
|
||||||
android:title="@string/lbl_sort_count" />
|
android:title="@string/lbl_sort_count" />
|
||||||
|
<item
|
||||||
|
android:id="@+id/option_sort_date_added"
|
||||||
|
android:title="@string/lbl_sort_date_added" />
|
||||||
</group>
|
</group>
|
||||||
<group android:checkableBehavior="all">
|
<group android:checkableBehavior="all">
|
||||||
<item
|
<item
|
||||||
|
|
|
@ -30,9 +30,10 @@
|
||||||
<string name="lbl_sort_album">Album</string>
|
<string name="lbl_sort_album">Album</string>
|
||||||
<string name="lbl_sort_year">Year</string>
|
<string name="lbl_sort_year">Year</string>
|
||||||
<string name="lbl_sort_duration">Duration</string>
|
<string name="lbl_sort_duration">Duration</string>
|
||||||
<string name="lbl_sort_count">Song Count</string>
|
<string name="lbl_sort_count">Song count</string>
|
||||||
<string name="lbl_sort_disc">Disc</string>
|
<string name="lbl_sort_disc">Disc</string>
|
||||||
<string name="lbl_sort_track">Track</string>
|
<string name="lbl_sort_track">Track</string>
|
||||||
|
<string name="lbl_sort_date_added">Date added</string>
|
||||||
<string name="lbl_sort_asc">Ascending</string>
|
<string name="lbl_sort_asc">Ascending</string>
|
||||||
|
|
||||||
<string name="lbl_playback">Now Playing</string>
|
<string name="lbl_playback">Now Playing</string>
|
||||||
|
|
Loading…
Reference in a new issue