## 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" />
-
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index 2e7fc651d..71c88c72d 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -89,7 +89,7 @@
Keine Musik gefunden
- Laden die Musik fehlgeschlagen
+ Laden die Musik fehlgeschlagenAuxio braucht Berechtigung, zu lesen deine MusikbibliothekLink könnte nicht geöffnet werden
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index f3732552d..7a5b075a0 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -8,6 +8,7 @@
RetryGrant
+ View ErrorLibraryGenres
@@ -90,7 +91,7 @@
No music found
- Music loading failed
+ Music loading failedAuxio needs permission to read your music libraryCould not open link
diff --git a/info/FAQ.md b/info/FAQ.md
index ae2a4dd17..ad853b307 100644
--- a/info/FAQ.md
+++ b/info/FAQ.md
@@ -14,7 +14,7 @@ Currently, its available on the [IzzyOnDroid F-Droid repository](https://apt.izz
## Can I translate Auxio to my native language?
-See the [Translations](https://github.com/OxygenCobalt/Auxio/issues/3) issue for guidance on how to create translations and submit them to the project. Any contributions are apprieciated.
+See the [Translations](https://github.com/OxygenCobalt/Auxio/issues/3) issue for guidance on how to create translations and submit them to the project. Any contributions are appreciated.
## How can I contribute/report issues?