Heavily refactor Library Management

Remove MusicRepository/All Library models and replace it with a single shared ViewModel, Move MainFragment into MainActivity, and remove the LoadingFragment -> MainFragment navigation path.
This commit is contained in:
OxygenCobalt 2020-09-07 16:12:23 -06:00
parent e9ee9d1ef1
commit 031815d746
17 changed files with 360 additions and 494 deletions

View file

@ -5,20 +5,51 @@ import android.os.Bundle
import android.util.AttributeSet import android.util.AttributeSet
import android.util.Log import android.util.Log
import android.view.View import android.view.View
import androidx.activity.result.ActivityResultLauncher
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.core.content.ContextCompat
import androidx.databinding.DataBindingUtil
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.ViewModelProvider
import androidx.viewpager2.adapter.FragmentStateAdapter
import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator
import org.oxycblt.auxio.databinding.ActivityMainBinding
import org.oxycblt.auxio.library.LibraryFragment
import org.oxycblt.auxio.music.MusicViewModel
import org.oxycblt.auxio.music.processing.MusicLoaderResponse
import org.oxycblt.auxio.library.SongsFragment
import org.oxycblt.auxio.theme.accent import org.oxycblt.auxio.theme.accent
import org.oxycblt.auxio.theme.getInactiveAlpha
import org.oxycblt.auxio.theme.getTransparentAccent
import org.oxycblt.auxio.theme.toColor
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
// Debug placeholder so I can test dark and light modes. Ignore. private val shownFragments = listOf(0, 1)
override fun onAttachedToWindow() {
super.onAttachedToWindow()
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO) private val libraryFragment: LibraryFragment by lazy { LibraryFragment() }
private val songsFragment: SongsFragment by lazy { SongsFragment() }
private val tabIcons = listOf(
R.drawable.ic_library,
R.drawable.ic_song
)
private lateinit var binding: ActivityMainBinding
private lateinit var permLauncher: ActivityResultLauncher<String>
private val musicModel: MusicViewModel by lazy {
ViewModelProvider(
this, MusicViewModel.Factory(application)
).get(MusicViewModel::class.java)
} }
override fun onCreateView(name: String, context: Context, attrs: AttributeSet): View? { override fun onCreateView(name: String, context: Context, attrs: AttributeSet): View? {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
// Apply the theme // Apply the theme
setTheme(accent.second) setTheme(accent.second)
@ -27,8 +58,92 @@ class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main) binding = DataBindingUtil.setContentView<ActivityMainBinding>(
this, R.layout.activity_main
)
binding.lifecycleOwner = this
val adapter = PagerAdapter(this)
binding.viewPager.adapter = adapter
val colorActive = accent.first.toColor(baseContext)
val colorInactive = getTransparentAccent(
baseContext,
accent.first,
getInactiveAlpha(accent.first)
)
// Link the ViewPager & Tab View
TabLayoutMediator(binding.tabs, binding.viewPager) { tab, position ->
tab.icon = ContextCompat.getDrawable(baseContext, tabIcons[position])
// Set the icon tint to deselected if its not the default tab
if (position > 0) {
tab.icon?.setTint(colorInactive)
}
// Init the fragment
fragmentAt(position)
}.attach()
// Set up the selected/deselected colors
binding.tabs.addOnTabSelectedListener(
object : TabLayout.OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab) {
tab.icon?.setTint(colorActive)
}
override fun onTabUnselected(tab: TabLayout.Tab) {
tab.icon?.setTint(colorInactive)
}
override fun onTabReselected(tab: TabLayout.Tab?) {
}
}
)
musicModel.response.observe(
this, {
if (it == MusicLoaderResponse.DONE) {
binding.loadingFragment.visibility = View.GONE
binding.viewPager.visibility = View.VISIBLE
}
}
)
musicModel.go()
Log.d(this::class.simpleName, "Activity Created.") Log.d(this::class.simpleName, "Activity Created.")
} }
private fun fragmentAt(position: Int): Fragment {
return when (position) {
0 -> libraryFragment
1 -> songsFragment
else -> libraryFragment
}
}
private inner class PagerAdapter(activity: FragmentActivity) : FragmentStateAdapter(activity) {
override fun getItemCount(): Int = shownFragments.size
override fun createFragment(position: Int): Fragment {
Log.d(this::class.simpleName, "Switching to fragment $position.")
if (shownFragments.contains(position)) {
return fragmentAt(position)
}
// Not sure how this would happen but it might
Log.e(
this::class.simpleName,
"Attempted to index a fragment that shouldn't be shown. Returning libraryFragment."
)
return libraryFragment
}
}
} }

