diff --git a/app/src/main/java/org/oxycblt/auxio/home/ErrorDetailsDialog.kt b/app/src/main/java/org/oxycblt/auxio/home/ErrorDetailsDialog.kt
new file mode 100644
index 000000000..19ac97a34
--- /dev/null
+++ b/app/src/main/java/org/oxycblt/auxio/home/ErrorDetailsDialog.kt
@@ -0,0 +1,89 @@
+/*
+ * Copyright (c) 2023 Auxio Project
+ * ErrorDetailsDialog.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 .
+ */
+
+package org.oxycblt.auxio.home
+
+import android.content.ClipData
+import android.content.ClipboardManager
+import android.os.Build
+import android.os.Bundle
+import android.view.LayoutInflater
+import androidx.appcompat.app.AlertDialog
+import androidx.navigation.fragment.navArgs
+import org.oxycblt.auxio.R
+import org.oxycblt.auxio.databinding.DialogErrorDetailsBinding
+import org.oxycblt.auxio.ui.ViewBindingMaterialDialogFragment
+import org.oxycblt.auxio.util.getSystemServiceCompat
+import org.oxycblt.auxio.util.openInBrowser
+import org.oxycblt.auxio.util.showToast
+
+/**
+ * A dialog that shows a stack trace for a music loading error.
+ *
+ * TODO: Extend to other errors
+ *
+ * @author Alexander Capehart (OxygenCobalt)
+ */
+class ErrorDetailsDialog : ViewBindingMaterialDialogFragment() {
+ private val args: ErrorDetailsDialogArgs by navArgs()
+ private var clipboardManager: ClipboardManager? = null
+
+ override fun onConfigDialog(builder: AlertDialog.Builder) {
+ builder
+ .setTitle(R.string.lbl_error_info)
+ .setPositiveButton(R.string.lbl_report) { _, _ ->
+ requireContext().openInBrowser(LINK_ISSUES)
+ }
+ .setNegativeButton(R.string.lbl_cancel, null)
+ }
+
+ override fun onCreateBinding(inflater: LayoutInflater) =
+ DialogErrorDetailsBinding.inflate(inflater)
+
+ override fun onBindingCreated(binding: DialogErrorDetailsBinding, savedInstanceState: Bundle?) {
+ super.onBindingCreated(binding, savedInstanceState)
+
+ clipboardManager = requireContext().getSystemServiceCompat(ClipboardManager::class)
+
+ // --- UI SETUP ---
+ binding.errorStackTrace.text = args.error.stackTraceToString().trimEnd('\n')
+ binding.errorCopy.setOnClickListener { copyStackTrace() }
+ }
+
+ override fun onDestroyBinding(binding: DialogErrorDetailsBinding) {
+ super.onDestroyBinding(binding)
+ clipboardManager = null
+ }
+
+ private fun copyStackTrace() {
+ requireNotNull(clipboardManager) { "Clipboard was unavailable" }
+ .setPrimaryClip(
+ ClipData.newPlainText("Exception Stack Trace", args.error.stackTraceToString()))
+ // A copy notice is shown by the system from Android 13 onwards
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
+ requireContext().showToast(R.string.lbl_copied)
+ }
+ }
+
+ private companion object {
+ /** The URL to the bug report issue form */
+ const val LINK_ISSUES =
+ "https://github.com/OxygenCobalt/Auxio/issues/new" +
+ "?assignees=OxygenCobalt&labels=bug&projects=&template=bug-crash-report.yml"
+ }
+}
diff --git a/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt
index 7228e10ec..c016c6602 100644
--- a/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt
+++ b/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt
@@ -330,11 +330,12 @@ class HomeFragment :
}
}
- private fun setupCompleteState(binding: FragmentHomeBinding, error: Throwable?) {
+ private fun setupCompleteState(binding: FragmentHomeBinding, error: Exception?) {
if (error == null) {
logD("Received ok response")
binding.homeFab.show()
binding.homeIndexingContainer.visibility = View.INVISIBLE
+ binding.homeIndexingError.visibility = View.INVISIBLE
return
}
@@ -357,6 +358,7 @@ class HomeFragment :
.launch(PERMISSION_READ_AUDIO)
}
}
+ binding.homeIndexingError.visibility = View.INVISIBLE
}
is NoMusicException -> {
logD("Showing no music error")
@@ -367,6 +369,7 @@ class HomeFragment :
text = context.getString(R.string.lbl_retry)
setOnClickListener { musicModel.refresh() }
}
+ binding.homeIndexingError.visibility = View.INVISIBLE
}
else -> {
logD("Showing generic error")
@@ -377,6 +380,12 @@ class HomeFragment :
text = context.getString(R.string.lbl_retry)
setOnClickListener { musicModel.rescan() }
}
+ binding.homeIndexingError.apply {
+ visibility = View.VISIBLE
+ setOnClickListener {
+ findNavController().navigateSafe(HomeFragmentDirections.reportError(error))
+ }
+ }
}
}
}
diff --git a/app/src/main/java/org/oxycblt/auxio/music/Indexing.kt b/app/src/main/java/org/oxycblt/auxio/music/Indexing.kt
index a185d5b2f..d4e582660 100644
--- a/app/src/main/java/org/oxycblt/auxio/music/Indexing.kt
+++ b/app/src/main/java/org/oxycblt/auxio/music/Indexing.kt
@@ -47,7 +47,7 @@ sealed interface IndexingState {
* @param error If music loading has failed, the error that occurred will be here. Otherwise, it
* will be null.
*/
- data class Completed(val error: Throwable?) : IndexingState
+ data class Completed(val error: Exception?) : IndexingState
}
/**
diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt
index 6e45c5ae9..d5263da7b 100644
--- a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt
+++ b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt
@@ -371,6 +371,7 @@ constructor(
// parallel.
logD("Starting MediaStore query")
emitIndexingProgress(IndexingProgress.Indeterminate)
+
val mediaStoreQueryJob =
worker.scope.async {
val query =
diff --git a/app/src/main/java/org/oxycblt/auxio/settings/AboutFragment.kt b/app/src/main/java/org/oxycblt/auxio/settings/AboutFragment.kt
index cd6217a9f..3c3258ab9 100644
--- a/app/src/main/java/org/oxycblt/auxio/settings/AboutFragment.kt
+++ b/app/src/main/java/org/oxycblt/auxio/settings/AboutFragment.kt
@@ -18,13 +18,8 @@
package org.oxycblt.auxio.settings
-import android.content.ActivityNotFoundException
-import android.content.Intent
-import android.content.pm.PackageManager
-import android.os.Build
import android.os.Bundle
import android.view.LayoutInflater
-import androidx.core.net.toUri
import androidx.core.view.updatePadding
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
@@ -37,8 +32,7 @@ import org.oxycblt.auxio.music.MusicViewModel
import org.oxycblt.auxio.playback.formatDurationMs
import org.oxycblt.auxio.ui.ViewBindingFragment
import org.oxycblt.auxio.util.collectImmediately
-import org.oxycblt.auxio.util.logD
-import org.oxycblt.auxio.util.showToast
+import org.oxycblt.auxio.util.openInBrowser
import org.oxycblt.auxio.util.systemBarInsetsCompat
/**
@@ -69,10 +63,10 @@ class AboutFragment : ViewBindingFragment() {
}
binding.aboutVersion.text = BuildConfig.VERSION_NAME
- binding.aboutCode.setOnClickListener { openLinkInBrowser(LINK_SOURCE) }
- binding.aboutWiki.setOnClickListener { openLinkInBrowser(LINK_WIKI) }
- binding.aboutLicenses.setOnClickListener { openLinkInBrowser(LINK_LICENSES) }
- binding.aboutAuthor.setOnClickListener { openLinkInBrowser(LINK_AUTHOR) }
+ binding.aboutCode.setOnClickListener { requireContext().openInBrowser(LINK_SOURCE) }
+ binding.aboutWiki.setOnClickListener { requireContext().openInBrowser(LINK_WIKI) }
+ binding.aboutLicenses.setOnClickListener { requireContext().openInBrowser(LINK_LICENSES) }
+ binding.aboutAuthor.setOnClickListener { requireContext().openInBrowser(LINK_AUTHOR) }
// VIEWMODEL SETUP
collectImmediately(musicModel.statistics, ::updateStatistics)
@@ -93,74 +87,6 @@ class AboutFragment : ViewBindingFragment() {
(statistics?.durationMs ?: 0).formatDurationMs(false))
}
- /**
- * Open the given URI in a web browser.
- *
- * @param uri The URL to open.
- */
- private fun openLinkInBrowser(uri: String) {
- logD("Opening $uri")
- val context = requireContext()
- val browserIntent =
- Intent(Intent.ACTION_VIEW, uri.toUri()).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
-
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
- // Android 11 seems to now handle the app chooser situations on its own now
- // [along with adding a new permission that breaks the old manual code], so
- // we just do a typical activity launch.
- logD("Using API 30+ chooser")
- try {
- context.startActivity(browserIntent)
- } catch (e: ActivityNotFoundException) {
- // No app installed to open the link
- context.showToast(R.string.err_no_app)
- }
- } else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
- // On older versions of android, opening links from an ACTION_VIEW intent might
- // not work in all cases, especially when no default app was set. If that is the
- // case, we will try to manually handle these cases before we try to launch the
- // browser.
- logD("Resolving browser activity for chooser")
- val pkgName =
- context.packageManager
- .resolveActivity(browserIntent, PackageManager.MATCH_DEFAULT_ONLY)
- ?.run { activityInfo.packageName }
-
- if (pkgName != null) {
- if (pkgName == "android") {
- // No default browser [Must open app chooser, may not be supported]
- logD("No default browser found")
- openAppChooser(browserIntent)
- } else logD("Opening browser intent")
- try {
- browserIntent.setPackage(pkgName)
- startActivity(browserIntent)
- } catch (e: ActivityNotFoundException) {
- // Not a browser but an app chooser
- browserIntent.setPackage(null)
- openAppChooser(browserIntent)
- }
- } else {
- // No app installed to open the link
- context.showToast(R.string.err_no_app)
- }
- }
- }
-
- /**
- * Open an app chooser for a given [Intent].
- *
- * @param intent The [Intent] to show an app chooser for.
- */
- private fun openAppChooser(intent: Intent) {
- logD("Opening app chooser for ${intent.action}")
- val chooserIntent =
- Intent(Intent.ACTION_CHOOSER)
- .putExtra(Intent.EXTRA_INTENT, intent)
- .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
- startActivity(chooserIntent)
- }
-
private companion object {
/** The URL to the source code. */
const val LINK_SOURCE = "https://github.com/OxygenCobalt/Auxio"
diff --git a/app/src/main/java/org/oxycblt/auxio/util/FrameworkUtil.kt b/app/src/main/java/org/oxycblt/auxio/util/FrameworkUtil.kt
index e58a34c93..1662c47c5 100644
--- a/app/src/main/java/org/oxycblt/auxio/util/FrameworkUtil.kt
+++ b/app/src/main/java/org/oxycblt/auxio/util/FrameworkUtil.kt
@@ -18,7 +18,10 @@
package org.oxycblt.auxio.util
+import android.content.ActivityNotFoundException
import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
import android.graphics.PointF
import android.graphics.drawable.Drawable
import android.os.Build
@@ -33,6 +36,7 @@ import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.app.ShareCompat
import androidx.core.graphics.Insets
import androidx.core.graphics.drawable.DrawableCompat
+import androidx.core.net.toUri
import androidx.core.view.children
import androidx.navigation.NavController
import androidx.navigation.NavDirections
@@ -41,6 +45,7 @@ import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding
import com.google.android.material.appbar.MaterialToolbar
import java.lang.IllegalArgumentException
+import org.oxycblt.auxio.R
import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Song
@@ -322,3 +327,65 @@ fun Context.share(songs: Collection) {
builder.setType(mimeTypes.singleOrNull() ?: "audio/*").startChooser()
}
+
+/**
+ * Open the given URI in a web browser.
+ *
+ * @param uri The URL to open.
+ */
+fun Context.openInBrowser(uri: String) {
+ fun openAppChooser(intent: Intent) {
+ logD("Opening app chooser for ${intent.action}")
+ val chooserIntent =
+ Intent(Intent.ACTION_CHOOSER)
+ .putExtra(Intent.EXTRA_INTENT, intent)
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ startActivity(chooserIntent)
+ }
+
+ logD("Opening $uri")
+ val browserIntent =
+ Intent(Intent.ACTION_VIEW, uri.toUri()).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ // Android 11 seems to now handle the app chooser situations on its own now
+ // [along with adding a new permission that breaks the old manual code], so
+ // we just do a typical activity launch.
+ logD("Using API 30+ chooser")
+ try {
+ startActivity(browserIntent)
+ } catch (e: ActivityNotFoundException) {
+ // No app installed to open the link
+ showToast(R.string.err_no_app)
+ }
+ } else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
+ // On older versions of android, opening links from an ACTION_VIEW intent might
+ // not work in all cases, especially when no default app was set. If that is the
+ // case, we will try to manually handle these cases before we try to launch the
+ // browser.
+ logD("Resolving browser activity for chooser")
+ val pkgName =
+ packageManager.resolveActivity(browserIntent, PackageManager.MATCH_DEFAULT_ONLY)?.run {
+ activityInfo.packageName
+ }
+
+ if (pkgName != null) {
+ if (pkgName == "android") {
+ // No default browser [Must open app chooser, may not be supported]
+ logD("No default browser found")
+ openAppChooser(browserIntent)
+ } else logD("Opening browser intent")
+ try {
+ browserIntent.setPackage(pkgName)
+ startActivity(browserIntent)
+ } catch (e: ActivityNotFoundException) {
+ // Not a browser but an app chooser
+ browserIntent.setPackage(null)
+ openAppChooser(browserIntent)
+ }
+ } else {
+ // No app installed to open the link
+ showToast(R.string.err_no_app)
+ }
+ }
+}
diff --git a/app/src/main/res/drawable/ic_copy_24.xml b/app/src/main/res/drawable/ic_copy_24.xml
new file mode 100644
index 000000000..65bb96df5
--- /dev/null
+++ b/app/src/main/res/drawable/ic_copy_24.xml
@@ -0,0 +1,11 @@
+
+
+
+
diff --git a/app/src/main/res/layout-sw600dp/fragment_playback_panel.xml b/app/src/main/res/layout-sw600dp/fragment_playback_panel.xml
index c673dd8ca..b46562fa2 100644
--- a/app/src/main/res/layout-sw600dp/fragment_playback_panel.xml
+++ b/app/src/main/res/layout-sw600dp/fragment_playback_panel.xml
@@ -69,8 +69,8 @@
android:id="@+id/playback_seek_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:layout_marginStart="@dimen/spacing_small"
- android:layout_marginEnd="@dimen/spacing_small"
+ android:layout_marginStart="@dimen/spacing_tiny"
+ android:layout_marginEnd="@dimen/spacing_tiny"
app:layout_constraintBottom_toTopOf="@+id/playback_controls_container"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
diff --git a/app/src/main/res/layout/dialog_error_details.xml b/app/src/main/res/layout/dialog_error_details.xml
new file mode 100644
index 000000000..729c17d0b
--- /dev/null
+++ b/app/src/main/res/layout/dialog_error_details.xml
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml
index 049256481..8eeb42c13 100644
--- a/app/src/main/res/layout/fragment_home.xml
+++ b/app/src/main/res/layout/fragment_home.xml
@@ -108,16 +108,33 @@
+
+
diff --git a/app/src/main/res/navigation/inner.xml b/app/src/main/res/navigation/inner.xml
index d665a90d3..b1f834f95 100644
--- a/app/src/main/res/navigation/inner.xml
+++ b/app/src/main/res/navigation/inner.xml
@@ -81,8 +81,21 @@
+
+
+