diff --git a/README.md b/README.md index 5b860d378..c4ace68b2 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ Minimum SDK

FAQ / Formats / Licenses / Contributing

+

## About diff --git a/app/src/main/java/org/oxycblt/auxio/loading/LoadingFragment.kt b/app/src/main/java/org/oxycblt/auxio/loading/LoadingFragment.kt index 3b78e4d19..10b12aa23 100644 --- a/app/src/main/java/org/oxycblt/auxio/loading/LoadingFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/loading/LoadingFragment.kt @@ -13,17 +13,9 @@ 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.logD import org.oxycblt.auxio.music.MusicStore -import org.oxycblt.auxio.music.processing.MusicLoader -/** - * An intermediary [Fragment] that asks for the READ_EXTERNAL_STORAGE permission and runs - * the music loading process in the background. - * @author OxygenCobalt - */ -class LoadingFragment : Fragment(R.layout.fragment_loading) { - // LoadingViewModel is scoped to this fragment only +class LoadingFragment : Fragment() { private val loadingModel: LoadingViewModel by viewModels { LoadingViewModel.Factory(requireActivity().application) } @@ -35,115 +27,110 @@ class LoadingFragment : Fragment(R.layout.fragment_loading) { ): View { val binding = FragmentLoadingBinding.inflate(inflater) - // Set up the permission launcher, as its disallowed outside of onCreate. - val permLauncher = - registerForActivityResult( - ActivityResultContracts.RequestPermission() - ) { granted: Boolean -> - // If its actually granted, restart the loading process again. - if (granted) { - returnToLoading(binding) - - loadingModel.reload() - } else { - showError(binding) - - binding.loadingGrantButton.visibility = View.VISIBLE - binding.loadingErrorText.text = getString(R.string.error_no_perms) - } - } + // Build the permission launcher here as you can only do it in onCreateView/onCreate + val permLauncher = registerForActivityResult( + ActivityResultContracts.RequestPermission(), ::onPermResult + ) // --- UI SETUP --- - binding.lifecycleOwner = this binding.loadingModel = loadingModel // --- VIEWMODEL SETUP --- - loadingModel.response.observe(viewLifecycleOwner) { - if (it == MusicLoader.Response.SUCCESS) { - 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.loadingErrorText.text = - if (it == MusicLoader.Response.NO_MUSIC) - getString(R.string.error_no_music) - else - getString(R.string.error_music_load_failed) - - showError(binding) - binding.loadingRetryButton.visibility = View.VISIBLE - } - } - - loadingModel.doReload.observe(viewLifecycleOwner) { - if (it) { - returnToLoading(binding) - loadingModel.doneWithReload() - } - } - loadingModel.doGrant.observe(viewLifecycleOwner) { if (it) { permLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE) - returnToLoading(binding) loadingModel.doneWithGrant() } } - // Force an error screen if the permissions are denied or the prompt needs to be shown. - if (checkPerms()) { - showError(binding) + loadingModel.response.observe(viewLifecycleOwner) { response -> + when (response) { + // Success should lead to Auxio navigating away from the fragment + MusicStore.Response.SUCCESS -> findNavController().navigate( + LoadingFragmentDirections.actionToMain() + ) - binding.loadingGrantButton.visibility = View.VISIBLE - binding.loadingErrorText.text = getString(R.string.error_no_perms) - } else { - loadingModel.go() + // Null means that the loading process is going on + null -> showLoading(binding) + + // Anything else is an error + else -> { + showError(binding, response) + } + } } - logD("Fragment created.") + if (noPermissions()) { + // MusicStore.Response.NO_PERMS isnt actually returned by MusicStore, its just + // a way to keep the current permission state on_hand + loadingModel.notifyNoPermissions() + } + + if (loadingModel.response.value == null) { + loadingModel.load() + } return binding.root } - override fun onResume() { - super.onResume() + // --- PERMISSIONS --- - // If the music was already loaded, then don't do it again. - if (MusicStore.getInstance().loaded) { - findNavController().navigate( - LoadingFragmentDirections.actionToMain() - ) + private fun noPermissions(): 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() } } - // Check for two things: - // - If Auxio needs to show the rationale for getting the READ_EXTERNAL_STORAGE permission. - // - If Auxio straight up doesn't have the READ_EXTERNAL_STORAGE permission. - private fun checkPerms(): Boolean { - return shouldShowRequestPermissionRationale( - Manifest.permission.READ_EXTERNAL_STORAGE - ) || ContextCompat.checkSelfPermission( - requireContext(), Manifest.permission.READ_EXTERNAL_STORAGE - ) == PackageManager.PERMISSION_DENIED + // --- UI DISPLAY --- + + private fun showLoading(binding: FragmentLoadingBinding) { + binding.apply { + loadingCircle.visibility = View.VISIBLE + loadingErrorIcon.visibility = View.GONE + loadingErrorText.visibility = View.GONE + loadingRetryButton.visibility = View.GONE + loadingGrantButton.visibility = View.GONE + } } - // Remove the loading indicator and show the error groups - private fun showError(binding: FragmentLoadingBinding) { - binding.loadingBar.visibility = View.GONE + private fun showError(binding: FragmentLoadingBinding, error: MusicStore.Response) { + binding.loadingCircle.visibility = View.GONE binding.loadingErrorIcon.visibility = View.VISIBLE binding.loadingErrorText.visibility = View.VISIBLE - } - // Wipe views and switch back to the plain ProgressBar - private fun returnToLoading(binding: FragmentLoadingBinding) { - binding.loadingBar.visibility = View.VISIBLE - binding.loadingErrorText.visibility = View.GONE - binding.loadingErrorIcon.visibility = View.GONE - binding.loadingRetryButton.visibility = View.GONE - binding.loadingGrantButton.visibility = View.GONE + when (error) { + MusicStore.Response.NO_MUSIC -> { + binding.loadingRetryButton.visibility = View.VISIBLE + binding.loadingErrorText.text = getString(R.string.error_no_music) + } + + MusicStore.Response.NO_PERMS -> { + binding.loadingGrantButton.visibility = View.VISIBLE + binding.loadingErrorText.text = getString(R.string.error_no_perms) + } + + MusicStore.Response.FAILED -> { + binding.loadingRetryButton.visibility = View.VISIBLE + binding.loadingErrorText.text = getString(R.string.error_load_failed) + } + + else -> {} + } } } diff --git a/app/src/main/java/org/oxycblt/auxio/loading/LoadingViewModel.kt b/app/src/main/java/org/oxycblt/auxio/loading/LoadingViewModel.kt index 62400e9cf..dd25e5634 100644 --- a/app/src/main/java/org/oxycblt/auxio/loading/LoadingViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/loading/LoadingViewModel.kt @@ -6,87 +6,47 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import org.oxycblt.auxio.music.MusicStore -import org.oxycblt.auxio.music.processing.MusicLoader -/** - * A [ViewModel] responsible for getting the music loading process going and managing the response - * returned. - * @author OxygenCobalt - */ class LoadingViewModel(private val app: Application) : ViewModel() { - private val mResponse = MutableLiveData() - val response: LiveData get() = mResponse + private val mResponse = MutableLiveData(null) + val response: LiveData = mResponse - private val mRedo = MutableLiveData() - val doReload: LiveData get() = mRedo + private val mDoGrant = MutableLiveData(false) + val doGrant: LiveData = mDoGrant - private val mDoGrant = MutableLiveData() - val doGrant: LiveData get() = mDoGrant + private var isBusy = false - private var started = false + private val musicStore = MusicStore.getInstance() - /** - * Start the music loading sequence. - * This should only be ran once, use reload() for all other loads. - */ - fun go() { - if (!started) { - started = true - doLoad() - } - } + fun load() { + // Dont start a new load if the last one hasnt finished + if (isBusy) return + + isBusy = true + mResponse.value = null - private fun doLoad() { viewModelScope.launch { - val musicStore = MusicStore.getInstance() - - val response = musicStore.load(app) - - withContext(Dispatchers.Main) { - mResponse.value = response - } + mResponse.value = musicStore.load(app) + isBusy = false } } - /** - * Reload the music - */ - fun reload() { - mRedo.value = true - - doLoad() - } - - /** - * Mark that the UI is done with the reload call - */ - fun doneWithReload() { - mRedo.value = false - } - - /** - * Mark to start the grant process - */ fun grant() { mDoGrant.value = true } - /** - * Mark that the UI is done with the grant process. - */ fun doneWithGrant() { mDoGrant.value = false } - /** - * Factory for [LoadingViewModel] instances. - */ + fun notifyNoPermissions() { + mResponse.value = MusicStore.Response.NO_PERMS + } + + @Suppress("UNCHECKED_CAST") 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 diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicStore.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicStore.kt index d39e4057d..3e76ccf82 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicStore.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicStore.kt @@ -41,7 +41,7 @@ class MusicStore private constructor() { * ***THIS SHOULD ONLY BE RAN FROM AN IO THREAD.*** * @param app [Application] required to load the music. */ - suspend fun load(app: Application): MusicLoader.Response { + suspend fun load(app: Application): Response { return withContext(Dispatchers.IO) { this@MusicStore.logD("Starting initial music load...") @@ -50,7 +50,7 @@ class MusicStore private constructor() { val loader = MusicLoader(app) val response = loader.loadMusic() - if (response == MusicLoader.Response.SUCCESS) { + if (response == Response.SUCCESS) { // If the loading succeeds, then sort the songs and update the value val sorter = MusicSorter(loader.songs, loader.albums) @@ -72,6 +72,10 @@ class MusicStore private constructor() { } } + enum class Response { + NO_MUSIC, NO_PERMS, FAILED, SUCCESS + } + companion object { @Volatile private var INSTANCE: MusicStore? = null diff --git a/app/src/main/java/org/oxycblt/auxio/music/processing/MusicLoader.kt b/app/src/main/java/org/oxycblt/auxio/music/processing/MusicLoader.kt index 05fc72c90..c67fa23cf 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/processing/MusicLoader.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/processing/MusicLoader.kt @@ -12,6 +12,7 @@ import org.oxycblt.auxio.logD import org.oxycblt.auxio.logE import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Genre +import org.oxycblt.auxio.music.MusicStore import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.toAlbumArtURI @@ -26,23 +27,25 @@ class MusicLoader(private val app: Application) { private val resolver = app.contentResolver - fun loadMusic(): Response { + fun loadMusic(): MusicStore.Response { try { loadGenres() loadAlbums() loadSongs() } catch (error: Exception) { - logE("Something went horribly wrong.") - error.printStackTrace() + val trace = error.stackTraceToString() - return Response.FAILED + logE("Something went horribly wrong.") + logE(trace) + + return MusicStore.Response.FAILED } if (songs.isEmpty()) { - return Response.NO_MUSIC + return MusicStore.Response.NO_MUSIC } - return Response.SUCCESS + return MusicStore.Response.SUCCESS } private fun loadGenres() { @@ -89,7 +92,6 @@ class MusicLoader(private val app: Application) { Albums._ID, // 0 Albums.ALBUM, // 1 Albums.ARTIST, // 2 - Albums.FIRST_YEAR, // 3 ), null, null, @@ -207,8 +209,4 @@ class MusicLoader(private val app: Application) { logD("Song search finished with ${songs.size} found") } - - enum class Response { - SUCCESS, FAILED, NO_MUSIC - } } diff --git a/app/src/main/java/org/oxycblt/auxio/ui/Accent.kt b/app/src/main/java/org/oxycblt/auxio/ui/Accent.kt index e8dd7bf3d..09e3e1f31 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/Accent.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/Accent.kt @@ -7,7 +7,7 @@ import android.text.Spanned import androidx.annotation.ColorRes import androidx.annotation.StringRes import androidx.annotation.StyleRes -import androidx.core.text.toSpanned +import androidx.core.text.HtmlCompat import org.oxycblt.auxio.R import org.oxycblt.auxio.settings.SettingsManager import java.util.Locale @@ -59,10 +59,10 @@ data class Accent(@ColorRes val color: Int, @StyleRes val theme: Int, @StringRes val name = context.getString(name) val hex = context.getString(color).toUpperCase(Locale.getDefault()) - return context.getString( - R.string.format_accent_summary, - name, hex - ).toSpanned().render() + return HtmlCompat.fromHtml( + context.getString(R.string.format_accent_summary, name, hex), + HtmlCompat.FROM_HTML_MODE_COMPACT + ) } companion object { @@ -70,8 +70,7 @@ data class Accent(@ColorRes val color: Int, @StyleRes val theme: Int, @StringRes private var current: Accent? = null /** - * Get the current accent, will default to whatever is stored in [SettingsManager] - * if there isnt one. + * Get the current accent. * @return The current accent */ fun get(): Accent { diff --git a/app/src/main/java/org/oxycblt/auxio/ui/InterfaceUtils.kt b/app/src/main/java/org/oxycblt/auxio/ui/InterfaceUtils.kt index 0d5114e59..72cf116d3 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/InterfaceUtils.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/InterfaceUtils.kt @@ -8,7 +8,6 @@ import android.content.res.Resources import android.graphics.Point import android.graphics.drawable.AnimatedVectorDrawable import android.os.Build -import android.text.Spanned import android.util.DisplayMetrics import android.view.LayoutInflater import android.view.View @@ -22,7 +21,6 @@ import androidx.annotation.DrawableRes import androidx.annotation.PluralsRes import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat -import androidx.core.text.HtmlCompat import androidx.fragment.app.Fragment import androidx.recyclerview.widget.RecyclerView import com.google.android.material.button.MaterialButton @@ -109,16 +107,6 @@ fun Fragment.requireCompatActivity(): AppCompatActivity { } } -/** - * "Render" a [Spanned] using [HtmlCompat]. (As in making text bolded and whatnot). - * @return A [Spanned] that actually works. - */ -fun Spanned.render(): Spanned { - return HtmlCompat.fromHtml( - this.toString(), HtmlCompat.FROM_HTML_OPTION_USE_CSS_COLORS - ) -} - /** * Resolve a color. * @param context [Context] required diff --git a/app/src/main/java/org/oxycblt/auxio/ui/SlideLinearLayout.kt b/app/src/main/java/org/oxycblt/auxio/ui/SlideLinearLayout.kt index 2a79fc39f..ae8933a39 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/SlideLinearLayout.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/SlideLinearLayout.kt @@ -39,7 +39,7 @@ class SlideLinearLayout @JvmOverloads constructor( init { if (disappearingChildrenField != null) { - // Create a junk view and add it, which makes all the magic happen [I think]. + // Create a invisible junk view and add it, which makes all the magic happen [I think]. dumpView = View(context) addView(dumpView, 0, 0) } @@ -58,13 +58,18 @@ class SlideLinearLayout @JvmOverloads constructor( val children = getDisappearingChildren() if (doDrawingTrick && children != null) { - if (child == dumpView) { // Use the dump view as a marker for when to do the trick - var more = false + // Use the dump view as a marker for when to do the trick + if (child == dumpView) { + // I dont even know what this does. + var consumed = false + children.forEach { - more = more or super.drawChild(canvas, it, drawingTime) // What???? + consumed = consumed or super.drawChild(canvas, it, drawingTime) } - return more - } else if (children.contains(child)) { // Ignore the disappearing children + + return consumed + } else if (children.contains(child)) { + // Ignore the disappearing children return false } } diff --git a/app/src/main/res/layout/fragment_loading.xml b/app/src/main/res/layout/fragment_loading.xml index 7a38d5af7..629d64a83 100644 --- a/app/src/main/res/layout/fragment_loading.xml +++ b/app/src/main/res/layout/fragment_loading.xml @@ -1,34 +1,30 @@ - + xmlns:tools="http://schemas.android.com/tools"> - - + android:orientation="vertical" + android:animateLayoutChanges="true" + android:gravity="center"> + android:layout_margin="@dimen/margin_small" + android:paddingBottom="@dimen/padding_tiny" /> + tools:visibility="visible"/> + tools:text="Some kind of error" />