View file

@ -1,117 +0,0 @@
package org.oxycblt.auxio
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.databinding.DataBindingUtil
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.viewpager2.adapter.FragmentStateAdapter
import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator
import org.oxycblt.auxio.databinding.FragmentMainBinding
import org.oxycblt.auxio.library.LibraryFragment
import org.oxycblt.auxio.songs.SongsFragment
import org.oxycblt.auxio.theme.accent
import org.oxycblt.auxio.theme.getInactiveAlpha
import org.oxycblt.auxio.theme.getTransparentAccent
import org.oxycblt.auxio.theme.toColor
class MainFragment : Fragment() {
private val shownFragments = listOf(0, 1)
private val libraryFragment: LibraryFragment by lazy { LibraryFragment() }
private val songsFragment: SongsFragment by lazy { SongsFragment() }
private val tabIcons = listOf(
R.drawable.ic_library,
R.drawable.ic_song
)
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val binding = DataBindingUtil.inflate<FragmentMainBinding>(
inflater, R.layout.fragment_main, container, false
)
val adapter = PagerAdapter(requireActivity())
binding.viewPager.adapter = adapter
val colorActive = accent.first.toColor(requireContext())
val colorInactive = getTransparentAccent(
requireContext(),
accent.first,
getInactiveAlpha(accent.first)
)
// Link the ViewPager & Tab View
TabLayoutMediator(binding.tabs, binding.viewPager) { tab, position ->
tab.icon = ContextCompat.getDrawable(requireContext(), tabIcons[position])
// Set the icon tint to deselected if its not the default tab
if (position > 0) {
tab.icon?.setTint(colorInactive)
}
// Init the fragment
fragmentAt(position)
}.attach()
// Set up the selected/deselected colors
binding.tabs.addOnTabSelectedListener(
object : TabLayout.OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab) {
tab.icon?.setTint(colorActive)
}
override fun onTabUnselected(tab: TabLayout.Tab) {
tab.icon?.setTint(colorInactive)
}
override fun onTabReselected(tab: TabLayout.Tab?) {
}
}
)
Log.d(this::class.simpleName, "Fragment Created.")
return binding.root
}
private fun fragmentAt(position: Int): Fragment {
return when (position) {
0 -> libraryFragment
1 -> songsFragment
else -> libraryFragment
}
}
private inner class PagerAdapter(activity: FragmentActivity) : FragmentStateAdapter(activity) {
override fun getItemCount(): Int = shownFragments.size
override fun createFragment(position: Int): Fragment {
Log.d(this::class.simpleName, "Switching to fragment $position.")
if (shownFragments.contains(position)) {
return fragmentAt(position)
}
// Not sure how this would happen but it might
Log.e(
this::class.simpleName,
"Attempted to index a fragment that shouldn't be shown. Returning libraryFragment."
)
return libraryFragment
}
}
}

View file

