music: make musicloader async

Make MusicLoader instantiation fully asynchronous. This implementation
changes a lot about Auxio. For one, the loading screen is now gone.
However, many parts of the app now run under the fact that MusicStore
might not be available. However, I don't think there will be too much bugs
from it. Some more changes will be made to improve this implementation.
This commit is contained in:
OxygenCobalt 2021-10-24 20:01:15 -06:00
parent 926fef4218
commit fe0c2761c7
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
22 changed files with 286 additions and 503 deletions

View file

@ -58,8 +58,6 @@ class MainFragment : Fragment() {
// --- VIEWMODEL SETUP --- // --- VIEWMODEL SETUP ---
playbackModel.setupPlayback(requireContext())
// Change CompactPlaybackFragment's visibility here so that an animation occurs. // Change CompactPlaybackFragment's visibility here so that an animation occurs.
binding.mainPlayback.isVisible = playbackModel.song.value != null binding.mainPlayback.isVisible = playbackModel.song.value != null

View file

@ -78,23 +78,28 @@ class DetailViewModel : ViewModel() {
private var currentMenuContext: DisplayMode? = null private var currentMenuContext: DisplayMode? = null
private val musicStore = MusicStore.getInstance()
private val settingsManager = SettingsManager.getInstance() private val settingsManager = SettingsManager.getInstance()
fun setGenre(id: Long, context: Context) { fun setGenre(id: Long, context: Context) {
if (mCurGenre.value?.id == id) return if (mCurGenre.value?.id == id) return
val musicStore = MusicStore.requireInstance()
mCurGenre.value = musicStore.genres.find { it.id == id } mCurGenre.value = musicStore.genres.find { it.id == id }
refreshGenreData(context) refreshGenreData(context)
} }
fun setArtist(id: Long, context: Context) { fun setArtist(id: Long, context: Context) {
if (mCurArtist.value?.id == id) return if (mCurArtist.value?.id == id) return
val musicStore = MusicStore.requireInstance()
mCurArtist.value = musicStore.artists.find { it.id == id } mCurArtist.value = musicStore.artists.find { it.id == id }
refreshArtistData(context) refreshArtistData(context)
} }
fun setAlbum(id: Long, context: Context) { fun setAlbum(id: Long, context: Context) {
if (mCurAlbum.value?.id == id) return if (mCurAlbum.value?.id == id) return
val musicStore = MusicStore.requireInstance()
mCurAlbum.value = musicStore.albums.find { it.id == id } mCurAlbum.value = musicStore.albums.find { it.id == id }
refreshAlbumData(context) refreshAlbumData(context)
} }

View file

@ -18,12 +18,16 @@
package org.oxycblt.auxio.home package org.oxycblt.auxio.home
import android.Manifest
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Button
import androidx.activity.result.contract.ActivityResultContracts
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.content.res.ResourcesCompat
import androidx.core.view.iterator import androidx.core.view.iterator
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.core.view.updatePaddingRelative import androidx.core.view.updatePaddingRelative
@ -34,6 +38,7 @@ import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.adapter.FragmentStateAdapter import androidx.viewpager2.adapter.FragmentStateAdapter
import androidx.viewpager2.widget.ViewPager2 import androidx.viewpager2.widget.ViewPager2
import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.tabs.TabLayoutMediator import com.google.android.material.tabs.TabLayoutMediator
import org.oxycblt.auxio.MainFragmentDirections import org.oxycblt.auxio.MainFragmentDirections
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
@ -46,6 +51,7 @@ import org.oxycblt.auxio.home.list.SongListFragment
import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Genre
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.ui.DisplayMode import org.oxycblt.auxio.ui.DisplayMode
@ -73,6 +79,13 @@ class HomeFragment : Fragment() {
var bottomPadding = 0 var bottomPadding = 0
val sortItem: MenuItem val sortItem: MenuItem
// Build the permission launcher here as you can only do it in onCreateView/onCreate
val permLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) {
homeModel.reloadMusic(requireContext())
}
// --- UI SETUP --- // --- UI SETUP ---
binding.lifecycleOwner = viewLifecycleOwner binding.lifecycleOwner = viewLifecycleOwner
@ -193,22 +206,96 @@ class HomeFragment : Fragment() {
registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) = homeModel.updateCurrentTab(position) override fun onPageSelected(position: Int) = homeModel.updateCurrentTab(position)
}) })
TabLayoutMediator(binding.homeTabs, this) { tab, pos ->
tab.setText(homeModel.tabs[pos].string)
}.attach()
} }
binding.homeFab.setOnClickListener { binding.homeFab.setOnClickListener {
playbackModel.shuffleAll() playbackModel.shuffleAll()
} }
TabLayoutMediator(binding.homeTabs, binding.homePager) { tab, pos ->
tab.setText(homeModel.tabs[pos].string)
}.attach()
// --- VIEWMODEL SETUP --- // --- VIEWMODEL SETUP ---
// Initialize music loading. Unlike MainFragment, we can not only do this here on startup
// but also show a SnackBar in a reasonable place in this fragment.
homeModel.loadMusic(requireContext())
// There is no way a fast scrolling event can continue across a re-create. Reset it. // There is no way a fast scrolling event can continue across a re-create. Reset it.
homeModel.updateFastScrolling(false) homeModel.updateFastScrolling(false)
homeModel.loaderResponse.observe(viewLifecycleOwner) { response ->
// Handle the loader response.
when (response) {
is MusicStore.Response.Ok -> {
logD("Received Ok")
binding.homeFab.show()
playbackModel.setupPlayback(requireContext())
}
is MusicStore.Response.Err -> {
logD("Received Error")
// We received an error. Hide the FAB and show a Snackbar with the error
// message and a corresponding action
binding.homeFab.hide()
val errorRes = when (response.kind) {
MusicStore.ErrorKind.NO_MUSIC -> R.string.err_no_music
MusicStore.ErrorKind.NO_PERMS -> R.string.err_no_perms
MusicStore.ErrorKind.FAILED -> R.string.err_load_failed
}
val snackbar = Snackbar.make(
binding.root, getString(errorRes), Snackbar.LENGTH_INDEFINITE
)
snackbar.view.apply {
// Change the font family to our semibold color
findViewById<Button>(
com.google.android.material.R.id.snackbar_action
).typeface = ResourcesCompat.getFont(requireContext(), R.font.inter_semibold)
fitsSystemWindows = false
// Prevent fitsSystemWindows margins from being applied to this view
// [We already do it]
setOnApplyWindowInsetsListener { v, insets -> insets }
}
when (response.kind) {
MusicStore.ErrorKind.FAILED, MusicStore.ErrorKind.NO_MUSIC -> {
snackbar.setAction(R.string.lbl_retry) {
homeModel.reloadMusic(requireContext())
}
}
MusicStore.ErrorKind.NO_PERMS -> {
snackbar.setAction(R.string.lbl_grant) {
permLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE)
}
}
}
snackbar.show()
}
// While loading or during an error, make sure we keep the shuffle fab hidden so
// that any kind of loading is impossible. PlaybackStateManager also relies on this
// invariant, so please don't change it.
null -> binding.homeFab.hide()
}
}
homeModel.fastScrolling.observe(viewLifecycleOwner) { scrolling -> homeModel.fastScrolling.observe(viewLifecycleOwner) { scrolling ->
// Make sure an update here doesn't mess up the FAB state when it comes to the
// loader response.
if (homeModel.loaderResponse.value !is MusicStore.Response.Ok) {
return@observe
}
if (scrolling) { if (scrolling) {
binding.homeFab.hide() binding.homeFab.hide()
} else { } else {

View file

@ -18,9 +18,12 @@
package org.oxycblt.auxio.home package org.oxycblt.auxio.home
import android.content.Context
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Genre
@ -36,13 +39,8 @@ import org.oxycblt.auxio.ui.SortMode
* @author OxygenCobalt * @author OxygenCobalt
*/ */
class HomeViewModel : ViewModel(), SettingsManager.Callback { class HomeViewModel : ViewModel(), SettingsManager.Callback {
private val musicStore = MusicStore.getInstance()
private val settingsManager = SettingsManager.getInstance() private val settingsManager = SettingsManager.getInstance()
/** Internal getter for getting the visible library tabs */
private val visibleTabs: List<DisplayMode> get() = settingsManager.libTabs
.filterIsInstance<Tab.Visible>().map { it.mode }
private val mSongs = MutableLiveData(listOf<Song>()) private val mSongs = MutableLiveData(listOf<Song>())
val songs: LiveData<List<Song>> get() = mSongs val songs: LiveData<List<Song>> get() = mSongs
@ -58,12 +56,13 @@ class HomeViewModel : ViewModel(), SettingsManager.Callback {
var tabs: List<DisplayMode> = visibleTabs var tabs: List<DisplayMode> = visibleTabs
private set private set
/** Internal getter for getting the visible library tabs */
private val visibleTabs: List<DisplayMode> get() = settingsManager.libTabs
.filterIsInstance<Tab.Visible>().map { it.mode }
private val mCurTab = MutableLiveData(tabs[0]) private val mCurTab = MutableLiveData(tabs[0])
val curTab: LiveData<DisplayMode> = mCurTab val curTab: LiveData<DisplayMode> = mCurTab
private val mFastScrolling = MutableLiveData(false)
val fastScrolling: LiveData<Boolean> = mFastScrolling
/** /**
* Marker to recreate all library tabs, usually initiated by a settings change. * Marker to recreate all library tabs, usually initiated by a settings change.
* When this flag is set, all tabs (and their respective viewpager fragments) will be * When this flag is set, all tabs (and their respective viewpager fragments) will be
@ -72,13 +71,51 @@ class HomeViewModel : ViewModel(), SettingsManager.Callback {
private val mRecreateTabs = MutableLiveData(false) private val mRecreateTabs = MutableLiveData(false)
val recreateTabs: LiveData<Boolean> = mRecreateTabs val recreateTabs: LiveData<Boolean> = mRecreateTabs
private val mFastScrolling = MutableLiveData(false)
val fastScrolling: LiveData<Boolean> = mFastScrolling
private val mLoaderResponse = MutableLiveData<MusicStore.Response?>(null)
val loaderResponse: LiveData<MusicStore.Response?> = mLoaderResponse
private var isBusy = false
init { init {
settingsManager.addCallback(this)
}
/**
* Initiate the loading process. This is done here since HomeFragment will be the first
* fragment navigated to and because SnackBars will have the best UX here.
*/
fun loadMusic(context: Context) {
if (mLoaderResponse.value != null || isBusy) {
return
}
isBusy = true
mLoaderResponse.value = null
viewModelScope.launch {
val result = MusicStore.initInstance(context)
isBusy = false
mLoaderResponse.value = result
if (result is MusicStore.Response.Ok) {
val musicStore = result.musicStore
mSongs.value = settingsManager.libSongSort.sortSongs(musicStore.songs) mSongs.value = settingsManager.libSongSort.sortSongs(musicStore.songs)
mAlbums.value = settingsManager.libAlbumSort.sortAlbums(musicStore.albums) mAlbums.value = settingsManager.libAlbumSort.sortAlbums(musicStore.albums)
mArtists.value = settingsManager.libArtistSort.sortModels(musicStore.artists) mArtists.value = settingsManager.libArtistSort.sortModels(musicStore.artists)
mGenres.value = settingsManager.libGenreSort.sortModels(musicStore.genres) mGenres.value = settingsManager.libGenreSort.sortModels(musicStore.genres)
}
}
}
settingsManager.addCallback(this) fun reloadMusic(context: Context) {
mLoaderResponse.value = null
loadMusic(context)
} }
/** /**

View file

@ -38,6 +38,7 @@ import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.util.canScroll
import org.oxycblt.auxio.util.resolveAttr import org.oxycblt.auxio.util.resolveAttr
import org.oxycblt.auxio.util.resolveDrawable import org.oxycblt.auxio.util.resolveDrawable
import kotlin.math.abs import kotlin.math.abs
@ -47,13 +48,13 @@ import kotlin.math.abs
* Zhanghi's AndroidFastScroll but slimmed down for Auxio and with a couple of enhancements. * Zhanghi's AndroidFastScroll but slimmed down for Auxio and with a couple of enhancements.
* *
* Attributions as per the Apache 2.0 license: * Attributions as per the Apache 2.0 license:
* ORIGINAL AUTHOR: Zhanghai [https://github.com/zhanghai] * ORIGINAL AUTHOR: Hai Zhang [https://github.com/zhanghai]
* PROJECT: Android Fast Scroll [https://github.com/zhanghai/AndroidFastScroll] * PROJECT: Android Fast Scroll [https://github.com/zhanghai/AndroidFastScroll]
* MODIFIER: OxygenCobalt [https://github.com/] * MODIFIER: OxygenCobalt [https://github.com/]
* *
* !!! MODIFICATIONS !!!: * !!! MODIFICATIONS !!!:
* - Scroller will no longer show itself on startup, which looked unpleasent with multiple * - Scroller will no longer show itself on startup or relayouts, which looked unpleasant
* views * with multiple views
* - DefaultAnimationHelper and RecyclerViewHelper were merged into the class * - DefaultAnimationHelper and RecyclerViewHelper were merged into the class
* - FastScroller overlay was merged into RecyclerView instance * - FastScroller overlay was merged into RecyclerView instance
* - Removed FastScrollerBuilder * - Removed FastScrollerBuilder
@ -65,8 +66,6 @@ import kotlin.math.abs
* - Added drag listener * - Added drag listener
* - TODO: Added documentation * - TODO: Added documentation
* - TODO: Popup will center itself to the thumb when possible * - TODO: Popup will center itself to the thumb when possible
*
* TODO: Debug this
*/ */
class FastScrollRecyclerView @JvmOverloads constructor( class FastScrollRecyclerView @JvmOverloads constructor(
context: Context, context: Context,
@ -193,10 +192,6 @@ class FastScrollRecyclerView @JvmOverloads constructor(
}) })
} }
fun addPopupProvider(provider: (Int) -> String) {
popupProvider = provider
}
// --- RECYCLERVIEW EVENT MANAGEMENT --- // --- RECYCLERVIEW EVENT MANAGEMENT ---
private fun onPreDraw() { private fun onPreDraw() {
@ -316,6 +311,10 @@ class FastScrollRecyclerView @JvmOverloads constructor(
} }
private fun updateScrollbarState() { private fun updateScrollbarState() {
if (!canScroll() || childCount == 0) {
return
}
// Getting a pixel-perfect scroll position from a recyclerview is a bit of an involved // Getting a pixel-perfect scroll position from a recyclerview is a bit of an involved
// process. It's kind of expected given how RecyclerView well...recycles views, but it's // process. It's kind of expected given how RecyclerView well...recycles views, but it's
// still very annoying how many hoops one has to jump through. // still very annoying how many hoops one has to jump through.

View file

@ -39,7 +39,7 @@ import kotlin.math.sqrt
* This is an adaptation from AndroidFastScroll's MD2 theme. * This is an adaptation from AndroidFastScroll's MD2 theme.
* *
* Attributions as per the Apache 2.0 license: * Attributions as per the Apache 2.0 license:
* ORIGINAL AUTHOR: Zhanghai [https://github.com/zhanghai] * ORIGINAL AUTHOR: Hai Zhang [https://github.com/zhanghai]
* PROJECT: Android Fast Scroll [https://github.com/zhanghai/AndroidFastScroll] * PROJECT: Android Fast Scroll [https://github.com/zhanghai/AndroidFastScroll]
* MODIFIER: OxygenCobalt [https://github.com/] * MODIFIER: OxygenCobalt [https://github.com/]
* *
@ -47,7 +47,6 @@ import kotlin.math.sqrt
* - Use modified Auxio resources instead of AFS resources * - Use modified Auxio resources instead of AFS resources
* - Variable names are no longer prefixed with m * - Variable names are no longer prefixed with m
* - Suppressed deprecation warning when dealing with convexness * - Suppressed deprecation warning when dealing with convexness
* - TODO: Popup will center itself to the thumb when possible
*/ */
class Md2PopupBackground(context: Context) : Drawable() { class Md2PopupBackground(context: Context) : Drawable() {
private val paint: Paint = Paint() private val paint: Paint = Paint()

View file

@ -29,7 +29,6 @@ import org.oxycblt.auxio.ui.SongViewHolder
import org.oxycblt.auxio.ui.SortMode import org.oxycblt.auxio.ui.SortMode
import org.oxycblt.auxio.ui.newMenu import org.oxycblt.auxio.ui.newMenu
import org.oxycblt.auxio.ui.sliceArticle import org.oxycblt.auxio.ui.sliceArticle
import org.oxycblt.auxio.util.applySpans
class SongListFragment : HomeListFragment() { class SongListFragment : HomeListFragment() {
override fun onCreateView( override fun onCreateView(
@ -47,7 +46,6 @@ class SongListFragment : HomeListFragment() {
) )
setupRecycler(R.id.home_song_list, adapter, homeModel.songs) setupRecycler(R.id.home_song_list, adapter, homeModel.songs)
binding.homeRecycler.applySpans { it == 0 }
return binding.root return binding.root
} }

View file

@ -1,197 +0,0 @@
/*
* Copyright (c) 2021 Auxio Project
* LoadingFragment.kt is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.loading
import android.Manifest
import android.content.pm.PackageManager
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentLoadingBinding
import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.util.logD
/**
* Fragment that handles what to display during the loading process.
* TODO: Figure out how to phase out the loading screen since
* Android 12 is annoyingly stubborn about having one splash
* screen and one splash screen only.
* @author OxygenCobalt
*/
class LoadingFragment : Fragment() {
private val loadingModel: LoadingViewModel by viewModels()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val binding = FragmentLoadingBinding.inflate(inflater)
// Build the permission launcher here as you can only do it in onCreateView/onCreate
val permLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission(), ::onPermResult
)
// --- UI SETUP ---
binding.lifecycleOwner = viewLifecycleOwner
binding.loadingModel = loadingModel
// --- VIEWMODEL SETUP ---
loadingModel.doGrant.observe(viewLifecycleOwner) { doGrant ->
if (doGrant) {
permLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE)
loadingModel.doneWithGrant()
}
}
loadingModel.response.observe(viewLifecycleOwner) { response ->
when (response) {
// Success should lead to navigation to the main fragment
MusicStore.Response.SUCCESS -> findNavController().navigate(
LoadingFragmentDirections.actionToMain()
)
// Null means that the loading process is going on
null -> showLoading(binding)
// Anything else is an error
else -> showError(binding, response)
}
}
if (hasNoPermissions()) {
// MusicStore.Response.NO_PERMS isnt actually returned by MusicStore, its just
// a way to keep the current permission state across device changes
loadingModel.notifyNoPermissions()
}
if (loadingModel.response.value == null) {
loadingModel.load(requireContext())
}
logD("Fragment created")
return binding.root
}
override fun onResume() {
super.onResume()
// Navigate away if the music has already been loaded.
// This causes a memory leak, but there's nothing I can do about it.
if (loadingModel.loaded) {
findNavController().navigate(
LoadingFragmentDirections.actionToMain()
)
}
}
// --- PERMISSIONS ---
/**
* Check if Auxio has the permissions to load music
*/
private fun hasNoPermissions(): Boolean {
val needRationale = shouldShowRequestPermissionRationale(
Manifest.permission.READ_EXTERNAL_STORAGE
)
val notGranted = ContextCompat.checkSelfPermission(
requireContext(), Manifest.permission.READ_EXTERNAL_STORAGE
) == PackageManager.PERMISSION_DENIED
return needRationale || notGranted
}
private fun onPermResult(granted: Boolean) {
if (granted) {
// If granted, its now safe to load, which will clear the NO_PERMS response
// we applied earlier.
loadingModel.load(requireContext())
}
}
// --- UI DISPLAY ---
/**
* Hide all error elements and return to the loading view
*/
private fun showLoading(binding: FragmentLoadingBinding) {
binding.apply {
loadingErrorText.visibility = View.INVISIBLE
loadingActionButton.visibility = View.INVISIBLE
loadingCircle.visibility = View.VISIBLE
}
}
/**
* Show an error prompt.
* @param error The [MusicStore.Response] that this error corresponds to. Ignores
* [MusicStore.Response.SUCCESS]
*/
private fun showError(binding: FragmentLoadingBinding, error: MusicStore.Response) {
binding.loadingCircle.visibility = View.GONE
binding.loadingErrorText.visibility = View.VISIBLE
binding.loadingActionButton.visibility = View.VISIBLE
when (error) {
MusicStore.Response.NO_MUSIC -> {
binding.loadingErrorText.text = getString(R.string.err_no_music)
binding.loadingActionButton.apply {
setText(R.string.lbl_retry)
setOnClickListener {
loadingModel.load(context)
}
}
}
MusicStore.Response.FAILED -> {
binding.loadingErrorText.text = getString(R.string.err_load_failed)
binding.loadingActionButton.apply {
setText(R.string.lbl_retry)
setOnClickListener {
loadingModel.load(context)
}
}
}
MusicStore.Response.NO_PERMS -> {
binding.loadingErrorText.text = getString(R.string.err_no_perms)
binding.loadingActionButton.apply {
setText(R.string.lbl_grant)
setOnClickListener {
loadingModel.grant()
}
}
}
else -> {}
}
}
}

View file

@ -1,82 +0,0 @@
/*
* Copyright (c) 2021 Auxio Project
* LoadingViewModel.kt is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.loading
import android.content.Context
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
import org.oxycblt.auxio.music.MusicStore
/**
* ViewModel responsible for the loading UI and beginning the loading process overall.
* @author OxygenCobalt
*/
class LoadingViewModel : ViewModel() {
private val mResponse = MutableLiveData<MusicStore.Response?>(null)
private val mDoGrant = MutableLiveData(false)
private var isBusy = false
/** The last response from [MusicStore]. Null if the loading process is occurring. */
val response: LiveData<MusicStore.Response?> = mResponse
val doGrant: LiveData<Boolean> = mDoGrant
val loaded: Boolean get() = musicStore.loaded
private val musicStore = MusicStore.getInstance()
/**
* Begin the music loading process. The response from MusicStore is pushed to [response]
*/
fun load(context: Context) {
// Dont start a new load if the last one hasnt finished
if (isBusy) return
isBusy = true
mResponse.value = null
viewModelScope.launch {
mResponse.value = musicStore.load(context)
isBusy = false
}
}
/**
* Show the grant prompt.
*/
fun grant() {
mDoGrant.value = true
}
/**
* Mark that the grant prompt is now shown.
*/
fun doneWithGrant() {
mDoGrant.value = false
}
/**
* Notify this viewmodel that there are no permissions
*/
fun notifyNoPermissions() {
mResponse.value = MusicStore.Response.NO_PERMS
}
}

View file

@ -18,11 +18,14 @@
package org.oxycblt.auxio.music package org.oxycblt.auxio.music
import android.Manifest
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.ContentResolver import android.content.ContentResolver
import android.content.Context import android.content.Context
import android.content.pm.PackageManager
import android.net.Uri import android.net.Uri
import android.provider.OpenableColumns import android.provider.OpenableColumns
import androidx.core.content.ContextCompat
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
@ -46,25 +49,20 @@ class MusicStore private constructor() {
private var mSongs = listOf<Song>() private var mSongs = listOf<Song>()
val songs: List<Song> get() = mSongs val songs: List<Song> get() = mSongs
/** Marker for whether the music loading process has successfully completed. */
var loaded = false
private set
/** /**
* Load/Sort the entire music library. Should always be ran on a coroutine. * Load/Sort the entire music library. Should always be ran on a coroutine.
*/ */
suspend fun load(context: Context): Response { private fun load(context: Context): Response {
return withContext(Dispatchers.IO) {
loadMusicInternal(context)
}
}
/**
* Do the actual music loading process internally.
*/
private fun loadMusicInternal(context: Context): Response {
logD("Starting initial music load...") logD("Starting initial music load...")
val notGranted = ContextCompat.checkSelfPermission(
context, Manifest.permission.READ_EXTERNAL_STORAGE
) == PackageManager.PERMISSION_DENIED
if (notGranted) {
return Response.Err(ErrorKind.NO_PERMS)
}
try { try {
val start = System.currentTimeMillis() val start = System.currentTimeMillis()
@ -72,7 +70,7 @@ class MusicStore private constructor() {
loader.load() loader.load()
if (loader.songs.isEmpty()) { if (loader.songs.isEmpty()) {
return Response.NO_MUSIC return Response.Err(ErrorKind.NO_MUSIC)
} }
mSongs = loader.songs mSongs = loader.songs
@ -85,12 +83,10 @@ class MusicStore private constructor() {
logE("Something went horribly wrong.") logE("Something went horribly wrong.")
logE(e.stackTraceToString()) logE(e.stackTraceToString())
return Response.FAILED return Response.Err(ErrorKind.FAILED)
} }
loaded = true return Response.Ok(this)
return Response.SUCCESS
} }
/** /**
@ -121,31 +117,77 @@ class MusicStore private constructor() {
} }
/** /**
* Responses that [MusicStore] sends back when a [load] call completes. * A response that [MusicStore] returns when loading music.
* And before you ask, yes, I do like rust.
*/ */
enum class Response { sealed class Response {
NO_MUSIC, NO_PERMS, FAILED, SUCCESS class Ok(val musicStore: MusicStore) : Response()
class Err(val kind: ErrorKind) : Response()
}
enum class ErrorKind {
NO_PERMS, NO_MUSIC, FAILED
} }
companion object { companion object {
@Volatile @Volatile
private var INSTANCE: MusicStore? = null private var INSTANCE: Response? = null
/** /**
* Get/Instantiate the single instance of [MusicStore]. * Initialize the loading process for this instance. This must be ran on a background
* thread. If the instance has already been loaded successfully, then it will be returned
* immediately.
*/ */
fun getInstance(): MusicStore { suspend fun initInstance(context: Context): Response {
val currentInstance = INSTANCE val currentInstance = INSTANCE
if (currentInstance != null) { if (currentInstance is Response.Ok) {
return currentInstance return currentInstance
} }
return withContext(Dispatchers.IO) {
val result = MusicStore().load(context)
synchronized(this) { synchronized(this) {
val newInstance = MusicStore() INSTANCE = result
INSTANCE = newInstance
return newInstance
} }
result
}
}
/**
* Maybe get a MusicStore instance.
*
* @return null if the music store instance is still loading or if the loading process has
* encountered an error. An instance is returned otherwise.
*/
fun maybeGetInstance(): MusicStore? {
val currentInstance = INSTANCE
return if (currentInstance is Response.Ok) {
currentInstance.musicStore
} else {
null
}
}
/**
* Require a MusicStore instance. This function is dangerous and should only be used if
* it's guaranteed that the caller's code will only be called after the initial loading
* process.
*/
fun requireInstance(): MusicStore {
return requireNotNull(maybeGetInstance()) {
"MusicStore instance was not loaded or loading failed."
}
}
/**
* Check if this instance has successfully loaded or not.
*/
fun loaded(): Boolean {
return maybeGetInstance() != null
} }
} }
} }

View file

@ -99,9 +99,8 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
it.slice((mIndex.value!! + 1) until it.size) it.slice((mIndex.value!! + 1) until it.size)
} }
private val playbackManager = PlaybackStateManager.getInstance() private val playbackManager = PlaybackStateManager.maybeGetInstance()
private val settingsManager = SettingsManager.getInstance() private val settingsManager = SettingsManager.getInstance()
private val musicStore = MusicStore.getInstance()
init { init {
playbackManager.addCallback(this) playbackManager.addCallback(this)
@ -173,7 +172,7 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
*/ */
fun playWithUri(uri: Uri, context: Context) { fun playWithUri(uri: Uri, context: Context) {
// Check if everything is already running to run the URI play // Check if everything is already running to run the URI play
if (playbackManager.isRestored && musicStore.loaded) { if (playbackManager.isRestored && MusicStore.loaded()) {
playWithUriInternal(uri, context) playWithUriInternal(uri, context)
} else { } else {
logD("Cant play this URI right now, waiting...") logD("Cant play this URI right now, waiting...")
@ -189,18 +188,13 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
private fun playWithUriInternal(uri: Uri, context: Context) { private fun playWithUriInternal(uri: Uri, context: Context) {
logD("Playing with uri $uri") logD("Playing with uri $uri")
val musicStore = MusicStore.requireInstance()
musicStore.findSongForUri(uri, context.contentResolver)?.let { song -> musicStore.findSongForUri(uri, context.contentResolver)?.let { song ->
playSong(song) playSong(song)
} }
} }
/**
* Play all songs
*/
fun playAll() {
playbackManager.playAll()
}
/** /**
* Shuffle all songs * Shuffle all songs
*/ */
@ -370,6 +364,7 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
* Restore playback on startup. This can do one of two things: * Restore playback on startup. This can do one of two things:
* - Play a file intent that was given by MainActivity in [playWithUri] * - Play a file intent that was given by MainActivity in [playWithUri]
* - Restore the last playback state if there is no active file intent. * - Restore the last playback state if there is no active file intent.
* TODO: Re-add this to HomeFragment once state can be restored
*/ */
fun setupPlayback(context: Context) { fun setupPlayback(context: Context) {
val intentUri = mIntentUri val intentUri = mIntentUri

View file

@ -38,7 +38,7 @@ import org.oxycblt.auxio.util.logE
* - If you want to use the playback state in the UI, use [org.oxycblt.auxio.playback.PlaybackViewModel] as it can withstand volatile UIs. * - If you want to use the playback state in the UI, use [org.oxycblt.auxio.playback.PlaybackViewModel] as it can withstand volatile UIs.
* - If you want to use the playback state with the ExoPlayer instance or system-side things, use [org.oxycblt.auxio.playback.system.PlaybackService]. * - If you want to use the playback state with the ExoPlayer instance or system-side things, use [org.oxycblt.auxio.playback.system.PlaybackService].
* *
* All access should be done with [PlaybackStateManager.getInstance]. * All access should be done with [PlaybackStateManager.maybeGetInstance].
* @author OxygenCobalt * @author OxygenCobalt
*/ */
class PlaybackStateManager private constructor() { class PlaybackStateManager private constructor() {
@ -132,7 +132,6 @@ class PlaybackStateManager private constructor() {
val hasPlayed: Boolean get() = mHasPlayed val hasPlayed: Boolean get() = mHasPlayed
private val settingsManager = SettingsManager.getInstance() private val settingsManager = SettingsManager.getInstance()
private val musicStore = MusicStore.getInstance()
// --- CALLBACKS --- // --- CALLBACKS ---
@ -164,6 +163,8 @@ class PlaybackStateManager private constructor() {
when (mode) { when (mode) {
PlaybackMode.ALL_SONGS -> { PlaybackMode.ALL_SONGS -> {
val musicStore = MusicStore.requireInstance()
mParent = null mParent = null
mQueue = musicStore.songs.toMutableList() mQueue = musicStore.songs.toMutableList()
} }
@ -231,22 +232,12 @@ class PlaybackStateManager private constructor() {
updatePlayback(mQueue[0]) updatePlayback(mQueue[0])
} }
/**
* Play all songs.
*/
fun playAll() {
mMode = PlaybackMode.ALL_SONGS
mQueue = musicStore.songs.toMutableList()
mParent = null
setShuffling(false, keepSong = false)
updatePlayback(mQueue[0])
}
/** /**
* Shuffle all songs. * Shuffle all songs.
*/ */
fun shuffleAll() { fun shuffleAll() {
val musicStore = MusicStore.maybeGetInstance() ?: return
mMode = PlaybackMode.ALL_SONGS mMode = PlaybackMode.ALL_SONGS
mQueue = musicStore.songs.toMutableList() mQueue = musicStore.songs.toMutableList()
mParent = null mParent = null
@ -478,11 +469,17 @@ class PlaybackStateManager private constructor() {
private fun resetShuffle(keepSong: Boolean, useLastSong: Boolean) { private fun resetShuffle(keepSong: Boolean, useLastSong: Boolean) {
val lastSong = if (useLastSong) mQueue[mIndex] else mSong val lastSong = if (useLastSong) mQueue[mIndex] else mSong
val musicStore = MusicStore.requireInstance()
mQueue = when (mMode) { mQueue = when (mMode) {
PlaybackMode.IN_ARTIST -> orderSongsInArtist(mParent as Artist) PlaybackMode.ALL_SONGS ->
PlaybackMode.IN_ALBUM -> orderSongsInAlbum(mParent as Album) settingsManager.libSongSort.sortSongs(musicStore.songs).toMutableList()
PlaybackMode.IN_GENRE -> orderSongsInGenre(mParent as Genre) PlaybackMode.IN_ALBUM ->
PlaybackMode.ALL_SONGS -> orderSongs() settingsManager.detailAlbumSort.sortAlbum(mParent as Album).toMutableList()
PlaybackMode.IN_ARTIST ->
settingsManager.detailArtistSort.sortArtist(mParent as Artist).toMutableList()
PlaybackMode.IN_GENRE ->
settingsManager.detailGenreSort.sortGenre(mParent as Genre).toMutableList()
} }
if (keepSong) { if (keepSong) {
@ -610,8 +607,10 @@ class PlaybackStateManager private constructor() {
if (playbackState != null) { if (playbackState != null) {
logD("Found playback state $playbackState with queue size ${queueItems.size}") logD("Found playback state $playbackState with queue size ${queueItems.size}")
unpackFromPlaybackState(playbackState) val musicStore = MusicStore.requireInstance()
unpackQueue(queueItems)
unpackFromPlaybackState(playbackState, musicStore)
unpackQueue(queueItems, musicStore)
doParentSanityCheck() doParentSanityCheck()
} }
@ -640,12 +639,12 @@ class PlaybackStateManager private constructor() {
/** /**
* Unpack a [playbackState] into this instance. * Unpack a [playbackState] into this instance.
*/ */
private fun unpackFromPlaybackState(playbackState: DatabaseState) { private fun unpackFromPlaybackState(playbackState: DatabaseState, musicStore: MusicStore) {
// Turn the simplified information from PlaybackState into usable data. // Turn the simplified information from PlaybackState into usable data.
// Do queue setup first // Do queue setup first
mMode = PlaybackMode.fromInt(playbackState.mode) ?: PlaybackMode.ALL_SONGS mMode = PlaybackMode.fromInt(playbackState.mode) ?: PlaybackMode.ALL_SONGS
mParent = findParent(playbackState.parentHash, mMode) mParent = findParent(playbackState.parentHash, mMode, musicStore)
mIndex = playbackState.index mIndex = playbackState.index
// Then set up the current state // Then set up the current state
@ -663,7 +662,6 @@ class PlaybackStateManager private constructor() {
*/ */
private fun packQueue(): List<DatabaseQueueItem> { private fun packQueue(): List<DatabaseQueueItem> {
val unified = mutableListOf<DatabaseQueueItem>() val unified = mutableListOf<DatabaseQueueItem>()
var queueItemId = 0L var queueItemId = 0L
mUserQueue.forEach { song -> mUserQueue.forEach { song ->
@ -683,7 +681,7 @@ class PlaybackStateManager private constructor() {
* Unpack a list of queue items into a queue & user queue. * Unpack a list of queue items into a queue & user queue.
* @param queueItems The list of [DatabaseQueueItem]s to unpack. * @param queueItems The list of [DatabaseQueueItem]s to unpack.
*/ */
private fun unpackQueue(queueItems: List<DatabaseQueueItem>) { private fun unpackQueue(queueItems: List<DatabaseQueueItem>, musicStore: MusicStore) {
for (item in queueItems) { for (item in queueItems) {
musicStore.findSongFast(item.songHash, item.albumHash)?.let { song -> musicStore.findSongFast(item.songHash, item.albumHash)?.let { song ->
if (item.isUserQueue) { if (item.isUserQueue) {
@ -711,7 +709,7 @@ class PlaybackStateManager private constructor() {
/** /**
* Get a [Parent] from music store given a [hash] and PlaybackMode [mode]. * Get a [Parent] from music store given a [hash] and PlaybackMode [mode].
*/ */
private fun findParent(hash: Int, mode: PlaybackMode): Parent? { private fun findParent(hash: Int, mode: PlaybackMode, musicStore: MusicStore): Parent? {
return when (mode) { return when (mode) {
PlaybackMode.IN_GENRE -> musicStore.genres.find { it.hash == hash } PlaybackMode.IN_GENRE -> musicStore.genres.find { it.hash == hash }
PlaybackMode.IN_ARTIST -> musicStore.artists.find { it.hash == hash } PlaybackMode.IN_ARTIST -> musicStore.artists.find { it.hash == hash }
@ -737,36 +735,6 @@ class PlaybackStateManager private constructor() {
} }
} }
// --- ORDERING FUNCTIONS ---
/**
* Create an ordered queue based on the main list of songs
*/
private fun orderSongs(): MutableList<Song> {
return settingsManager.libSongSort.sortSongs(musicStore.songs).toMutableList()
}
/**
* Create an ordered queue based on an [Album].
*/
private fun orderSongsInAlbum(album: Album): MutableList<Song> {
return settingsManager.detailAlbumSort.sortAlbum(album).toMutableList()
}
/**
* Create an ordered queue based on an [Artist].
*/
private fun orderSongsInArtist(artist: Artist): MutableList<Song> {
return settingsManager.detailArtistSort.sortArtist(artist).toMutableList()
}
/**
* Create an ordered queue based on a [Genre].
*/
private fun orderSongsInGenre(genre: Genre): MutableList<Song> {
return settingsManager.detailGenreSort.sortGenre(genre).toMutableList()
}
/** /**
* The interface for receiving updates from [PlaybackStateManager]. * The interface for receiving updates from [PlaybackStateManager].
* Add the callback to [PlaybackStateManager] using [addCallback], * Add the callback to [PlaybackStateManager] using [addCallback],
@ -796,7 +764,7 @@ class PlaybackStateManager private constructor() {
/** /**
* Get/Instantiate the single instance of [PlaybackStateManager]. * Get/Instantiate the single instance of [PlaybackStateManager].
*/ */
fun getInstance(): PlaybackStateManager { fun maybeGetInstance(): PlaybackStateManager {
val currentInstance = INSTANCE val currentInstance = INSTANCE
if (currentInstance != null) { if (currentInstance != null) {

View file

@ -37,7 +37,7 @@ class AudioReactor(
context: Context, context: Context,
private val player: SimpleExoPlayer private val player: SimpleExoPlayer
) : AudioManager.OnAudioFocusChangeListener { ) : AudioManager.OnAudioFocusChangeListener {
private val playbackManager = PlaybackStateManager.getInstance() private val playbackManager = PlaybackStateManager.maybeGetInstance()
private val settingsManager = SettingsManager.getInstance() private val settingsManager = SettingsManager.getInstance()
private val audioManager = context.getSystemServiceSafe(AudioManager::class) private val audioManager = context.getSystemServiceSafe(AudioManager::class)

View file

@ -92,7 +92,7 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac
private val systemReceiver = SystemEventReceiver() private val systemReceiver = SystemEventReceiver()
// Managers // Managers
private val playbackManager = PlaybackStateManager.getInstance() private val playbackManager = PlaybackStateManager.maybeGetInstance()
private val settingsManager = SettingsManager.getInstance() private val settingsManager = SettingsManager.getInstance()
// State // State

View file

@ -39,7 +39,7 @@ class PlaybackSessionConnector(
private val player: Player, private val player: Player,
private val mediaSession: MediaSessionCompat private val mediaSession: MediaSessionCompat
) : PlaybackStateManager.Callback, Player.Listener, MediaSessionCompat.Callback() { ) : PlaybackStateManager.Callback, Player.Listener, MediaSessionCompat.Callback() {
private val playbackManager = PlaybackStateManager.getInstance() private val playbackManager = PlaybackStateManager.maybeGetInstance()
private val emptyMetadata = MediaMetadataCompat.Builder().build() private val emptyMetadata = MediaMetadataCompat.Builder().build()
init { init {

View file

@ -48,7 +48,6 @@ class SearchViewModel : ViewModel() {
val isNavigating: Boolean get() = mIsNavigating val isNavigating: Boolean get() = mIsNavigating
val filterMode: DisplayMode? get() = mFilterMode val filterMode: DisplayMode? get() = mFilterMode
private val musicStore = MusicStore.getInstance()
private val settingsManager = SettingsManager.getInstance() private val settingsManager = SettingsManager.getInstance()
init { init {
@ -60,9 +59,10 @@ class SearchViewModel : ViewModel() {
* Will push results to [searchResults]. * Will push results to [searchResults].
*/ */
fun doSearch(query: String, context: Context) { fun doSearch(query: String, context: Context) {
val musicStore = MusicStore.maybeGetInstance()
mLastQuery = query mLastQuery = query
if (query.isEmpty()) { if (query.isEmpty() || musicStore == null) {
mSearchResults.value = listOf() mSearchResults.value = listOf()
return return

View file

@ -29,11 +29,13 @@ import android.view.ViewGroup
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentAboutBinding import org.oxycblt.auxio.databinding.FragmentAboutBinding
import org.oxycblt.auxio.home.HomeViewModel
import org.oxycblt.auxio.music.MusicStore import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.util.applyEdge import org.oxycblt.auxio.util.applyEdge
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
@ -44,13 +46,14 @@ import org.oxycblt.auxio.util.showToast
* @author OxygenCobalt * @author OxygenCobalt
*/ */
class AboutFragment : Fragment() { class AboutFragment : Fragment() {
private val homeModel: HomeViewModel by activityViewModels()
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View { ): View {
val binding = FragmentAboutBinding.inflate(layoutInflater) val binding = FragmentAboutBinding.inflate(layoutInflater)
val musicStore = MusicStore.getInstance()
binding.applyEdge { bars -> binding.applyEdge { bars ->
binding.aboutAppbar.updatePadding(top = bars.top) binding.aboutAppbar.updatePadding(top = bars.top)
@ -65,9 +68,17 @@ class AboutFragment : Fragment() {
binding.aboutCode.setOnClickListener { openLinkInBrowser(LINK_CODEBASE) } binding.aboutCode.setOnClickListener { openLinkInBrowser(LINK_CODEBASE) }
binding.aboutFaq.setOnClickListener { openLinkInBrowser(LINK_FAQ) } binding.aboutFaq.setOnClickListener { openLinkInBrowser(LINK_FAQ) }
binding.aboutLicenses.setOnClickListener { openLinkInBrowser(LINK_LICENSES) } binding.aboutLicenses.setOnClickListener { openLinkInBrowser(LINK_LICENSES) }
homeModel.loaderResponse.observe(viewLifecycleOwner) { response ->
val count = when (response) {
is MusicStore.Response.Ok -> response.musicStore.songs.size
else -> 0
}
binding.aboutSongCount.text = getString( binding.aboutSongCount.text = getString(
R.string.fmt_songs_loaded, musicStore.songs.size R.string.fmt_songs_loaded, count
) )
}
logD("Dialog created.") logD("Dialog created.")

View file

@ -191,10 +191,12 @@ fun View.applyEdge(onApply: (Rect) -> Unit) {
* Stopgap measure to make edge-to-edge work on views that also have a playback bar. * Stopgap measure to make edge-to-edge work on views that also have a playback bar.
* The issue is that while we can apply padding initially, the padding will still be applied * The issue is that while we can apply padding initially, the padding will still be applied
* when the bar is shown, which is very ungood. We mitigate this by just checking the song state * when the bar is shown, which is very ungood. We mitigate this by just checking the song state
* and removing the padding if it isnt available, which works okayish. I think Material Files has * and removing the padding if there is one, which is a stupidly fragile band-aid but it
* a better implementation of the same fix however, so once I'm able to hack that layout into * works.
* Auxio things should be better. *
* TODO: Get rid of this get rid of this get rid of this * TODO: Dumpster this and replace it with a dedicated layout. Only issue with that is how
* nested our layouts are, which basically forces us to do recursion magic. Hai Zhang's Material
* Files layout may help in this task.
*/ */
fun View.applyEdgeRespectingBar( fun View.applyEdgeRespectingBar(
playbackModel: PlaybackViewModel, playbackModel: PlaybackViewModel,

View file

@ -33,7 +33,7 @@ import org.oxycblt.auxio.settings.SettingsManager
class WidgetController(private val context: Context) : class WidgetController(private val context: Context) :
PlaybackStateManager.Callback, PlaybackStateManager.Callback,
SettingsManager.Callback { SettingsManager.Callback {
private val playbackManager = PlaybackStateManager.getInstance() private val playbackManager = PlaybackStateManager.maybeGetInstance()
private val settingsManager = SettingsManager.getInstance() private val settingsManager = SettingsManager.getInstance()
private val widget = WidgetProvider() private val widget = WidgetProvider()

View file

@ -55,6 +55,7 @@
android:layout_margin="@dimen/spacing_medium" android:layout_margin="@dimen/spacing_medium"
android:contentDescription="@string/desc_shuffle_all" android:contentDescription="@string/desc_shuffle_all"
app:layout_anchor="@id/home_pager" app:layout_anchor="@id/home_pager"
app:tint="?attr/colorControlNormal"
app:layout_behavior="com.google.android.material.behavior.HideBottomViewOnScrollBehavior" app:layout_behavior="com.google.android.material.behavior.HideBottomViewOnScrollBehavior"
app:layout_anchorGravity="bottom|end" /> app:layout_anchorGravity="bottom|end" />

View file

@ -1,64 +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">
<data>
<variable
name="loadingModel"
type="org.oxycblt.auxio.loading.LoadingViewModel" />
</data>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/loading_panel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/spacing_large"
android:animateLayoutChanges="true"
android:layout_gravity="center"
android:gravity="center"
android:orientation="vertical">
<ProgressBar
android:id="@+id/loading_circle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="@+id/loading_action_button"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/loading_error_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/spacing_small"
android:fontFamily="@font/inter_semibold"
android:textAlignment="center"
android:textColor="?android:attr/textColorPrimary"
android:textSize="@dimen/text_size_medium"
android:visibility="invisible"
app:layout_constraintBottom_toTopOf="@+id/loading_action_button"
tools:text="No Music Found" />
<com.google.android.material.button.MaterialButton
android:id="@+id/loading_action_button"
style="@style/Widget.Auxio.Button.Primary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="@dimen/spacing_insane"
android:paddingEnd="@dimen/spacing_insane"
android:text="@string/lbl_retry"
android:visibility="invisible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</FrameLayout>
</layout>

View file

@ -2,23 +2,7 @@
<navigation xmlns:android="http://schemas.android.com/apk/res/android" <navigation 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"
app:startDestination="@id/loading_fragment"> app:startDestination="@id/main_fragment">
<fragment
android:id="@+id/loading_fragment"
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/main_fragment"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:launchSingleTop="true"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim"
app:popUpTo="@id/loading_fragment"
app:popUpToInclusive="true" />
</fragment>
<fragment <fragment
android:id="@+id/main_fragment" android:id="@+id/main_fragment"
android:name="org.oxycblt.auxio.MainFragment" android:name="org.oxycblt.auxio.MainFragment"