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:
parent
24452e8fa4
commit
6508280900
5 changed files with 253 additions and 215 deletions
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
174
app/src/main/java/org/oxycblt/auxio/music/MusicLoader.kt
Normal file
174
app/src/main/java/org/oxycblt/auxio/music/MusicLoader.kt
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
51
app/src/main/java/org/oxycblt/auxio/music/MusicSorting.kt
Normal file
51
app/src/main/java/org/oxycblt/auxio/music/MusicSorting.kt
Normal 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
|
||||
}
|
Loading…
Reference in a new issue