Change music storage to shared object

Change MusicViewModel from a ViewModel to a shared object, so that it can be used everywhere in the app instead of just in fragments.
This commit is contained in:
OxygenCobalt 2020-10-13 16:44:14 -06:00
parent 3376b57f8e
commit 96c30b3f93
15 changed files with 253 additions and 233 deletions

View file

@ -14,7 +14,8 @@ import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator import com.google.android.material.tabs.TabLayoutMediator
import org.oxycblt.auxio.databinding.FragmentMainBinding import org.oxycblt.auxio.databinding.FragmentMainBinding
import org.oxycblt.auxio.library.LibraryFragment import org.oxycblt.auxio.library.LibraryFragment
import org.oxycblt.auxio.music.MusicViewModel import org.oxycblt.auxio.loading.LoadingViewModel
import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.songs.SongsFragment import org.oxycblt.auxio.songs.SongsFragment
import org.oxycblt.auxio.theme.accent import org.oxycblt.auxio.theme.accent
import org.oxycblt.auxio.theme.getInactiveAlpha import org.oxycblt.auxio.theme.getInactiveAlpha
@ -22,8 +23,8 @@ import org.oxycblt.auxio.theme.getTransparentAccent
import org.oxycblt.auxio.theme.toColor import org.oxycblt.auxio.theme.toColor
class MainFragment : Fragment() { class MainFragment : Fragment() {
private val musicModel: MusicViewModel by activityViewModels { private val loadingModel: LoadingViewModel by activityViewModels {
MusicViewModel.Factory(requireActivity().application) LoadingViewModel.Factory(requireActivity().application)
} }
private val shownFragments = listOf(0, 1) private val shownFragments = listOf(0, 1)
@ -39,9 +40,9 @@ class MainFragment : Fragment() {
): View? { ): View? {
val binding = FragmentMainBinding.inflate(inflater) val binding = FragmentMainBinding.inflate(inflater)
// If musicModel was cleared while the app was closed [Likely due to Auxio being suspended // If the music was cleared while the app was closed [Likely due to Auxio being suspended
// in the background], then navigate back to LoadingFragment to reload the music. // in the background], then navigate back to LoadingFragment to reload the music.
if (musicModel.response.value == null) { if (MusicStore.getInstance().songs.isEmpty()) {
findNavController().navigate(MainFragmentDirections.actionReturnToLoading()) findNavController().navigate(MainFragmentDirections.actionReturnToLoading())
return null return null

View file

@ -12,7 +12,7 @@ import androidx.navigation.fragment.navArgs
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentAlbumDetailBinding import org.oxycblt.auxio.databinding.FragmentAlbumDetailBinding
import org.oxycblt.auxio.detail.adapters.DetailSongAdapter import org.oxycblt.auxio.detail.adapters.DetailSongAdapter
import org.oxycblt.auxio.music.MusicViewModel import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.theme.applyDivider import org.oxycblt.auxio.theme.applyDivider
import org.oxycblt.auxio.theme.disable import org.oxycblt.auxio.theme.disable
@ -22,7 +22,6 @@ class AlbumDetailFragment : Fragment() {
private val args: AlbumDetailFragmentArgs by navArgs() private val args: AlbumDetailFragmentArgs by navArgs()
private val detailModel: DetailViewModel by activityViewModels() private val detailModel: DetailViewModel by activityViewModels()
private val playbackModel: PlaybackViewModel by activityViewModels() private val playbackModel: PlaybackViewModel by activityViewModels()
private val musicModel: MusicViewModel by activityViewModels()
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
@ -31,22 +30,21 @@ class AlbumDetailFragment : Fragment() {
): View? { ): View? {
val binding = FragmentAlbumDetailBinding.inflate(inflater) val binding = FragmentAlbumDetailBinding.inflate(inflater)
// If DetailViewModel isn't already storing the album, get it from MusicViewModel // If DetailViewModel isn't already storing the album, get it from MusicStore
// using the ID given by the navigation arguments. // using the ID given by the navigation arguments.
if (detailModel.currentAlbum.value == null || if (detailModel.currentAlbum.value == null ||
detailModel.currentAlbum.value?.id != args.albumId detailModel.currentAlbum.value?.id != args.albumId
) { ) {
val musicModel: MusicViewModel by activityViewModels()
detailModel.updateAlbum( detailModel.updateAlbum(
musicModel.albums.value!!.find { MusicStore.getInstance().albums.find {
it.id == args.albumId it.id == args.albumId
}!! }!!
) )
} }
val songAdapter = DetailSongAdapter { val songAdapter = DetailSongAdapter {
playbackModel.update(it, musicModel.songs.value!!) playbackModel.update(it)
} }
// --- UI SETUP --- // --- UI SETUP ---

View file

@ -11,7 +11,7 @@ import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs import androidx.navigation.fragment.navArgs
import org.oxycblt.auxio.databinding.FragmentArtistDetailBinding import org.oxycblt.auxio.databinding.FragmentArtistDetailBinding
import org.oxycblt.auxio.detail.adapters.DetailAlbumAdapter import org.oxycblt.auxio.detail.adapters.DetailAlbumAdapter
import org.oxycblt.auxio.music.MusicViewModel import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.theme.applyDivider import org.oxycblt.auxio.theme.applyDivider
import org.oxycblt.auxio.theme.disable import org.oxycblt.auxio.theme.disable
@ -30,15 +30,13 @@ class ArtistDetailFragment : Fragment() {
): View? { ): View? {
val binding = FragmentArtistDetailBinding.inflate(inflater) val binding = FragmentArtistDetailBinding.inflate(inflater)
// If DetailViewModel isn't already storing the artist, get it from MusicViewModel // If DetailViewModel isn't already storing the artist, get it from MusicStore
// using the ID given by the navigation arguments // using the ID given by the navigation arguments
if (detailModel.currentArtist.value == null || if (detailModel.currentArtist.value == null ||
detailModel.currentArtist.value?.id != args.artistId detailModel.currentArtist.value?.id != args.artistId
) { ) {
val musicModel: MusicViewModel by activityViewModels()
detailModel.updateArtist( detailModel.updateArtist(
musicModel.artists.value!!.find { MusicStore.getInstance().artists.find {
it.id == args.artistId it.id == args.artistId
}!! }!!
) )

View file

@ -11,7 +11,7 @@ import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs import androidx.navigation.fragment.navArgs
import org.oxycblt.auxio.databinding.FragmentGenreDetailBinding import org.oxycblt.auxio.databinding.FragmentGenreDetailBinding
import org.oxycblt.auxio.detail.adapters.DetailArtistAdapter import org.oxycblt.auxio.detail.adapters.DetailArtistAdapter
import org.oxycblt.auxio.music.MusicViewModel import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.theme.applyDivider import org.oxycblt.auxio.theme.applyDivider
import org.oxycblt.auxio.theme.disable import org.oxycblt.auxio.theme.disable
@ -27,15 +27,13 @@ class GenreDetailFragment : Fragment() {
): View? { ): View? {
val binding = FragmentGenreDetailBinding.inflate(inflater) val binding = FragmentGenreDetailBinding.inflate(inflater)
// If DetailViewModel isn't already storing the genre, get it from MusicViewModel // If DetailViewModel isn't already storing the genre, get it from MusicStore
// using the ID given by the navigation arguments // using the ID given by the navigation arguments
if (detailModel.currentGenre.value == null || if (detailModel.currentGenre.value == null ||
detailModel.currentGenre.value?.id != args.genreId detailModel.currentGenre.value?.id != args.genreId
) { ) {
val musicModel: MusicViewModel by activityViewModels()
detailModel.updateGenre( detailModel.updateGenre(
musicModel.genres.value!!.find { MusicStore.getInstance().genres.find {
it.id == args.genreId it.id == args.genreId
}!! }!!
) )

View file

@ -23,10 +23,9 @@ import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.BaseModel import org.oxycblt.auxio.music.BaseModel
import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.MusicViewModel import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.recycler.ShowMode
import org.oxycblt.auxio.theme.applyColor import org.oxycblt.auxio.theme.applyColor
import org.oxycblt.auxio.theme.applyDivider import org.oxycblt.auxio.theme.applyDivider
import org.oxycblt.auxio.theme.resolveAttr import org.oxycblt.auxio.theme.resolveAttr
@ -34,10 +33,6 @@ import org.oxycblt.auxio.theme.resolveAttr
// A Fragment to show all the music in the Library. // A Fragment to show all the music in the Library.
class LibraryFragment : Fragment(), SearchView.OnQueryTextListener { class LibraryFragment : Fragment(), SearchView.OnQueryTextListener {
private val musicModel: MusicViewModel by activityViewModels {
MusicViewModel.Factory(requireActivity().application)
}
private val libraryModel: LibraryViewModel by activityViewModels() private val libraryModel: LibraryViewModel by activityViewModels()
private val playbackModel: PlaybackViewModel by activityViewModels() private val playbackModel: PlaybackViewModel by activityViewModels()
@ -48,6 +43,8 @@ class LibraryFragment : Fragment(), SearchView.OnQueryTextListener {
): View? { ): View? {
val binding = FragmentLibraryBinding.inflate(inflater) val binding = FragmentLibraryBinding.inflate(inflater)
val musicStore = MusicStore.getInstance()
val libraryAdapter = LibraryAdapter(libraryModel.showMode.value!!) { val libraryAdapter = LibraryAdapter(libraryModel.showMode.value!!) {
navToItem(it) navToItem(it)
} }
@ -137,13 +134,7 @@ class LibraryFragment : Fragment(), SearchView.OnQueryTextListener {
// Update the adapter with the new data // Update the adapter with the new data
libraryAdapter.updateData( libraryAdapter.updateData(
mode.getSortedBaseModelList( mode.getSortedBaseModelList(
when (libraryModel.showMode.value) { musicStore.getListForShowMode(libraryModel.showMode.value!!)
ShowMode.SHOW_GENRES -> musicModel.genres.value!!
ShowMode.SHOW_ARTISTS -> musicModel.artists.value!!
ShowMode.SHOW_ALBUMS -> musicModel.albums.value!!
else -> musicModel.artists.value!!
}
) )
) )
@ -179,7 +170,7 @@ class LibraryFragment : Fragment(), SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String): Boolean = false override fun onQueryTextSubmit(query: String): Boolean = false
override fun onQueryTextChange(query: String): Boolean { override fun onQueryTextChange(query: String): Boolean {
libraryModel.updateSearchQuery(query, musicModel) libraryModel.updateSearchQuery(query)
return false return false
} }
@ -190,7 +181,7 @@ class LibraryFragment : Fragment(), SearchView.OnQueryTextListener {
// If the item is a song [That was selected through search], then update the playback // If the item is a song [That was selected through search], then update the playback
// to that song instead of doing any navigation // to that song instead of doing any navigation
if (baseModel is Song) { if (baseModel is Song) {
playbackModel.update(baseModel, musicModel.songs.value!!) playbackModel.update(baseModel)
return return
} }

View file

@ -9,7 +9,7 @@ import kotlinx.coroutines.launch
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.music.BaseModel import org.oxycblt.auxio.music.BaseModel
import org.oxycblt.auxio.music.Header import org.oxycblt.auxio.music.Header
import org.oxycblt.auxio.music.MusicViewModel import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.recycler.ShowMode import org.oxycblt.auxio.recycler.ShowMode
import org.oxycblt.auxio.recycler.SortMode import org.oxycblt.auxio.recycler.SortMode
@ -44,7 +44,7 @@ class LibraryViewModel : ViewModel() {
} }
} }
fun updateSearchQuery(query: String, musicModel: MusicViewModel) { fun updateSearchQuery(query: String) {
// Don't bother if the query is blank. // Don't bother if the query is blank.
if (query == "") { if (query == "") {
resetQuery() resetQuery()
@ -52,16 +52,17 @@ class LibraryViewModel : ViewModel() {
return return
} }
// Search MusicViewModel for all the items [Artists, Albums, Songs] that contain // Search MusicStore for all the items [Artists, Albums, Songs] that contain
// the query, and update the LiveData with those items. This is done on a separate // the query, and update the LiveData with those items. This is done on a separate
// thread as it can be a very long operation for large music libraries. // thread as it can be a very long operation for large music libraries.
viewModelScope.launch { viewModelScope.launch {
val musicStore = MusicStore.getInstance()
val combined = mutableListOf<BaseModel>() val combined = mutableListOf<BaseModel>()
val children = showMode.value!!.getChildren() val children = showMode.value!!.getChildren()
// If the Library ShowMode supports it, include artists / genres in the search. // If the Library ShowMode supports it, include artists / genres in the search.
if (children.contains(ShowMode.SHOW_GENRES)) { if (children.contains(ShowMode.SHOW_GENRES)) {
val genres = musicModel.genres.value!!.filter { it.name.contains(query, true) } val genres = musicStore.genres.filter { it.name.contains(query, true) }
if (genres.isNotEmpty()) { if (genres.isNotEmpty()) {
combined.add(Header(id = ShowMode.SHOW_GENRES.constant)) combined.add(Header(id = ShowMode.SHOW_GENRES.constant))
@ -70,7 +71,7 @@ class LibraryViewModel : ViewModel() {
} }
if (children.contains(ShowMode.SHOW_ARTISTS)) { if (children.contains(ShowMode.SHOW_ARTISTS)) {
val artists = musicModel.artists.value!!.filter { it.name.contains(query, true) } val artists = musicStore.artists.filter { it.name.contains(query, true) }
if (artists.isNotEmpty()) { if (artists.isNotEmpty()) {
combined.add(Header(id = ShowMode.SHOW_ARTISTS.constant)) combined.add(Header(id = ShowMode.SHOW_ARTISTS.constant))
@ -79,14 +80,14 @@ class LibraryViewModel : ViewModel() {
} }
// Albums & Songs are always included. // Albums & Songs are always included.
val albums = musicModel.albums.value!!.filter { it.name.contains(query, true) } val albums = musicStore.albums.filter { it.name.contains(query, true) }
if (albums.isNotEmpty()) { if (albums.isNotEmpty()) {
combined.add(Header(id = ShowMode.SHOW_ALBUMS.constant)) combined.add(Header(id = ShowMode.SHOW_ALBUMS.constant))
combined.addAll(albums) combined.addAll(albums)
} }
val songs = musicModel.songs.value!!.filter { it.name.contains(query, true) } val songs = musicStore.songs.filter { it.name.contains(query, true) }
if (songs.isNotEmpty()) { if (songs.isNotEmpty()) {
combined.add(Header(id = ShowMode.SHOW_SONGS.constant)) combined.add(Header(id = ShowMode.SHOW_SONGS.constant))

View file

@ -14,13 +14,12 @@ import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentLoadingBinding import org.oxycblt.auxio.databinding.FragmentLoadingBinding
import org.oxycblt.auxio.music.MusicViewModel
import org.oxycblt.auxio.music.processing.MusicLoaderResponse import org.oxycblt.auxio.music.processing.MusicLoaderResponse
class LoadingFragment : Fragment(R.layout.fragment_loading) { class LoadingFragment : Fragment(R.layout.fragment_loading) {
private val musicModel: MusicViewModel by activityViewModels { private val loadingModel: LoadingViewModel by activityViewModels {
MusicViewModel.Factory(requireActivity().application) LoadingViewModel.Factory(requireActivity().application)
} }
override fun onCreateView( override fun onCreateView(
@ -39,18 +38,18 @@ class LoadingFragment : Fragment(R.layout.fragment_loading) {
if (granted) { if (granted) {
returnToLoading(binding) returnToLoading(binding)
musicModel.reload() loadingModel.reload()
} }
} }
// --- UI SETUP --- // --- UI SETUP ---
binding.lifecycleOwner = this binding.lifecycleOwner = this
binding.musicModel = musicModel binding.loadingModel = loadingModel
// --- VIEWMODEL SETUP --- // --- VIEWMODEL SETUP ---
musicModel.response.observe(viewLifecycleOwner) { loadingModel.response.observe(viewLifecycleOwner) {
if (it == MusicLoaderResponse.DONE) { if (it == MusicLoaderResponse.DONE) {
findNavController().navigate( findNavController().navigate(
LoadingFragmentDirections.actionToMain() LoadingFragmentDirections.actionToMain()
@ -69,17 +68,17 @@ class LoadingFragment : Fragment(R.layout.fragment_loading) {
} }
} }
musicModel.doReload.observe(viewLifecycleOwner) { loadingModel.doReload.observe(viewLifecycleOwner) {
if (it) { if (it) {
returnToLoading(binding) returnToLoading(binding)
musicModel.doneWithReload() loadingModel.doneWithReload()
} }
} }
musicModel.doGrant.observe(viewLifecycleOwner) { loadingModel.doGrant.observe(viewLifecycleOwner) {
if (it) { if (it) {
permLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE) permLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE)
musicModel.doneWithGrant() loadingModel.doneWithGrant()
} }
} }
@ -90,7 +89,7 @@ class LoadingFragment : Fragment(R.layout.fragment_loading) {
binding.loadingGrantButton.visibility = View.VISIBLE binding.loadingGrantButton.visibility = View.VISIBLE
binding.loadingErrorText.text = getString(R.string.error_no_perms) binding.loadingErrorText.text = getString(R.string.error_no_perms)
} else { } else {
musicModel.go() loadingModel.go()
} }
Log.d(this::class.simpleName, "Fragment created.") Log.d(this::class.simpleName, "Fragment created.")
@ -109,7 +108,7 @@ class LoadingFragment : Fragment(R.layout.fragment_loading) {
) == PackageManager.PERMISSION_DENIED ) == PackageManager.PERMISSION_DENIED
} }
// Remove the loading ui_indicator and show the error groups // Remove the loading indicator and show the error groups
private fun showError(binding: FragmentLoadingBinding) { private fun showError(binding: FragmentLoadingBinding) {
binding.loadingBar.visibility = View.GONE binding.loadingBar.visibility = View.GONE
binding.loadingErrorIcon.visibility = View.VISIBLE binding.loadingErrorIcon.visibility = View.VISIBLE

View file

@ -0,0 +1,95 @@
package org.oxycblt.auxio.loading
import android.app.Application
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.music.processing.MusicLoaderResponse
class LoadingViewModel(private val app: Application) : ViewModel() {
// Coroutine
private val loadingJob = Job()
private val ioScope = CoroutineScope(
loadingJob + Dispatchers.IO
)
// UI control
private val mResponse = MutableLiveData<MusicLoaderResponse>()
val response: LiveData<MusicLoaderResponse> get() = mResponse
private val mRedo = MutableLiveData<Boolean>()
val doReload: LiveData<Boolean> get() = mRedo
private val mDoGrant = MutableLiveData<Boolean>()
val doGrant: LiveData<Boolean> get() = mDoGrant
private var started = false
// Start the music loading sequence.
// This should only be ran once, use reload() for all other loads.
fun go() {
if (!started) {
started = true
doLoad()
}
}
private fun doLoad() {
ioScope.launch {
val musicStore = MusicStore.getInstance()
val response = musicStore.load(app)
withContext(Dispatchers.Main) {
mResponse.value = response
}
}
}
// UI communication functions
// LoadingFragment uses these so that button presses can update the ViewModel.
// all doneWithX functions are to reset the value so that LoadingFragment doesn't
// repeat commands if the view is recreated.
fun reload() {
mRedo.value = true
doLoad()
}
fun doneWithReload() {
mRedo.value = false
}
fun grant() {
mDoGrant.value = true
}
fun doneWithGrant() {
mDoGrant.value = false
}
override fun onCleared() {
super.onCleared()
// Cancel the current loading job if the app has been stopped
loadingJob.cancel()
}
class Factory(private val application: Application) : ViewModelProvider.Factory {
@Suppress("unchecked_cast")
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(LoadingViewModel::class.java)) {
return LoadingViewModel(application) as T
}
throw IllegalArgumentException("Unknown ViewModel class.")
}
}
}

View file

@ -0,0 +1,100 @@
package org.oxycblt.auxio.music
import android.app.Application
import android.util.Log
import org.oxycblt.auxio.R
import org.oxycblt.auxio.music.processing.MusicLoader
import org.oxycblt.auxio.music.processing.MusicLoaderResponse
import org.oxycblt.auxio.music.processing.MusicSorter
import org.oxycblt.auxio.recycler.ShowMode
// Storage for Music Data. Only use getInstance() to access this object.
class MusicStore private constructor() {
private var mGenres = listOf<Genre>()
val genres: List<Genre> get() = mGenres
private var mArtists = listOf<Artist>()
val artists: List<Artist> get() = mArtists
private var mAlbums = listOf<Album>()
val albums: List<Album> get() = mAlbums
private var mSongs = listOf<Song>()
val songs: List<Song> get() = mSongs
suspend fun load(app: Application): MusicLoaderResponse {
Log.i(this::class.simpleName, "Starting initial music load...")
val start = System.currentTimeMillis()
// Get the placeholder strings, which are used by MusicLoader & MusicSorter for
// any music that doesn't have metadata.
val genrePlaceholder = app.getString(R.string.placeholder_genre)
val artistPlaceholder = app.getString(R.string.placeholder_artist)
val albumPlaceholder = app.getString(R.string.placeholder_album)
val loader = MusicLoader(
app.contentResolver,
genrePlaceholder,
artistPlaceholder,
albumPlaceholder
)
if (loader.response == MusicLoaderResponse.DONE) {
// If the loading succeeds, then sort the songs and update the value
val sorter = MusicSorter(
loader.genres,
loader.artists,
loader.albums,
loader.songs,
genrePlaceholder,
artistPlaceholder,
albumPlaceholder
)
mSongs = sorter.songs.toList()
mAlbums = sorter.albums.toList()
mArtists = sorter.artists.toList()
mGenres = sorter.genres.toList()
val elapsed = System.currentTimeMillis() - start
Log.i(
this::class.simpleName,
"Music load completed successfully in ${elapsed}ms."
)
}
return loader.response
}
fun getListForShowMode(showMode: ShowMode): List<BaseModel> {
return when (showMode) {
ShowMode.SHOW_GENRES -> mGenres
ShowMode.SHOW_ARTISTS -> mArtists
ShowMode.SHOW_ALBUMS -> mAlbums
ShowMode.SHOW_SONGS -> mSongs
}
}
companion object {
@Volatile
private var INSTANCE: MusicStore? = null
fun getInstance(): MusicStore {
val currentInstance = INSTANCE
if (currentInstance != null) {
return currentInstance
}
synchronized(this) {
val newInstance = MusicStore()
INSTANCE = newInstance
return newInstance
}
}
}
}

View file

@ -1,154 +0,0 @@
package org.oxycblt.auxio.music
import android.app.Application
import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.oxycblt.auxio.R
import org.oxycblt.auxio.music.processing.MusicLoader
import org.oxycblt.auxio.music.processing.MusicLoaderResponse
import org.oxycblt.auxio.music.processing.MusicSorter
// ViewModel for music storage.
// TODO: Move genre usage to songs [If there's a way to find songs without a genre]
class MusicViewModel(private val app: Application) : ViewModel() {
// Coroutine
private val loadingJob = Job()
private val ioScope = CoroutineScope(
loadingJob + Dispatchers.IO
)
// Values
private val mGenres = MutableLiveData<List<Genre>>()
val genres: LiveData<List<Genre>> get() = mGenres
private val mArtists = MutableLiveData<List<Artist>>()
val artists: LiveData<List<Artist>> get() = mArtists
private val mAlbums = MutableLiveData<List<Album>>()
val albums: LiveData<List<Album>> get() = mAlbums
private val mSongs = MutableLiveData<List<Song>>()
val songs: LiveData<List<Song>> get() = mSongs
private val mResponse = MutableLiveData<MusicLoaderResponse>()
val response: LiveData<MusicLoaderResponse> get() = mResponse
// UI control
private val mRedo = MutableLiveData<Boolean>()
val doReload: LiveData<Boolean> get() = mRedo
private val mDoGrant = MutableLiveData<Boolean>()
val doGrant: LiveData<Boolean> get() = mDoGrant
private var started = false
// Start the music loading sequence.
// This should only be ran once, use reload() for all other loads.
fun go() {
if (!started) {
started = true
doLoad()
}
}
private fun doLoad() {
Log.i(this::class.simpleName, "Starting initial music load...")
ioScope.launch {
val start = System.currentTimeMillis()
// Get the placeholder strings, which are used by MusicLoader & MusicSorter for
// any music that doesn't have metadata.
val genrePlaceholder = app.getString(R.string.placeholder_genre)
val artistPlaceholder = app.getString(R.string.placeholder_artist)
val albumPlaceholder = app.getString(R.string.placeholder_album)
val loader = MusicLoader(
app.contentResolver,
genrePlaceholder,
artistPlaceholder,
albumPlaceholder
)
withContext(Dispatchers.Main) {
if (loader.response == MusicLoaderResponse.DONE) {
// If the loading succeeds, then sort the songs and update the value
val sorter = MusicSorter(
loader.genres,
loader.artists,
loader.albums,
loader.songs,
genrePlaceholder,
artistPlaceholder,
albumPlaceholder
)
mSongs.value = sorter.songs.toList()
mAlbums.value = sorter.albums.toList()
mArtists.value = sorter.artists.toList()
mGenres.value = sorter.genres.toList()
}
mResponse.value = loader.response
val elapsed = System.currentTimeMillis() - start
Log.i(
this::class.simpleName,
"Music load completed successfully in ${elapsed}ms."
)
}
}
}
// UI communication functions
// LoadingFragment uses these so that button presses can update the ViewModel.
// all doneWithX functions are to reset the value so that LoadingFragment doesn't
// repeat commands if the view is recreated.
fun reload() {
mRedo.value = true
doLoad()
}
fun doneWithReload() {
mRedo.value = false
}
fun grant() {
mDoGrant.value = true
}
fun doneWithGrant() {
mDoGrant.value = false
}
override fun onCleared() {
super.onCleared()
// Cancel the current loading job if the app has been stopped
loadingJob.cancel()
}
class Factory(private val application: Application) : ViewModelProvider.Factory {
@Suppress("unchecked_cast")
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(MusicViewModel::class.java)) {
return MusicViewModel(application) as T
}
throw IllegalArgumentException("Unknown ViewModel class.")
}
}
}

View file

@ -13,14 +13,9 @@ import androidx.navigation.fragment.findNavController
import org.oxycblt.auxio.MainFragmentDirections import org.oxycblt.auxio.MainFragmentDirections
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentCompactPlaybackBinding import org.oxycblt.auxio.databinding.FragmentCompactPlaybackBinding
import org.oxycblt.auxio.music.MusicViewModel import org.oxycblt.auxio.music.MusicStore
import kotlin.time.seconds
class CompactPlaybackFragment : Fragment() { class CompactPlaybackFragment : Fragment() {
private val musicModel: MusicViewModel by activityViewModels {
MusicViewModel.Factory(requireActivity().application)
}
private val playbackModel: PlaybackViewModel by activityViewModels() private val playbackModel: PlaybackViewModel by activityViewModels()
override fun onCreateView( override fun onCreateView(
@ -44,7 +39,7 @@ class CompactPlaybackFragment : Fragment() {
// Put a placeholder song in the binding & hide the playback fragment initially, // Put a placeholder song in the binding & hide the playback fragment initially,
// as for some reason the attach event doesn't register anymore w/LiveData // as for some reason the attach event doesn't register anymore w/LiveData
binding.song = musicModel.songs.value!![0] binding.song = MusicStore.getInstance().songs[0]
binding.playbackModel = playbackModel binding.playbackModel = playbackModel
binding.root.visibility = View.GONE binding.root.visibility = View.GONE

View file

@ -78,12 +78,9 @@ class PlaybackFragment : Fragment(), SeekBar.OnSeekBarChangeListener {
binding.playbackSkipPrev.disable(requireContext()) binding.playbackSkipPrev.disable(requireContext())
} }
Log.d(this::class.simpleName, it.toString())
if (it < playbackModel.queue.value!!.lastIndex) { if (it < playbackModel.queue.value!!.lastIndex) {
binding.playbackSkipNext.enable(requireContext()) binding.playbackSkipNext.enable(requireContext())
} else { } else {
Log.d(this::class.simpleName, "Fucking stupid retard.")
binding.playbackSkipNext.disable(requireContext()) binding.playbackSkipNext.disable(requireContext())
} }
} }

View file

@ -4,6 +4,7 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Transformations import androidx.lifecycle.Transformations
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.toDuration import org.oxycblt.auxio.music.toDuration
@ -40,11 +41,13 @@ class PlaybackViewModel : ViewModel() {
} }
// Update the current song while changing the queue to All Songs. // Update the current song while changing the queue to All Songs.
fun update(song: Song, allSongs: List<Song>) { fun update(song: Song) {
val musicStore = MusicStore.getInstance()
updatePlayback(song) updatePlayback(song)
mQueue.value = allSongs.toMutableList() mQueue.value = musicStore.songs.toMutableList()
mCurrentIndex.value = allSongs.indexOf(song) mCurrentIndex.value = musicStore.songs.indexOf(song)
} }
private fun updatePlayback(song: Song) { private fun updatePlayback(song: Song) {

View file

@ -8,15 +8,11 @@ import android.view.ViewGroup
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import org.oxycblt.auxio.databinding.FragmentSongsBinding import org.oxycblt.auxio.databinding.FragmentSongsBinding
import org.oxycblt.auxio.music.MusicViewModel import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.theme.applyDivider import org.oxycblt.auxio.theme.applyDivider
class SongsFragment : Fragment() { class SongsFragment : Fragment() {
private val musicModel: MusicViewModel by activityViewModels {
MusicViewModel.Factory(requireActivity().application)
}
private val playbackModel: PlaybackViewModel by activityViewModels() private val playbackModel: PlaybackViewModel by activityViewModels()
override fun onCreateView( override fun onCreateView(
@ -26,14 +22,16 @@ class SongsFragment : Fragment() {
): View? { ): View? {
val binding = FragmentSongsBinding.inflate(inflater) val binding = FragmentSongsBinding.inflate(inflater)
val musicStore = MusicStore.getInstance()
// TODO: Add option to search songs if LibraryFragment isn't enabled // TODO: Add option to search songs if LibraryFragment isn't enabled
// TODO: Maybe add fast scrolling or sorting // TODO: Maybe add fast scrolling or sorting
// --- UI SETUP --- // --- UI SETUP ---
binding.songRecycler.apply { binding.songRecycler.apply {
adapter = SongAdapter(musicModel.songs.value!!) { adapter = SongAdapter(musicStore.songs) {
playbackModel.update(it, musicModel.songs.value!!) playbackModel.update(it)
} }
applyDivider() applyDivider()
setHasFixedSize(true) setHasFixedSize(true)

View file

@ -7,8 +7,8 @@
<data> <data>
<variable <variable
name="musicModel" name="loadingModel"
type="org.oxycblt.auxio.music.MusicViewModel" /> type="org.oxycblt.auxio.loading.LoadingViewModel" />
</data> </data>
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
@ -66,7 +66,7 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:fontFamily="@font/inter_semibold" android:fontFamily="@font/inter_semibold"
android:onClick="@{() -> musicModel.reload()}" android:onClick="@{() -> loadingModel.reload()}"
android:text="@string/label_retry" android:text="@string/label_retry"
android:textColor="?attr/colorPrimary" android:textColor="?attr/colorPrimary"
android:visibility="gone" android:visibility="gone"
@ -85,7 +85,7 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:fontFamily="@font/inter_semibold" android:fontFamily="@font/inter_semibold"
android:onClick="@{() -> musicModel.grant()}" android:onClick="@{() -> loadingModel.grant()}"
android:text="@string/label_grant" android:text="@string/label_grant"
android:textColor="?attr/colorPrimary" android:textColor="?attr/colorPrimary"
android:visibility="gone" android:visibility="gone"