fastlane: update screenshots

Update app screenshots for 2.3.0. Notably, a new album screenshot has
been added showcasing the new disc functionality.
This commit is contained in:
OxygenCobalt 2022-05-26 15:51:17 -06:00
parent f34ea7f863
commit c0da3673c2
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
10 changed files with 65 additions and 64 deletions

View file

@ -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
}
} }
} }
} }

View file

@ -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)
} }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 160 KiB

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 137 KiB

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 133 KiB

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 296 KiB

After

Width:  |  Height:  |  Size: 295 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 166 KiB

After

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 152 KiB

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 254 KiB

After

Width:  |  Height:  |  Size: 246 KiB

View file

@ -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