@ -1,5 +1,6 @@
package org.oxycblt.auxio.coil package org.oxycblt.auxio.coil
import android.content.Context
import android.net.Uri import android.net.Uri
import android.widget.ImageView import android.widget.ImageView
import androidx.databinding.BindingAdapter import androidx.databinding.BindingAdapter
@ -15,13 +16,9 @@ private var artistImageFetcher: ArtistImageFetcher? = null
// Get the cover art for a song or album // Get the cover art for a song or album
@BindingAdapter("coverArt") @BindingAdapter("coverArt")
fun ImageView.getCoverArt(song: Song) { fun ImageView.getCoverArt(song: Song) {
val request = ImageRequest.Builder(context) val request = getDefaultRequest(context, this)
.data(song.album.coverUri) .data(song.album.coverUri)
.crossfade(true) .error(R.drawable.ic_song)
.placeholder(android.R.color.transparent)
.error(R.drawable.ic_artist)
.crossfade(true)
.target(this)
.build() .build()
Coil.imageLoader(context).enqueue(request) Coil.imageLoader(context).enqueue(request)
@ -29,13 +26,9 @@ fun ImageView.getCoverArt(song: Song) {
@BindingAdapter("coverArt") @BindingAdapter("coverArt")
fun ImageView.getCoverArt(album: Album) { fun ImageView.getCoverArt(album: Album) {
val request = ImageRequest.Builder(context) val request = getDefaultRequest(context, this)
.data(album.coverUri) .data(album.coverUri)
.crossfade(true) .error(R.drawable.ic_album)
.placeholder(android.R.color.transparent)
.error(R.drawable.ic_artist)
.crossfade(true)
.target(this)
.build() .build()
Coil.imageLoader(context).enqueue(request) Coil.imageLoader(context).enqueue(request)
@ -56,26 +49,25 @@ fun ImageView.getArtistImage(artist: Artist) {
artistImageFetcher = ArtistImageFetcher(context) artistImageFetcher = ArtistImageFetcher(context)
} }
// Manually create an image request, as that's the only way to add a fetcher that getDefaultRequest(context, this)
// takes a list of uris AFAIK.
ImageRequest.Builder(context)
.data(uris) .data(uris)
.fetcher(artistImageFetcher!!) .fetcher(artistImageFetcher!!)
.crossfade(true)
.placeholder(android.R.color.transparent)
.error(R.drawable.ic_artist) .error(R.drawable.ic_artist)
.target(this)
.build() .build()
} else { } else {
ImageRequest.Builder(context) getDefaultRequest(context, this)
.data(artist.albums[0].coverUri) .data(artist.albums[0].coverUri)
.crossfade(true)
.placeholder(android.R.color.transparent)
.error(R.drawable.ic_artist) .error(R.drawable.ic_artist)
.crossfade(true)
.target(this)
.build() .build()
} }
Coil.imageLoader(context).enqueue(request) Coil.imageLoader(context).enqueue(request)
} }
// Get the base request used across the app.
private fun getDefaultRequest(context: Context, imageView: ImageView): ImageRequest.Builder {
return ImageRequest.Builder(context)
.crossfade(true)
.placeholder(android.R.color.transparent)
.target(imageView)
}

View file

