From f5c7f25cdf5b235e1899d959f28a950fddd1ce02 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Mon, 14 Aug 2023 17:42:05 -0600 Subject: [PATCH] home: add music loading error dialog Add a dialog that shows the stack trace of a music loading error. This is an MVP that is only available to music loading to resolve some immediate issues. Resolves #527. --- .../oxycblt/auxio/home/ErrorDetailsDialog.kt | 89 +++++++++++++++++++ .../org/oxycblt/auxio/home/HomeFragment.kt | 11 ++- .../java/org/oxycblt/auxio/music/Indexing.kt | 2 +- .../oxycblt/auxio/music/MusicRepository.kt | 1 + .../oxycblt/auxio/settings/AboutFragment.kt | 84 ++--------------- .../org/oxycblt/auxio/util/FrameworkUtil.kt | 67 ++++++++++++++ app/src/main/res/drawable/ic_copy_24.xml | 11 +++ .../fragment_playback_panel.xml | 4 +- .../main/res/layout/dialog_error_details.xml | 67 ++++++++++++++ app/src/main/res/layout/fragment_home.xml | 21 ++++- app/src/main/res/navigation/inner.xml | 13 +++ app/src/main/res/values/dimens.xml | 1 + app/src/main/res/values/strings.xml | 8 ++ 13 files changed, 294 insertions(+), 85 deletions(-) create mode 100644 app/src/main/java/org/oxycblt/auxio/home/ErrorDetailsDialog.kt create mode 100644 app/src/main/res/drawable/ic_copy_24.xml create mode 100644 app/src/main/res/layout/dialog_error_details.xml 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 @@ + + + + + 48dp 56dp 64dp + 64dp 72dp 24dp diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fd9f2c2e7..852e9eeae 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -16,6 +16,8 @@ Monitoring music library Retry + + More Grant @@ -168,6 +170,12 @@ Selection + Error information + + Copied + + Report +