From 031815d746de28c78122c4931cd8cdf80574c184 Mon Sep 17 00:00:00 2001 From: OxygenCobalt Date: Mon, 7 Sep 2020 16:12:23 -0600 Subject: [PATCH] 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. --- .../java/org/oxycblt/auxio/MainActivity.kt | 125 ++++++++++++++- .../java/org/oxycblt/auxio/MainFragment.kt | 117 --------------- .../java/org/oxycblt/auxio/coil/CoilUtils.kt | 38 ++--- .../oxycblt/auxio/library/LibraryFragment.kt | 8 +- .../oxycblt/auxio/library/LibraryViewModel.kt | 27 ---- .../{loading => library}/LoadingFragment.kt | 55 +++---- .../auxio/{songs => library}/SongsFragment.kt | 10 +- .../oxycblt/auxio/loading/LoadingViewModel.kt | 99 ------------ .../oxycblt/auxio/music/MusicRepository.kt | 87 ----------- .../org/oxycblt/auxio/music/MusicViewModel.kt | 142 ++++++++++++++++++ .../org/oxycblt/auxio/songs/SongsViewModel.kt | 22 --- app/src/main/res/layout/activity_main.xml | 54 ++++++- app/src/main/res/layout/fragment_loading.xml | 8 +- app/src/main/res/layout/fragment_main.xml | 33 ---- app/src/main/res/navigation/nav_main.xml | 24 --- .../res/transition/transition_to_main.xml | 3 - app/src/main/res/values/strings.xml | 2 +- 17 files changed, 360 insertions(+), 494 deletions(-) delete mode 100644 app/src/main/java/org/oxycblt/auxio/MainFragment.kt delete mode 100644 app/src/main/java/org/oxycblt/auxio/library/LibraryViewModel.kt rename app/src/main/java/org/oxycblt/auxio/{loading => library}/LoadingFragment.kt (80%) rename app/src/main/java/org/oxycblt/auxio/{songs => library}/SongsFragment.kt (80%) delete mode 100644 app/src/main/java/org/oxycblt/auxio/loading/LoadingViewModel.kt delete mode 100644 app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt create mode 100644 app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt delete mode 100644 app/src/main/java/org/oxycblt/auxio/songs/SongsViewModel.kt delete mode 100644 app/src/main/res/layout/fragment_main.xml delete mode 100644 app/src/main/res/navigation/nav_main.xml delete mode 100644 app/src/main/res/transition/transition_to_main.xml diff --git a/app/src/main/java/org/oxycblt/auxio/MainActivity.kt b/app/src/main/java/org/oxycblt/auxio/MainActivity.kt index 9906847df..302bd36fb 100644 --- a/app/src/main/java/org/oxycblt/auxio/MainActivity.kt +++ b/app/src/main/java/org/oxycblt/auxio/MainActivity.kt @@ -5,20 +5,51 @@ import android.os.Bundle import android.util.AttributeSet import android.util.Log import android.view.View +import androidx.activity.result.ActivityResultLauncher import androidx.appcompat.app.AppCompatActivity 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.getInactiveAlpha +import org.oxycblt.auxio.theme.getTransparentAccent +import org.oxycblt.auxio.theme.toColor class MainActivity : AppCompatActivity() { - // Debug placeholder so I can test dark and light modes. Ignore. - override fun onAttachedToWindow() { - super.onAttachedToWindow() + private val shownFragments = listOf(0, 1) - 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 + + 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? { + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO) + // Apply the theme setTheme(accent.second) @@ -27,8 +58,92 @@ class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_main) + binding = DataBindingUtil.setContentView( + 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.") } + + 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 + } + } } diff --git a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt deleted file mode 100644 index fa39f5f3a..000000000 --- a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt +++ /dev/null @@ -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( - 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 - } - } -} diff --git a/app/src/main/java/org/oxycblt/auxio/coil/CoilUtils.kt b/app/src/main/java/org/oxycblt/auxio/coil/CoilUtils.kt index 27a0ede33..9e8dda1a9 100644 --- a/app/src/main/java/org/oxycblt/auxio/coil/CoilUtils.kt +++ b/app/src/main/java/org/oxycblt/auxio/coil/CoilUtils.kt @@ -1,5 +1,6 @@ package org.oxycblt.auxio.coil +import android.content.Context import android.net.Uri import android.widget.ImageView import androidx.databinding.BindingAdapter @@ -15,13 +16,9 @@ private var artistImageFetcher: ArtistImageFetcher? = null // Get the cover art for a song or album @BindingAdapter("coverArt") fun ImageView.getCoverArt(song: Song) { - val request = ImageRequest.Builder(context) + val request = getDefaultRequest(context, this) .data(song.album.coverUri) - .crossfade(true) - .placeholder(android.R.color.transparent) - .error(R.drawable.ic_artist) - .crossfade(true) - .target(this) + .error(R.drawable.ic_song) .build() Coil.imageLoader(context).enqueue(request) @@ -29,13 +26,9 @@ fun ImageView.getCoverArt(song: Song) { @BindingAdapter("coverArt") fun ImageView.getCoverArt(album: Album) { - val request = ImageRequest.Builder(context) + val request = getDefaultRequest(context, this) .data(album.coverUri) - .crossfade(true) - .placeholder(android.R.color.transparent) - .error(R.drawable.ic_artist) - .crossfade(true) - .target(this) + .error(R.drawable.ic_album) .build() Coil.imageLoader(context).enqueue(request) @@ -56,26 +49,25 @@ fun ImageView.getArtistImage(artist: Artist) { artistImageFetcher = ArtistImageFetcher(context) } - // Manually create an image request, as that's the only way to add a fetcher that - // takes a list of uris AFAIK. - ImageRequest.Builder(context) + getDefaultRequest(context, this) .data(uris) .fetcher(artistImageFetcher!!) - .crossfade(true) - .placeholder(android.R.color.transparent) .error(R.drawable.ic_artist) - .target(this) .build() } else { - ImageRequest.Builder(context) + getDefaultRequest(context, this) .data(artist.albums[0].coverUri) - .crossfade(true) - .placeholder(android.R.color.transparent) .error(R.drawable.ic_artist) - .crossfade(true) - .target(this) .build() } 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) +} diff --git a/app/src/main/java/org/oxycblt/auxio/library/LibraryFragment.kt b/app/src/main/java/org/oxycblt/auxio/library/LibraryFragment.kt index b42247f72..85040f705 100644 --- a/app/src/main/java/org/oxycblt/auxio/library/LibraryFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/library/LibraryFragment.kt @@ -7,17 +7,19 @@ import android.view.View import android.view.ViewGroup import androidx.databinding.DataBindingUtil import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels import androidx.lifecycle.ViewModelProvider import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.FragmentLibraryBinding +import org.oxycblt.auxio.music.MusicViewModel import org.oxycblt.auxio.recycler.adapters.ArtistAdapter import org.oxycblt.auxio.recycler.applyDivider import org.oxycblt.auxio.recycler.viewholders.ClickListener class LibraryFragment : Fragment() { - private val libraryModel: LibraryViewModel by lazy { - ViewModelProvider(this).get(LibraryViewModel::class.java) + private val musicModel: MusicViewModel by activityViewModels { + MusicViewModel.Factory(requireActivity().application) } override fun onCreateView( @@ -30,7 +32,7 @@ class LibraryFragment : Fragment() { ) binding.libraryRecycler.adapter = ArtistAdapter( - libraryModel.artists.value!!, + musicModel.artists.value!!, ClickListener { artist -> Log.d(this::class.simpleName, artist.name) } diff --git a/app/src/main/java/org/oxycblt/auxio/library/LibraryViewModel.kt b/app/src/main/java/org/oxycblt/auxio/library/LibraryViewModel.kt deleted file mode 100644 index 26566bafe..000000000 --- a/app/src/main/java/org/oxycblt/auxio/library/LibraryViewModel.kt +++ /dev/null @@ -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>() - private var mAlbums = MutableLiveData>() - - val artists: LiveData> get() = mArtists - val albums: LiveData> get() = mAlbums - - init { - val repo = MusicRepository.getInstance() - - mArtists.value = repo.artists - mAlbums.value = repo.albums - - Log.d(this::class.simpleName, "ViewModel created.") - } -} diff --git a/app/src/main/java/org/oxycblt/auxio/loading/LoadingFragment.kt b/app/src/main/java/org/oxycblt/auxio/library/LoadingFragment.kt similarity index 80% rename from app/src/main/java/org/oxycblt/auxio/loading/LoadingFragment.kt rename to app/src/main/java/org/oxycblt/auxio/library/LoadingFragment.kt index fc650e828..cfc0c38d7 100644 --- a/app/src/main/java/org/oxycblt/auxio/loading/LoadingFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/library/LoadingFragment.kt @@ -1,9 +1,8 @@ -package org.oxycblt.auxio.loading +package org.oxycblt.auxio.library import android.Manifest import android.content.pm.PackageManager import android.os.Bundle -import android.transition.TransitionInflater import android.util.Log import android.view.LayoutInflater import android.view.View @@ -13,21 +12,16 @@ import androidx.activity.result.contract.ActivityResultContracts import androidx.core.content.ContextCompat import androidx.databinding.DataBindingUtil import androidx.fragment.app.Fragment -import androidx.lifecycle.ViewModelProvider -import androidx.navigation.fragment.findNavController +import androidx.fragment.app.activityViewModels import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.FragmentLoadingBinding +import org.oxycblt.auxio.music.MusicViewModel import org.oxycblt.auxio.music.processing.MusicLoaderResponse class LoadingFragment : Fragment(R.layout.fragment_loading) { - private val loadingModel: LoadingViewModel by lazy { - ViewModelProvider( - this, - LoadingViewModel.Factory( - requireActivity().application - ) - ).get(LoadingViewModel::class.java) + private val musicModel: MusicViewModel by activityViewModels { + MusicViewModel.Factory(requireActivity().application) } private lateinit var binding: FragmentLoadingBinding @@ -43,23 +37,23 @@ class LoadingFragment : Fragment(R.layout.fragment_loading) { ) binding.lifecycleOwner = this - binding.loadingModel = loadingModel + binding.musicModel = musicModel - loadingModel.musicRepoResponse.observe( + musicModel.response.observe( viewLifecycleOwner, { response -> onMusicLoadResponse(response) } ) - loadingModel.doRetry.observe( + musicModel.doReload.observe( viewLifecycleOwner, { retry -> onRetry(retry) } ) - loadingModel.doGrant.observe( + musicModel.doGrant.observe( viewLifecycleOwner, { grant -> onGrant(grant) @@ -75,7 +69,7 @@ class LoadingFragment : Fragment(R.layout.fragment_loading) { if (granted) { wipeViews() - loadingModel.retry() + musicModel.reload() } } @@ -85,7 +79,7 @@ class LoadingFragment : Fragment(R.layout.fragment_loading) { if (checkPerms()) { onNoPerms() } else { - loadingModel.go() + musicModel.go() } Log.d(this::class.simpleName, "Fragment created.") @@ -110,28 +104,21 @@ class LoadingFragment : Fragment(R.layout.fragment_loading) { repoResponse?.let { response -> binding.loadingBar.visibility = View.GONE - 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 - + if (response != MusicLoaderResponse.DONE) { binding.errorText.text = if (response == MusicLoaderResponse.NO_MUSIC) getString(R.string.error_no_music) else 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) { wipeViews() - loadingModel.doneWithRetry() + musicModel.doneWithReload() } } @@ -160,7 +147,7 @@ class LoadingFragment : Fragment(R.layout.fragment_loading) { if (grant) { permLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE) - loadingModel.doneWithGrant() + musicModel.doneWithGrant() } } diff --git a/app/src/main/java/org/oxycblt/auxio/songs/SongsFragment.kt b/app/src/main/java/org/oxycblt/auxio/library/SongsFragment.kt similarity index 80% rename from app/src/main/java/org/oxycblt/auxio/songs/SongsFragment.kt rename to app/src/main/java/org/oxycblt/auxio/library/SongsFragment.kt index 122b67a9f..9704664b7 100644 --- a/app/src/main/java/org/oxycblt/auxio/songs/SongsFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/library/SongsFragment.kt @@ -1,4 +1,4 @@ -package org.oxycblt.auxio.songs +package org.oxycblt.auxio.library import android.os.Bundle import android.util.Log @@ -7,17 +7,19 @@ import android.view.View import android.view.ViewGroup import androidx.databinding.DataBindingUtil import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels import androidx.lifecycle.ViewModelProvider import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.FragmentSongsBinding +import org.oxycblt.auxio.music.MusicViewModel import org.oxycblt.auxio.recycler.adapters.SongAdapter import org.oxycblt.auxio.recycler.applyDivider import org.oxycblt.auxio.recycler.viewholders.ClickListener class SongsFragment : Fragment() { - private val songsModel: SongsViewModel by lazy { - ViewModelProvider(this).get(SongsViewModel::class.java) + private val musicModel: MusicViewModel by activityViewModels { + MusicViewModel.Factory(requireActivity().application) } override fun onCreateView( @@ -30,7 +32,7 @@ class SongsFragment : Fragment() { ) binding.songRecycler.adapter = SongAdapter( - songsModel.songs.value!!, + musicModel.songs.value!!, ClickListener { song -> Log.d(this::class.simpleName, song.name) } diff --git a/app/src/main/java/org/oxycblt/auxio/loading/LoadingViewModel.kt b/app/src/main/java/org/oxycblt/auxio/loading/LoadingViewModel.kt deleted file mode 100644 index 3c2bc8413..000000000 --- a/app/src/main/java/org/oxycblt/auxio/loading/LoadingViewModel.kt +++ /dev/null @@ -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() - val musicRepoResponse: LiveData get() = mMusicRepoResponse - - private val mDoRetry = MutableLiveData() - val doRetry: LiveData get() = mDoRetry - - private val mDoGrant = MutableLiveData() - val doGrant: LiveData 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 create(modelClass: Class): T { - if (modelClass.isAssignableFrom(LoadingViewModel::class.java)) { - return LoadingViewModel(application) as T - } - - throw IllegalArgumentException("Unknown ViewModel class.") - } - } -} diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt deleted file mode 100644 index 7fac22848..000000000 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt +++ /dev/null @@ -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 - lateinit var artists: List - lateinit var albums: List - lateinit var songs: List - - 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 - } - } - } -} diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt new file mode 100644 index 000000000..68c878f51 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt @@ -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>() + val genres: LiveData> get() = mGenres + + private val mArtists = MutableLiveData>() + val artists: LiveData> get() = mArtists + + private val mAlbums = MutableLiveData>() + val albums: LiveData> get() = mAlbums + + private val mSongs = MutableLiveData>() + val songs: LiveData> get() = mSongs + + private val mResponse = MutableLiveData() + val response: LiveData get() = mResponse + + // UI control + private val mRedo = MutableLiveData() + val doReload: LiveData get() = mRedo + + private val mDoGrant = MutableLiveData() + val doGrant: LiveData 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 create(modelClass: Class): T { + if (modelClass.isAssignableFrom(MusicViewModel::class.java)) { + return MusicViewModel(application) as T + } + + throw IllegalArgumentException("Unknown ViewModel class.") + } + } +} diff --git a/app/src/main/java/org/oxycblt/auxio/songs/SongsViewModel.kt b/app/src/main/java/org/oxycblt/auxio/songs/SongsViewModel.kt deleted file mode 100644 index fbefddafb..000000000 --- a/app/src/main/java/org/oxycblt/auxio/songs/SongsViewModel.kt +++ /dev/null @@ -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>() - val songs: LiveData> get() = mSongs - - init { - val repo = MusicRepository.getInstance() - - mSongs.value = repo.songs - - Log.d(this::class.simpleName, "ViewModel created.") - } -} diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index d0e00fb87..e70a89f5b 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,15 +1,53 @@ + xmlns:tools="http://schemas.android.com/tools"> - + + + + + + android:animateLayoutChanges="true"> + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_loading.xml b/app/src/main/res/layout/fragment_loading.xml index 865b5e5a1..aba183510 100644 --- a/app/src/main/res/layout/fragment_loading.xml +++ b/app/src/main/res/layout/fragment_loading.xml @@ -8,8 +8,8 @@ + name="musicModel" + type="org.oxycblt.auxio.music.MusicViewModel" /> - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/navigation/nav_main.xml b/app/src/main/res/navigation/nav_main.xml deleted file mode 100644 index 8ee52ca72..000000000 --- a/app/src/main/res/navigation/nav_main.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/transition/transition_to_main.xml b/app/src/main/res/transition/transition_to_main.xml deleted file mode 100644 index 13bb32d5d..000000000 --- a/app/src/main/res/transition/transition_to_main.xml +++ /dev/null @@ -1,3 +0,0 @@ - - \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7a328f15a..c863d435d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -7,7 +7,7 @@ No music found. Music loading failed. - Auxio needs permission to access to your music library. + Permissions to read storage are needed. Retry Grant