From 6508280900bf91f4865f907baf2f998b0a7d951b Mon Sep 17 00:00:00 2001 From: OxygenCobalt Date: Wed, 19 Aug 2020 11:14:20 -0600 Subject: [PATCH] Break up MusicRepository into seperate files Remove the loading/sorting functions from MusicRepository and place them into their own files. --- .../oxycblt/auxio/loading/LoadingFragment.kt | 8 +- .../oxycblt/auxio/loading/LoadingViewModel.kt | 6 +- .../org/oxycblt/auxio/music/MusicLoader.kt | 174 +++++++++++++ .../oxycblt/auxio/music/MusicRepository.kt | 229 ++---------------- .../org/oxycblt/auxio/music/MusicSorting.kt | 51 ++++ 5 files changed, 253 insertions(+), 215 deletions(-) create mode 100644 app/src/main/java/org/oxycblt/auxio/music/MusicLoader.kt create mode 100644 app/src/main/java/org/oxycblt/auxio/music/MusicSorting.kt diff --git a/app/src/main/java/org/oxycblt/auxio/loading/LoadingFragment.kt b/app/src/main/java/org/oxycblt/auxio/loading/LoadingFragment.kt index 823bc8363..4e4253c92 100644 --- a/app/src/main/java/org/oxycblt/auxio/loading/LoadingFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/loading/LoadingFragment.kt @@ -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) diff --git a/app/src/main/java/org/oxycblt/auxio/loading/LoadingViewModel.kt b/app/src/main/java/org/oxycblt/auxio/loading/LoadingViewModel.kt index aba0da2f7..8b318613f 100644 --- a/app/src/main/java/org/oxycblt/auxio/loading/LoadingViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/loading/LoadingViewModel.kt @@ -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() - val musicRepoResponse: LiveData get() = mMusicRepoResponse + private val mMusicRepoResponse = MutableLiveData() + val musicRepoResponse: LiveData get() = mMusicRepoResponse private val mDoRetry = MutableLiveData() val doRetry: LiveData get() = mDoRetry diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicLoader.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicLoader.kt new file mode 100644 index 000000000..8e36c028a --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicLoader.kt @@ -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() + + 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() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt index f9525e756..a8c35d25f 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt @@ -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 - private lateinit var mAlbums: List - private lateinit var mSongs: List + private val mArtists = MutableLiveData>() + private var mAlbums = MutableLiveData>() + private var mSongs = MutableLiveData>() - // Not sure if backings are necessary but they're vars so better safe than sorry - val artists: List get() = mArtists - val albums: List get() = mAlbums - val songs: List get() = mSongs + val artists: LiveData> get() = mArtists + val albums: LiveData> get() = mAlbums + val songs: LiveData> 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) + mArtists.value = sortIntoArtists(mAlbums.value as MutableList) } } - // 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? { - - val songList = mutableListOf() - 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) { - // 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() - - 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() - - 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 { diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicSorting.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicSorting.kt new file mode 100644 index 000000000..ea5c9a645 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicSorting.kt @@ -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) : MutableList { + // 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) : MutableList { + val songsByAlbum = songs.groupBy { it.album } + val albumList = mutableListOf() + + 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) : MutableList { + val albumsByArtist = albums.groupBy { it.artist } + val artistList = mutableListOf() + + albumsByArtist.keys.iterator().forEach { artist -> + val artistAlbums = albumsByArtist[artist] + + artistAlbums?.let { + artistList.add( + Artist(artistAlbums) + ) + } + } + + return artistList +} \ No newline at end of file