fastlane: update screenshots
Update app screenshots for 2.3.0. Notably, a new album screenshot has been added showcasing the new disc functionality.
|
@ -106,9 +106,9 @@ object Indexer {
|
||||||
// Establish the compatibility object to use when loading songs.
|
// Establish the compatibility object to use when loading songs.
|
||||||
val compat =
|
val compat =
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
Api30AudioCompat()
|
Api30MediaStoreCompat()
|
||||||
} else {
|
} else {
|
||||||
Api21AudioCompat()
|
Api21MediaStoreCompat()
|
||||||
}
|
}
|
||||||
|
|
||||||
val songs = loadSongs(context, compat)
|
val songs = loadSongs(context, compat)
|
||||||
|
@ -138,7 +138,7 @@ object Indexer {
|
||||||
* [buildArtists], and [readGenres] functions must be called with the returned list so that all
|
* [buildArtists], and [readGenres] functions must be called with the returned list so that all
|
||||||
* songs are properly linked up.
|
* songs are properly linked up.
|
||||||
*/
|
*/
|
||||||
private fun loadSongs(context: Context, compat: AudioDatabaseCompat): List<Song> {
|
private fun loadSongs(context: Context, compat: MediaStoreCompat): List<Song> {
|
||||||
val excludedDatabase = ExcludedDatabase.getInstance(context)
|
val excludedDatabase = ExcludedDatabase.getInstance(context)
|
||||||
var selector = "${MediaStore.Audio.Media.IS_MUSIC}=1"
|
var selector = "${MediaStore.Audio.Media.IS_MUSIC}=1"
|
||||||
val args = mutableListOf<String>()
|
val args = mutableListOf<String>()
|
||||||
|
@ -154,7 +154,8 @@ object Indexer {
|
||||||
|
|
||||||
var songs = mutableListOf<Song>()
|
var songs = mutableListOf<Song>()
|
||||||
|
|
||||||
val columns =
|
// Establish the columns that work across all versions of android.
|
||||||
|
val proj =
|
||||||
mutableListOf(
|
mutableListOf(
|
||||||
MediaStore.Audio.AudioColumns._ID,
|
MediaStore.Audio.AudioColumns._ID,
|
||||||
MediaStore.Audio.AudioColumns.TITLE,
|
MediaStore.Audio.AudioColumns.TITLE,
|
||||||
|
@ -168,12 +169,12 @@ object Indexer {
|
||||||
MediaStore.Audio.AudioColumns.DATA)
|
MediaStore.Audio.AudioColumns.DATA)
|
||||||
|
|
||||||
// Get the compat impl to add their version-specific columns.
|
// Get the compat impl to add their version-specific columns.
|
||||||
compat.addSongColumns(columns)
|
compat.mutateAudioProjection(proj)
|
||||||
|
|
||||||
context.contentResolverSafe
|
context.contentResolverSafe
|
||||||
.query(
|
.query(
|
||||||
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
|
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
|
||||||
columns.toTypedArray(),
|
proj.toTypedArray(),
|
||||||
selector,
|
selector,
|
||||||
args.toTypedArray(),
|
args.toTypedArray(),
|
||||||
null)
|
null)
|
||||||
|
@ -193,16 +194,16 @@ object Indexer {
|
||||||
val dataIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DATA)
|
val dataIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DATA)
|
||||||
|
|
||||||
while (cursor.moveToNext()) {
|
while (cursor.moveToNext()) {
|
||||||
val raw = RawSong()
|
val raw = Audio()
|
||||||
|
|
||||||
raw.songId = cursor.getLong(idIndex)
|
raw.id = cursor.getLong(idIndex)
|
||||||
raw.title = cursor.getString(titleIndex)
|
raw.title = cursor.getString(titleIndex)
|
||||||
|
|
||||||
// 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. Once again though, OEM issues get in our way and
|
// from the android system. Once again though, OEM issues get in our way and
|
||||||
// this field isn't available on some platforms. In that case, see if we can
|
// this field isn't available on some platforms. In that case, see if we can
|
||||||
// grok a file name from the DATA field.
|
// grok a file name from the DATA field.
|
||||||
raw.fileName =
|
raw.displayName =
|
||||||
cursor.getStringOrNull(fileIndex)
|
cursor.getStringOrNull(fileIndex)
|
||||||
?: cursor
|
?: cursor
|
||||||
.getStringOrNull(dataIndex)
|
.getStringOrNull(dataIndex)
|
||||||
|
@ -229,7 +230,7 @@ object Indexer {
|
||||||
raw.albumArtist = cursor.getStringOrNull(albumArtistIndex)
|
raw.albumArtist = cursor.getStringOrNull(albumArtistIndex)
|
||||||
|
|
||||||
// Allow the compatibility object to add their fields
|
// Allow the compatibility object to add their fields
|
||||||
compat.mutateSong(cursor, raw)
|
compat.populateAudio(cursor, raw)
|
||||||
|
|
||||||
songs.add(raw.toSong())
|
songs.add(raw.toSong())
|
||||||
}
|
}
|
||||||
|
@ -322,9 +323,10 @@ object Indexer {
|
||||||
for (entry in albumsByArtist) {
|
for (entry in albumsByArtist) {
|
||||||
val templateAlbum = entry.value[0]
|
val templateAlbum = entry.value[0]
|
||||||
val artistName =
|
val artistName =
|
||||||
when (templateAlbum._artistGroupingName) {
|
if (templateAlbum._artistGroupingName != MediaStore.UNKNOWN_STRING) {
|
||||||
MediaStore.UNKNOWN_STRING -> null
|
templateAlbum._artistGroupingName
|
||||||
else -> templateAlbum._artistGroupingName
|
} else {
|
||||||
|
null
|
||||||
}
|
}
|
||||||
val artistAlbums = entry.value
|
val artistAlbums = entry.value
|
||||||
|
|
||||||
|
@ -407,15 +409,16 @@ object Indexer {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a song currently being assembled by the indexer. There is no guarantee that
|
* Represents a song as it is represented by MediaStore. This is progressively mutated over
|
||||||
* metadata is sane or complete until it is transformed into a song with [toSong]
|
* several steps of the music loading process until it is complete enough to be transformed into
|
||||||
|
* a song.
|
||||||
*
|
*
|
||||||
* TODO: Add manual metadata parsing.
|
* TODO: Add manual metadata parsing.
|
||||||
*/
|
*/
|
||||||
private data class RawSong(
|
private data class Audio(
|
||||||
var songId: Long? = null,
|
var id: Long? = null,
|
||||||
var title: String? = null,
|
var title: String? = null,
|
||||||
var fileName: String? = null,
|
var displayName: String? = 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,
|
||||||
|
@ -425,15 +428,14 @@ object Indexer {
|
||||||
var artist: String? = null,
|
var artist: String? = null,
|
||||||
var albumArtist: String? = null,
|
var albumArtist: String? = null,
|
||||||
) {
|
) {
|
||||||
// TODO: Bundle this conversion into the grouping process
|
|
||||||
fun toSong(): Song =
|
fun toSong(): Song =
|
||||||
Song(
|
Song(
|
||||||
requireNotNull(title) { "Malformed song: No title" },
|
requireNotNull(title) { "Malformed song: No title" },
|
||||||
requireNotNull(fileName) { "Malformed song: No file name" },
|
requireNotNull(displayName) { "Malformed song: No file name" },
|
||||||
requireNotNull(duration) { "Malformed song: No duration" },
|
requireNotNull(duration) { "Malformed song: No duration" },
|
||||||
track,
|
track,
|
||||||
disc,
|
disc,
|
||||||
requireNotNull(songId) { "Malformed song: No song id" },
|
requireNotNull(id) { "Malformed song: No song id" },
|
||||||
year,
|
year,
|
||||||
requireNotNull(album) { "Malformed song: No album name" },
|
requireNotNull(album) { "Malformed song: No album name" },
|
||||||
requireNotNull(albumId) { "Malformed song: No album id" },
|
requireNotNull(albumId) { "Malformed song: No album id" },
|
||||||
|
@ -443,25 +445,24 @@ object Indexer {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A compatibility interface to implement version-specific audio database querying. */
|
/** A compatibility interface to implement version-specific audio database querying. */
|
||||||
private interface AudioDatabaseCompat {
|
private interface MediaStoreCompat {
|
||||||
/** Add version-specific columns to the given projection. */
|
/** Mutate the pre-existing projection with version-specific values. */
|
||||||
fun addSongColumns(columns: MutableList<String>)
|
fun mutateAudioProjection(proj: MutableList<String>)
|
||||||
|
/** Mutate [audio] with the columns added in [mutateAudioProjection], */
|
||||||
/** Mutate a [raw] song by reading the columns added in [addSongColumns], */
|
fun populateAudio(cursor: Cursor, audio: Audio)
|
||||||
fun mutateSong(cursor: Cursor, raw: RawSong)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.R)
|
@RequiresApi(Build.VERSION_CODES.R)
|
||||||
private class Api30AudioCompat : AudioDatabaseCompat {
|
private class Api30MediaStoreCompat : MediaStoreCompat {
|
||||||
private var trackIndex: Int = -1
|
private var trackIndex: Int = -1
|
||||||
private var discIndex: Int = -1
|
private var discIndex: Int = -1
|
||||||
|
|
||||||
override fun addSongColumns(columns: MutableList<String>) {
|
override fun mutateAudioProjection(proj: MutableList<String>) {
|
||||||
columns.add(MediaStore.Audio.AudioColumns.CD_TRACK_NUMBER)
|
proj.add(MediaStore.Audio.AudioColumns.CD_TRACK_NUMBER)
|
||||||
columns.add(MediaStore.Audio.AudioColumns.DISC_NUMBER)
|
proj.add(MediaStore.Audio.AudioColumns.DISC_NUMBER)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun mutateSong(cursor: Cursor, raw: RawSong) {
|
override fun populateAudio(cursor: Cursor, audio: Audio) {
|
||||||
if (trackIndex == -1) {
|
if (trackIndex == -1) {
|
||||||
trackIndex =
|
trackIndex =
|
||||||
cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.CD_TRACK_NUMBER)
|
cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.CD_TRACK_NUMBER)
|
||||||
|
@ -476,47 +477,46 @@ object Indexer {
|
||||||
// N is the number and T is the total. Parse the number while leaving out the
|
// N is the number and T is the total. Parse the number while leaving out the
|
||||||
// total, as we have no use for it.
|
// total, as we have no use for it.
|
||||||
|
|
||||||
val track = cursor.getStringOrNull(trackIndex)
|
cursor
|
||||||
val disc = cursor.getStringOrNull(discIndex)
|
.getStringOrNull(trackIndex)
|
||||||
|
?.split('/', limit = 2)
|
||||||
if (track != null) {
|
?.getOrNull(0)
|
||||||
raw.track = track.split('/', limit = 2).getOrNull(0)?.toIntOrNull()
|
?.toIntOrNull()
|
||||||
}
|
?.let { audio.track = it }
|
||||||
|
cursor
|
||||||
if (disc != null) {
|
.getStringOrNull(discIndex)
|
||||||
raw.disc = disc.split('/', limit = 2).getOrNull(0)?.toIntOrNull()
|
?.split('/', limit = 2)
|
||||||
}
|
?.getOrNull(0)
|
||||||
|
?.toIntOrNull()
|
||||||
|
?.let { audio.disc = it }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class Api21AudioCompat : AudioDatabaseCompat {
|
private class Api21MediaStoreCompat : MediaStoreCompat {
|
||||||
private var trackIndex: Int = -1
|
private var trackIndex: Int = -1
|
||||||
|
|
||||||
override fun addSongColumns(columns: MutableList<String>) {
|
override fun mutateAudioProjection(proj: MutableList<String>) {
|
||||||
columns.add(MediaStore.Audio.AudioColumns.TRACK)
|
proj.add(MediaStore.Audio.AudioColumns.TRACK)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun mutateSong(cursor: Cursor, raw: RawSong) {
|
override fun populateAudio(cursor: Cursor, audio: Audio) {
|
||||||
if (trackIndex == -1) {
|
if (trackIndex == -1) {
|
||||||
trackIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.TRACK)
|
trackIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.TRACK)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TRACK is formatted as DTTT where D is the disc number and T is the track number.
|
// TRACK is formatted as DTTT where D is the disc number and T is the track number.
|
||||||
// At least, I think so. I've so far been unable to reproduce track numbers on older
|
// At least, I think so. I've so far been unable to reproduce disc numbers on older
|
||||||
// devices. Keep it around just in case.
|
// devices. Keep it around just in case.
|
||||||
|
|
||||||
val rawTrack = cursor.getIntOrNull(trackIndex)
|
val rawTrack = cursor.getIntOrNull(trackIndex)
|
||||||
if (rawTrack != null) {
|
if (rawTrack != null) {
|
||||||
raw.track = rawTrack % 1000
|
audio.track = rawTrack % 1000
|
||||||
|
|
||||||
// A disc number of 0 means that there is no disc.
|
// A disc number of 0 means that there is no disc.
|
||||||
val disc = rawTrack / 1000
|
val disc = rawTrack / 1000
|
||||||
raw.disc =
|
if (disc > 0) {
|
||||||
if (disc > 0) {
|
audio.disc = disc
|
||||||
disc
|
}
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -88,6 +88,7 @@ data class Song(
|
||||||
result = 31 * result + album.rawName.hashCode()
|
result = 31 * result + album.rawName.hashCode()
|
||||||
result = 31 * result + album.artist.rawName.hashCode()
|
result = 31 * result + album.artist.rawName.hashCode()
|
||||||
result = 31 * result + (track ?: 0)
|
result = 31 * result + (track ?: 0)
|
||||||
|
// TODO: Rework hashing to add discs and handle null values correctly
|
||||||
result = 31 * result + durationMs.hashCode()
|
result = 31 * result + durationMs.hashCode()
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
@ -152,12 +153,12 @@ data class Song(
|
||||||
get() = _genre == null
|
get() = _genre == null
|
||||||
|
|
||||||
/** Internal method. Do not use. */
|
/** Internal method. Do not use. */
|
||||||
fun _linkAlbum(album: Album) {
|
fun _link(album: Album) {
|
||||||
_album = album
|
_album = album
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Internal method. Do not use. */
|
/** Internal method. Do not use. */
|
||||||
fun _linkGenre(genre: Genre) {
|
fun _link(genre: Genre) {
|
||||||
_genre = genre
|
_genre = genre
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -176,7 +177,7 @@ data class Album(
|
||||||
) : MusicParent() {
|
) : MusicParent() {
|
||||||
init {
|
init {
|
||||||
for (song in songs) {
|
for (song in songs) {
|
||||||
song._linkAlbum(this)
|
song._link(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -207,7 +208,7 @@ data class Album(
|
||||||
get() = _artist == null
|
get() = _artist == null
|
||||||
|
|
||||||
/** Internal method. Do not use. */
|
/** Internal method. Do not use. */
|
||||||
fun _linkArtist(artist: Artist) {
|
fun _link(artist: Artist) {
|
||||||
_artist = artist
|
_artist = artist
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -223,7 +224,7 @@ data class Artist(
|
||||||
) : MusicParent() {
|
) : MusicParent() {
|
||||||
init {
|
init {
|
||||||
for (album in albums) {
|
for (album in albums) {
|
||||||
album._linkArtist(this)
|
album._link(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -243,7 +244,7 @@ data class Artist(
|
||||||
data class Genre(override val rawName: String?, override val songs: List<Song>) : MusicParent() {
|
data class Genre(override val rawName: String?, override val songs: List<Song>) : MusicParent() {
|
||||||
init {
|
init {
|
||||||
for (song in songs) {
|
for (song in songs) {
|
||||||
song._linkGenre(this)
|
song._link(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Before Width: | Height: | Size: 160 KiB After Width: | Height: | Size: 158 KiB |
Before Width: | Height: | Size: 137 KiB After Width: | Height: | Size: 128 KiB |
Before Width: | Height: | Size: 133 KiB After Width: | Height: | Size: 129 KiB |
Before Width: | Height: | Size: 296 KiB After Width: | Height: | Size: 295 KiB |
Before Width: | Height: | Size: 166 KiB After Width: | Height: | Size: 161 KiB |
Before Width: | Height: | Size: 152 KiB After Width: | Height: | Size: 127 KiB |
Before Width: | Height: | Size: 254 KiB After Width: | Height: | Size: 246 KiB |
|
@ -128,7 +128,7 @@ The diagram below highlights the overall structure and connections:
|
||||||
┌──────────────────── PlaybackService ────────────────┐
|
┌──────────────────── PlaybackService ────────────────┐
|
||||||
│ │ │
|
│ │ │
|
||||||
PlaybackStateManager [Communicates with] │ │
|
PlaybackStateManager [Communicates with] │ │
|
||||||
│ │ [Contains] │
|
│ │ [Contains] │ [Communicates with]
|
||||||
│ │ │
|
│ │ │
|
||||||
│ ├ WidgetComponent ┤
|
│ ├ WidgetComponent ┤
|
||||||
│ ├ NotificationComponent ┤
|
│ ├ NotificationComponent ┤
|
||||||
|
@ -137,7 +137,7 @@ PlaybackStateManager [Communicates with] │ │
|
||||||
│
|
│
|
||||||
│
|
│
|
||||||
└──────────────────── PlaybackViewModel ───────────────────── UIs
|
└──────────────────── PlaybackViewModel ───────────────────── UIs
|
||||||
[Communicates With]
|
[Communicates with]
|
||||||
```
|
```
|
||||||
|
|
||||||
`PlaybackStateManager` is the shared object that contains the master copy of the playback state, doing all operations on it. This object should
|
`PlaybackStateManager` is the shared object that contains the master copy of the playback state, doing all operations on it. This object should
|
||||||
|
@ -203,10 +203,10 @@ Key classes in this package include:
|
||||||
This module not only contains the playback system described above, but also multiple other components:
|
This module not only contains the playback system described above, but also multiple other components:
|
||||||
|
|
||||||
- `queue` contains the Queue UI and it's fancy item UIs.
|
- `queue` contains the Queue UI and it's fancy item UIs.
|
||||||
- `state` contains the core playback state and persistence system.
|
|
||||||
- `replaygain` contains the ReplayGain implementation and the UIs related to it. Auxio's ReplayGain implementation is
|
- `replaygain` contains the ReplayGain implementation and the UIs related to it. Auxio's ReplayGain implementation is
|
||||||
somewhat different compared to other apps, as it leverages ExoPlayer's metadata and audio processing systems to not only
|
somewhat different compared to other apps, as it leverages ExoPlayer's metadata and audio processing systems to not only
|
||||||
parse ReplayGain
|
parse ReplayGain tags, but also allow volume amplification above 100%.
|
||||||
|
- `state` contains the core playback state and persistence system.
|
||||||
- `system` contains the system-facing playback system, i.e `PlaybackService`
|
- `system` contains the system-facing playback system, i.e `PlaybackService`
|
||||||
|
|
||||||
The base package contains the user-facing UIs representing the playback state, specifically the playback bar and the
|
The base package contains the user-facing UIs representing the playback state, specifically the playback bar and the
|
||||||
|
|