@ -7,17 +7,19 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.databinding.DataBindingUtil import androidx.databinding.DataBindingUtil
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentLibraryBinding import org.oxycblt.auxio.databinding.FragmentLibraryBinding
import org.oxycblt.auxio.music.MusicViewModel
import org.oxycblt.auxio.recycler.adapters.ArtistAdapter import org.oxycblt.auxio.recycler.adapters.ArtistAdapter
import org.oxycblt.auxio.recycler.applyDivider import org.oxycblt.auxio.recycler.applyDivider
import org.oxycblt.auxio.recycler.viewholders.ClickListener import org.oxycblt.auxio.recycler.viewholders.ClickListener
class LibraryFragment : Fragment() { class LibraryFragment : Fragment() {
private val libraryModel: LibraryViewModel by lazy { private val musicModel: MusicViewModel by activityViewModels {
ViewModelProvider(this).get(LibraryViewModel::class.java) MusicViewModel.Factory(requireActivity().application)
} }
override fun onCreateView( override fun onCreateView(
@ -30,7 +32,7 @@ class LibraryFragment : Fragment() {
) )
binding.libraryRecycler.adapter = ArtistAdapter( binding.libraryRecycler.adapter = ArtistAdapter(
libraryModel.artists.value!!, musicModel.artists.value!!,
ClickListener { artist -> ClickListener { artist ->
Log.d(this::class.simpleName, artist.name) Log.d(this::class.simpleName, artist.name)
} }

View file

@ -1,27 +0,0 @@
package org.oxycblt.auxio.library
import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import org.oxycblt.auxio.music.MusicRepository
import org.oxycblt.auxio.music.models.Album
import org.oxycblt.auxio.music.models.Artist
class LibraryViewModel : ViewModel() {
private val mArtists = MutableLiveData<List<Artist>>()
private var mAlbums = MutableLiveData<List<Album>>()
val artists: LiveData<List<Artist>> get() = mArtists
val albums: LiveData<List<Album>> get() = mAlbums
init {
val repo = MusicRepository.getInstance()
mArtists.value = repo.artists
mAlbums.value = repo.albums
Log.d(this::class.simpleName, "ViewModel created.")
}
}

View file

@ -1,9 +1,8 @@
package org.oxycblt.auxio.loading package org.oxycblt.auxio.library
import android.Manifest import android.Manifest
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.os.Bundle import android.os.Bundle
import android.transition.TransitionInflater
import android.util.Log import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
@ -13,21 +12,16 @@ import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.databinding.DataBindingUtil import androidx.databinding.DataBindingUtil
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider import androidx.fragment.app.activityViewModels
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 loadingModel: LoadingViewModel by lazy { private val musicModel: MusicViewModel by activityViewModels {
ViewModelProvider( MusicViewModel.Factory(requireActivity().application)
this,
LoadingViewModel.Factory(
requireActivity().application
)
).get(LoadingViewModel::class.java)
} }
private lateinit var binding: FragmentLoadingBinding private lateinit var binding: FragmentLoadingBinding
@ -43,23 +37,23 @@ class LoadingFragment : Fragment(R.layout.fragment_loading) {
) )
binding.lifecycleOwner = this binding.lifecycleOwner = this
binding.loadingModel = loadingModel binding.musicModel = musicModel
loadingModel.musicRepoResponse.observe( musicModel.response.observe(
viewLifecycleOwner, viewLifecycleOwner,
{ response -> { response ->
onMusicLoadResponse(response) onMusicLoadResponse(response)
} }
) )
loadingModel.doRetry.observe( musicModel.doReload.observe(
viewLifecycleOwner, viewLifecycleOwner,
{ retry -> { retry ->
onRetry(retry) onRetry(retry)
} }
) )
loadingModel.doGrant.observe( musicModel.doGrant.observe(
viewLifecycleOwner, viewLifecycleOwner,
{ grant -> { grant ->
onGrant(grant) onGrant(grant)
@ -75,7 +69,7 @@ class LoadingFragment : Fragment(R.layout.fragment_loading) {
if (granted) { if (granted) {
wipeViews() wipeViews()
loadingModel.retry() musicModel.reload()
} }
} }
@ -85,7 +79,7 @@ class LoadingFragment : Fragment(R.layout.fragment_loading) {
if (checkPerms()) { if (checkPerms()) {
onNoPerms() onNoPerms()
} else { } else {
loadingModel.go() musicModel.go()
} }
Log.d(this::class.simpleName, "Fragment created.") Log.d(this::class.simpleName, "Fragment created.")
@ -110,28 +104,21 @@ class LoadingFragment : Fragment(R.layout.fragment_loading) {
repoResponse?.let { response -> repoResponse?.let { response ->
binding.loadingBar.visibility = View.GONE binding.loadingBar.visibility = View.GONE
if (response == MusicLoaderResponse.DONE) { if (response != MusicLoaderResponse.DONE) {
val inflater = TransitionInflater.from(requireContext())
exitTransition = inflater.inflateTransition(R.transition.transition_to_main)
this.findNavController().navigate(
LoadingFragmentDirections.actionToMain()
)
} else {
// If the response wasn't a success, then show the specific error message
// depending on which error response was given, along with a retry button
binding.errorText.visibility = View.VISIBLE
binding.statusIcon.visibility = View.VISIBLE
binding.retryButton.visibility = View.VISIBLE
binding.errorText.text = binding.errorText.text =
if (response == MusicLoaderResponse.NO_MUSIC) if (response == MusicLoaderResponse.NO_MUSIC)
getString(R.string.error_no_music) getString(R.string.error_no_music)
else else
getString(R.string.error_music_load_failed) getString(R.string.error_music_load_failed)
// If the response wasn't a success, then show the specific error message
// depending on which error response was given, along with a retry button
binding.errorText.visibility = View.VISIBLE
binding.statusIcon.visibility = View.VISIBLE
binding.retryButton.visibility = View.VISIBLE
} }
loadingModel.doneWithResponse() musicModel.doneWithResponse()
} }
} }
@ -152,7 +139,7 @@ class LoadingFragment : Fragment(R.layout.fragment_loading) {
if (retry) { if (retry) {
wipeViews() wipeViews()
loadingModel.doneWithRetry() musicModel.doneWithReload()
} }
} }
@ -160,7 +147,7 @@ class LoadingFragment : Fragment(R.layout.fragment_loading) {
if (grant) { if (grant) {
permLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE) permLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE)
loadingModel.doneWithGrant() musicModel.doneWithGrant()
} }
} }

View file

@ -1,4 +1,4 @@
package org.oxycblt.auxio.songs package org.oxycblt.auxio.library
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
@ -7,17 +7,19 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.databinding.DataBindingUtil import androidx.databinding.DataBindingUtil
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentSongsBinding import org.oxycblt.auxio.databinding.FragmentSongsBinding
import org.oxycblt.auxio.music.MusicViewModel
import org.oxycblt.auxio.recycler.adapters.SongAdapter import org.oxycblt.auxio.recycler.adapters.SongAdapter
import org.oxycblt.auxio.recycler.applyDivider import org.oxycblt.auxio.recycler.applyDivider
import org.oxycblt.auxio.recycler.viewholders.ClickListener import org.oxycblt.auxio.recycler.viewholders.ClickListener
class SongsFragment : Fragment() { class SongsFragment : Fragment() {
private val songsModel: SongsViewModel by lazy { private val musicModel: MusicViewModel by activityViewModels {
ViewModelProvider(this).get(SongsViewModel::class.java) MusicViewModel.Factory(requireActivity().application)
} }
override fun onCreateView( override fun onCreateView(
@ -30,7 +32,7 @@ class SongsFragment : Fragment() {
) )
binding.songRecycler.adapter = SongAdapter( binding.songRecycler.adapter = SongAdapter(
songsModel.songs.value!!, musicModel.songs.value!!,
ClickListener { song -> ClickListener { song ->
Log.d(this::class.simpleName, song.name) Log.d(this::class.simpleName, song.name)
} }

View file

@ -1,99 +0,0 @@
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.MusicRepository
import org.oxycblt.auxio.music.processing.MusicLoaderResponse
class LoadingViewModel(private val app: Application) : ViewModel() {
private val loadingJob = Job()
private val ioScope = CoroutineScope(
loadingJob + Dispatchers.IO
)
private val mMusicRepoResponse = MutableLiveData<MusicLoaderResponse>()
val musicRepoResponse: LiveData<MusicLoaderResponse> get() = mMusicRepoResponse
private val mDoRetry = MutableLiveData<Boolean>()
val doRetry: LiveData<Boolean> get() = mDoRetry
private val mDoGrant = MutableLiveData<Boolean>()
val doGrant: LiveData<Boolean> get() = mDoGrant
private var started = false
// Start the music loading. It has already been called, one needs to call retry() instead.
fun go() {
if (!started) {
started = true
startMusicRepo()
}
}
// Start the music loading sequence.
private fun startMusicRepo() {
val repo = MusicRepository.getInstance()
// Allow MusicRepository to scan the file system on the IO thread
ioScope.launch {
val response = repo.init(app)
// Then actually notify listeners of the response in the Main thread
withContext(Dispatchers.Main) {
mMusicRepoResponse.value = response
}
}
}
// Functions for communicating between LoadingFragment & LoadingViewModel
fun doneWithResponse() {
mMusicRepoResponse.value = null
}
fun retry() {
startMusicRepo()
mDoRetry.value = true
}
fun doneWithRetry() {
mDoRetry.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

@ -1,87 +0,0 @@
package org.oxycblt.auxio.music
import android.app.Application
import android.util.Log
import org.oxycblt.auxio.R
import org.oxycblt.auxio.music.models.Album
import org.oxycblt.auxio.music.models.Artist
import org.oxycblt.auxio.music.models.Genre
import org.oxycblt.auxio.music.models.Song
import org.oxycblt.auxio.music.processing.MusicLoader
import org.oxycblt.auxio.music.processing.MusicLoaderResponse
import org.oxycblt.auxio.music.processing.MusicSorter
// Storage for music data.
class MusicRepository {
lateinit var genres: List<Genre>
lateinit var artists: List<Artist>
lateinit var albums: List<Album>
lateinit var songs: List<Song>
fun init(app: Application): MusicLoaderResponse {
Log.i(this::class.simpleName, "Starting initial music load...")
val start = System.currentTimeMillis()
val loader = MusicLoader(app.contentResolver)
if (loader.response == MusicLoaderResponse.DONE) {
// If the loading succeeds, then process the songs and set them.
val sorter = MusicSorter(
loader.genres,
loader.artists,
loader.albums,
loader.songs,
app.getString(R.string.placeholder_unknown_genre),
app.getString(R.string.placeholder_unknown_artist),
app.getString(R.string.placeholder_unknown_album)
)
songs = sorter.songs.toList()
albums = sorter.albums.toList()
artists = sorter.artists.toList()
genres = sorter.genres.toList()
val elapsed = System.currentTimeMillis() - start
Log.i(
this::class.simpleName,
"Music load completed successfully in ${elapsed}ms."
)
}
return loader.response
}
companion object {
@Volatile
private var INSTANCE: MusicRepository? = null
fun getInstance(): MusicRepository {
val tempInstance = INSTANCE
if (tempInstance != null) {
Log.d(
this::class.simpleName,
"Passed an existing instance of MusicRepository."
)
return tempInstance
}
synchronized(this) {
val newInstance = MusicRepository()
INSTANCE = newInstance
Log.d(
this::class.simpleName,
"Created an instance of MusicRepository."
)
return newInstance
}
}
}
}

View file

@ -0,0 +1,142 @@
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.*
import org.oxycblt.auxio.R
import org.oxycblt.auxio.music.models.Album
import org.oxycblt.auxio.music.models.Artist
import org.oxycblt.auxio.music.models.Genre
import org.oxycblt.auxio.music.models.Song
import org.oxycblt.auxio.music.processing.MusicLoader
import org.oxycblt.auxio.music.processing.MusicLoaderResponse
import org.oxycblt.auxio.music.processing.MusicSorter
// Storage for music data.
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 redo() 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()
val loader = MusicLoader(app.contentResolver)
withContext(Dispatchers.Main) {
if (loader.response == MusicLoaderResponse.DONE) {
// If the loading succeeds, then process the songs and set them.
val sorter = MusicSorter(
loader.genres,
loader.artists,
loader.albums,
loader.songs,
app.getString(R.string.placeholder_unknown_genre),
app.getString(R.string.placeholder_unknown_artist),
app.getString(R.string.placeholder_unknown_album)
)
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
fun doneWithResponse() {
mResponse.value = null
}
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

@ -1,22 +0,0 @@
package org.oxycblt.auxio.songs
import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import org.oxycblt.auxio.music.MusicRepository
import org.oxycblt.auxio.music.models.Song
class SongsViewModel : ViewModel() {
private val mSongs = MutableLiveData<List<Song>>()
val songs: LiveData<List<Song>> get() = mSongs
init {
val repo = MusicRepository.getInstance()
mSongs.value = repo.songs
Log.d(this::class.simpleName, "ViewModel created.")
}
}

View file

@ -1,15 +1,53 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android" <layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools">
tools:context=".MainActivity">
<androidx.fragment.app.FragmentContainerView <data>
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment" <variable
name="musicModel"
type="org.oxycblt.auxio.music.MusicViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
app:defaultNavHost="true" android:animateLayoutChanges="true">
app:navGraph="@navigation/nav_main"
tools:ignore="FragmentTagUsage" /> <androidx.viewpager2.widget.ViewPager2
android:id="@+id/viewPager"
android:layout_width="match_parent"
android:layout_height="0dp"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@+id/tabs"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.fragment.app.FragmentContainerView
android:id="@+id/loading_fragment"
android:name="org.oxycblt.auxio.loading.LoadingFragment"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@+id/tabs"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabs"
android:layout_width="match_parent"
android:layout_height="@dimen/tab_menu_size"
android:layout_gravity="bottom"
android:background="?android:attr/windowBackground"
android:elevation="@dimen/elevation_normal"
app:layout_constraintBottom_toBottomOf="parent"
app:tabGravity="fill"
app:tabIconTint="?android:attr/colorPrimary"
app:tabIconTintMode="src_in"
app:tabIndicator="@drawable/indicator"
app:tabIndicatorColor="?android:attr/colorPrimary"
app:tabMode="fixed"
app:tabRippleColor="@color/selection_color" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout> </layout>

View file

@ -8,8 +8,8 @@
<data> <data>
<variable <variable
name="loadingModel" name="musicModel"
type="org.oxycblt.auxio.loading.LoadingViewModel" /> type="org.oxycblt.auxio.music.MusicViewModel" />
</data> </data>
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
@ -69,7 +69,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="@{() -> loadingModel.retry()}" android:onClick="@{() -> musicModel.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"
@ -89,7 +89,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="@{() -> loadingModel.grant()}" android:onClick="@{() -> musicModel.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"

View file

@ -1,33 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/viewPager"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabs"
android:layout_width="match_parent"
android:layout_height="@dimen/tab_menu_size"
android:layout_gravity="bottom"
android:background="?android:attr/windowBackground"
android:elevation="@dimen/elevation_normal"
app:tabIndicatorColor="?android:attr/colorPrimary"
app:tabGravity="fill"
app:tabMode="fixed"
app:tabIconTint="?android:attr/colorPrimary"
app:tabIconTintMode="src_in"
app:tabIndicator="@drawable/indicator"
app:tabRippleColor="@color/selection_color" />
</LinearLayout>
</layout>

View file

@ -1,24 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/nav_main"
app:startDestination="@id/loadingFragment">
<fragment
android:id="@+id/loadingFragment"
android:name="org.oxycblt.auxio.loading.LoadingFragment"
android:label="LoadingFragment"
tools:layout="@layout/fragment_loading">
<action
android:id="@+id/action_to_main"
app:destination="@id/mainFragment"
app:popUpTo="@id/loadingFragment"
app:popUpToInclusive="true" />
</fragment>
<fragment
android:id="@+id/mainFragment"
android:name="org.oxycblt.auxio.MainFragment"
android:label="LibraryFragment"
tools:layout="@layout/fragment_main" />
</navigation>

View file

@ -1,3 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<fade xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="@android:integer/config_longAnimTime"></fade>

View file

@ -7,7 +7,7 @@
<string name="error_no_music">No music found.</string> <string name="error_no_music">No music found.</string>
<string name="error_music_load_failed">Music loading failed.</string> <string name="error_music_load_failed">Music loading failed.</string>
<string name="error_no_perms">Auxio needs permission to access to your music library.</string> <string name="error_no_perms">Permissions to read storage are needed.</string>
<string name="label_retry">Retry</string> <string name="label_retry">Retry</string>
<string name="label_grant">Grant</string> <string name="label_grant">Grant</string>