Break up MusicRepository into seperate files

Remove the loading/sorting functions from MusicRepository and place them into their own files.
This commit is contained in:
OxygenCobalt 2020-08-19 11:14:20 -06:00
parent 24452e8fa4
commit 6508280900
5 changed files with 253 additions and 215 deletions

View file

@ -12,7 +12,7 @@ import androidx.lifecycle.ViewModelProvider
import androidx.navigation.fragment.findNavController
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentLoadingBinding
import org.oxycblt.auxio.music.MusicLoadResponse
import org.oxycblt.auxio.music.MusicLoaderResponse
class LoadingFragment : Fragment() {
@ -58,12 +58,12 @@ class LoadingFragment : Fragment() {
return binding.root
}
private fun onMusicLoadResponse(repoResponse: MusicLoadResponse?) {
private fun onMusicLoadResponse(repoResponse: MusicLoaderResponse?) {
// Don't run this if the value is null, Which is what the value changes to after
// this is run.
repoResponse?.let { response ->
if (response == MusicLoadResponse.DONE) {
if (response == MusicLoaderResponse.DONE) {
this.findNavController().navigate(
LoadingFragmentDirections.actionToLibrary()
)
@ -75,7 +75,7 @@ class LoadingFragment : Fragment() {
binding.statusText.visibility = View.VISIBLE
binding.resetButton.visibility = View.VISIBLE
if (response == MusicLoadResponse.NO_MUSIC) {
if (response == MusicLoaderResponse.NO_MUSIC) {
binding.statusText.text = getString(R.string.error_no_music)
} else {
binding.statusText.text = getString(R.string.error_music_load_failed)

View file

@ -11,7 +11,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.oxycblt.auxio.music.MusicLoadResponse
import org.oxycblt.auxio.music.MusicLoaderResponse
import org.oxycblt.auxio.music.MusicRepository
class LoadingViewModel(private val app: Application) : ViewModel() {
@ -21,8 +21,8 @@ class LoadingViewModel(private val app: Application) : ViewModel() {
Dispatchers.IO
)
private val mMusicRepoResponse = MutableLiveData<MusicLoadResponse>()
val musicRepoResponse: LiveData<MusicLoadResponse> get() = mMusicRepoResponse
private val mMusicRepoResponse = MutableLiveData<MusicLoaderResponse>()
val musicRepoResponse: LiveData<MusicLoaderResponse> get() = mMusicRepoResponse
private val mDoRetry = MutableLiveData<Boolean>()
val doRetry: LiveData<Boolean> get() = mDoRetry

View file

@ -0,0 +1,174 @@
package org.oxycblt.auxio.music
import android.app.Application
import android.content.ContentResolver
import android.content.ContentUris
import android.database.Cursor
import android.media.MediaMetadataRetriever
import android.provider.MediaStore
import android.provider.MediaStore.Audio.AudioColumns
import android.util.Log
import org.oxycblt.auxio.music.models.Song
enum class MusicLoaderResponse {
DONE, FAILURE, NO_MUSIC
}
// Class that loads music from the FileSystem.
// This thing is probably full of memory leaks.
class MusicLoader(private val app: Application) {
var songs = mutableListOf<Song>()
private val retriever: MediaMetadataRetriever = MediaMetadataRetriever()
private var musicCursor: Cursor? = null
val response: MusicLoaderResponse
init {
response = findMusic()
}
private fun findMusic() : MusicLoaderResponse {
try {
musicCursor = getCursor(
app.contentResolver
)
Log.i(this::class.simpleName, "Starting music search...")
useCursor()
} catch (error: Exception) {
// TODO: Add better error handling
Log.e(this::class.simpleName, "Something went horribly wrong.")
error.printStackTrace()
finalize()
return MusicLoaderResponse.FAILURE
}
// If the main loading completed without a failure, return DONE or
// NO_MUSIC depending on if any music was found.
return if (songs.size > 0) {
Log.d(
this::class.simpleName,
"Successfully found " + songs.size.toString() + " Songs."
)
MusicLoaderResponse.DONE
} else {
Log.d(
this::class.simpleName,
"No music was found."
)
MusicLoaderResponse.NO_MUSIC
}
}
private fun getCursor(resolver: ContentResolver): Cursor? {
Log.i(this::class.simpleName, "Getting music cursor.")
return resolver.query(
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
arrayOf(
AudioColumns._ID,
AudioColumns.DISPLAY_NAME
),
AudioColumns.IS_MUSIC + "=1", null,
MediaStore.Audio.Media.DEFAULT_SORT_ORDER
)
}
// Use the cursor index music files from the shared storage, returns true if any were found.
private fun useCursor() {
musicCursor?.use { cursor ->
// Don't run the more expensive file loading operations if there is no music to index.
if (cursor.count == 0) {
return
}
val idIndex = cursor.getColumnIndexOrThrow(AudioColumns._ID)
val displayIndex = cursor.getColumnIndexOrThrow(AudioColumns.DISPLAY_NAME)
while (cursor.moveToNext()) {
val id = cursor.getLong(idIndex)
// Read the current file from the ID
retriever.setDataSource(
app.applicationContext,
ContentUris.withAppendedId(
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
id
)
)
// Get the metadata attributes
val title = retriever.extractMetadata(
MediaMetadataRetriever.METADATA_KEY_TITLE
) ?: cursor.getString(displayIndex)
val artist = retriever.extractMetadata(
MediaMetadataRetriever.METADATA_KEY_ARTIST
)
val album = retriever.extractMetadata(
MediaMetadataRetriever.METADATA_KEY_ALBUM
)
val genre = retriever.extractMetadata(
MediaMetadataRetriever.METADATA_KEY_GENRE
)
val year = (
retriever.extractMetadata(
MediaMetadataRetriever.METADATA_KEY_YEAR
) ?: "0"
).toInt()
// Track is formatted as X/0, so trim off the /0 part to parse
// the track number correctly.
val track = (
retriever.extractMetadata(
MediaMetadataRetriever.METADATA_KEY_CD_TRACK_NUMBER
) ?: "0/0"
).split("/")[0].toInt()
// Something has gone horribly wrong if a file has no duration,
// so assert it as such.
val duration = retriever.extractMetadata(
MediaMetadataRetriever.METADATA_KEY_DURATION
)!!.toLong()
// TODO: Add int-based genre compatibility
songs.add(
Song(
title,
artist,
album,
genre,
year,
track,
duration,
retriever.embeddedPicture,
id
)
)
}
}
}
// Free the metadata retriever & the musicCursor, and make the song list immutable.
private fun finalize() {
retriever.close()
musicCursor?.use{
it.close()
}
}
}

View file

@ -1,227 +1,40 @@
package org.oxycblt.auxio.music
import android.app.Application
import android.content.ContentResolver
import android.content.ContentUris
import android.database.Cursor
import android.media.MediaMetadataRetriever
import android.provider.MediaStore
import android.provider.MediaStore.Audio.AudioColumns
import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.oxycblt.auxio.music.models.Album
import org.oxycblt.auxio.music.models.Artist
import org.oxycblt.auxio.music.models.Song
enum class MusicLoadResponse {
DONE, FAILURE, NO_MUSIC
}
// Storage for music data. Design largely adapted from Music Player GO:
// https://github.com/enricocid/Music-Player-GO
// Storage for music data.
class MusicRepository {
private lateinit var mArtists: List<Artist>
private lateinit var mAlbums: List<Album>
private lateinit var mSongs: List<Song>
private val mArtists = MutableLiveData<List<Artist>>()
private var mAlbums = MutableLiveData<List<Album>>()
private var mSongs = MutableLiveData<List<Song>>()
// Not sure if backings are necessary but they're vars so better safe than sorry
val artists: List<Artist> get() = mArtists
val albums: List<Album> get() = mAlbums
val songs: List<Song> get() = mSongs
val artists: LiveData<List<Artist>> get() = mArtists
val albums: LiveData<List<Album>> get() = mAlbums
val songs: LiveData<List<Song>> get() = mSongs
fun init(app: Application): MusicLoadResponse {
suspend fun init(app: Application): MusicLoaderResponse {
val loader = MusicLoader(app)
findMusic(app)?.let { ss ->
return if (ss.size > 0) {
processSongs(ss)
MusicLoadResponse.DONE
} else {
MusicLoadResponse.NO_MUSIC
if (loader.response == MusicLoaderResponse.DONE) {
// If the loading succeeds, then process the songs into lists of
// songs, albums, and artists on the main thread.
withContext(Dispatchers.Main) {
mSongs.value = processSongs(loader.songs)
mAlbums.value = sortIntoAlbums(mSongs.value as MutableList<Song>)
mArtists.value = sortIntoArtists(mAlbums.value as MutableList<Album>)
}
}
// If the let function isn't run, then the loading has failed due to some Exception
// and FAILURE must be returned
return MusicLoadResponse.FAILURE
}
private fun findMusic(app: Application): MutableList<Song>? {
val songList = mutableListOf<Song>()
val retriever = MediaMetadataRetriever()
try {
val musicCursor = getCursor(
app.contentResolver
)
Log.i(this::class.simpleName, "Starting music search...")
// Index music files from shared storage
musicCursor?.use { cursor ->
// Don't run the more expensive file loading operations if there is no music
// to index.
if (cursor.count == 0) {
return songList
}
val idIndex = cursor.getColumnIndexOrThrow(AudioColumns._ID)
val displayIndex = cursor.getColumnIndexOrThrow(AudioColumns.DISPLAY_NAME)
while (cursor.moveToNext()) {
val id = cursor.getLong(idIndex)
// Read the current file from the ID
retriever.setDataSource(
app.applicationContext,
ContentUris.withAppendedId(
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
id
)
)
// Get the metadata attributes
val title = retriever.extractMetadata(
MediaMetadataRetriever.METADATA_KEY_TITLE
) ?: cursor.getString(displayIndex)
val artist = retriever.extractMetadata(
MediaMetadataRetriever.METADATA_KEY_ARTIST
)
val album = retriever.extractMetadata(
MediaMetadataRetriever.METADATA_KEY_ALBUM
)
val genre = retriever.extractMetadata(
MediaMetadataRetriever.METADATA_KEY_GENRE
)
val year = (
retriever.extractMetadata(
MediaMetadataRetriever.METADATA_KEY_YEAR
) ?: "0"
).toInt()
// Track is formatted as X/0, so trim off the /0 part to parse
// the track number correctly.
val track = (
retriever.extractMetadata(
MediaMetadataRetriever.METADATA_KEY_CD_TRACK_NUMBER
) ?: "0/0"
).split("/")[0].toInt()
// Something has gone horribly wrong if a file has no duration,
// so assert it as such.
val duration = retriever.extractMetadata(
MediaMetadataRetriever.METADATA_KEY_DURATION
)!!.toLong()
// TODO: Add int-based genre compatibility
songList.add(
Song(
title,
artist,
album,
genre,
year,
track,
duration,
retriever.embeddedPicture,
id
)
)
}
// Close the retriever/cursor so that it gets garbage collected
retriever.close()
cursor.close()
}
Log.d(
this::class.simpleName,
"Successfully found " + songList.size.toString() + " Songs."
)
return songList
} catch (error: Exception) {
// TODO: Add better error handling
Log.e(this::class.simpleName, "Something went horribly wrong.")
error.printStackTrace()
retriever.close()
return null
}
}
private fun getCursor(resolver: ContentResolver): Cursor? {
Log.i(this::class.simpleName, "Getting music cursor.")
return resolver.query(
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
arrayOf(
AudioColumns._ID,
AudioColumns.DISPLAY_NAME
),
AudioColumns.IS_MUSIC + "=1", null,
MediaStore.Audio.Media.DEFAULT_SORT_ORDER
)
}
// Sort the list of Song objects into an abstracted lis
private fun processSongs(songs: MutableList<Song>) {
// Eliminate all duplicates from the list
// excluding the ID, as that's guaranteed to be unique [I think]
val distinctSongs = songs.distinctBy {
it.name to it.artist to it.album to it.year to it.track to it.duration
}.toMutableList()
// Add an album abstraction for each group of songs
val songsByAlbum = distinctSongs.groupBy { it.album }
val albumList = mutableListOf<Album>()
songsByAlbum.keys.iterator().forEach { album ->
val albumSongs = songsByAlbum[album]
albumSongs?.let {
albumList.add(
Album(albumSongs)
)
}
}
// Then abstract the remaining albums into artist objects
val albumsByArtist = albumList.groupBy { it.artist }
val artistList = mutableListOf<Artist>()
albumsByArtist.keys.iterator().forEach { artist ->
val artistAlbums = albumsByArtist[artist]
artistAlbums?.let {
artistList.add(
Artist(artistAlbums)
)
}
}
Log.i(
this::class.simpleName,
"Successfully sorted songs into " +
artistList.size.toString() +
" Artists and " +
albumList.size.toString() +
" Albums."
)
mArtists = artistList
mAlbums = albumList
mSongs = distinctSongs
return loader.response
}
companion object {

View file

@ -0,0 +1,51 @@
package org.oxycblt.auxio.music
import android.util.Log
import org.oxycblt.auxio.music.models.Album
import org.oxycblt.auxio.music.models.Artist
import org.oxycblt.auxio.music.models.Song
// Sort a list of Song objects into lists of songs, albums, and artists.
fun processSongs(songs: MutableList<Song>) : MutableList<Song> {
// Eliminate all duplicates from the list
// excluding the ID, as that's guaranteed to be unique [I think]
return songs.distinctBy {
it.name to it.artist to it.album to it.year to it.track to it.duration
}.toMutableList()
}
// Sort a list of song objects into albums
fun sortIntoAlbums(songs: MutableList<Song>) : MutableList<Album> {
val songsByAlbum = songs.groupBy { it.album }
val albumList = mutableListOf<Album>()
songsByAlbum.keys.iterator().forEach { album ->
val albumSongs = songsByAlbum[album]
albumSongs?.let {
albumList.add(
Album(albumSongs)
)
}
}
return albumList
}
// Sort a list of album objects into artists
fun sortIntoArtists(albums: MutableList<Album>) : MutableList<Artist> {
val albumsByArtist = albums.groupBy { it.artist }
val artistList = mutableListOf<Artist>()
albumsByArtist.keys.iterator().forEach { artist ->
val artistAlbums = albumsByArtist[artist]
artistAlbums?.let {
artistList.add(
Artist(artistAlbums)
)
}
}
return artistList
}