Fix overlapping text in the error dialog (#1708)

This commit is contained in:
Michael 2021-12-28 12:16:52 +01:00 committed by GitHub
parent 17096ad11b
commit 5e96917508
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 111 additions and 180 deletions

View File

@ -38,13 +38,18 @@ class SyncWorker @AssistedInject constructor(
private val dispatchersProvider: DispatchersProvider
) : CoroutineWorker(appContext, workerParameters) {
override suspend fun doWork() = withContext(dispatchersProvider.io) {
override suspend fun doWork(): Result = withContext(dispatchersProvider.io) {
Timber.i("SyncWorker is starting")
if (!studentRepository.isCurrentStudentSet()) return@withContext Result.failure()
val student = studentRepository.getCurrentStudent()
val semester = semesterRepository.getCurrentSemester(student, true)
val (student, semester) = try {
val student = studentRepository.getCurrentStudent()
val semester = semesterRepository.getCurrentSemester(student, true)
student to semester
} catch (e: Throwable) {
return@withContext getResultFromErrors(listOf(e))
}
val exceptions = works.mapNotNull { work ->
try {
@ -62,20 +67,7 @@ class SyncWorker @AssistedInject constructor(
}
}
}
val result = when {
exceptions.isNotEmpty() && inputData.getBoolean("one_time", false) -> {
Result.failure(
Data.Builder()
.putString("error", exceptions.map { it.stackTraceToString() }.toString())
.build()
)
}
exceptions.isNotEmpty() -> Result.retry()
else -> {
preferencesRepository.lasSyncDate = LocalDateTime.now()
Result.success()
}
}
val result = getResultFromErrors(exceptions)
if (preferencesRepository.isDebugNotificationEnable) notify(result)
Timber.i("SyncWorker result: $result")
@ -83,6 +75,22 @@ class SyncWorker @AssistedInject constructor(
return@withContext result
}
private fun getResultFromErrors(errors: List<Throwable>): Result = when {
errors.isNotEmpty() && inputData.getBoolean("one_time", false) -> {
Result.failure(
Data.Builder()
.putString("error_message", errors.joinToString { it.message.toString() })
.putString("error_stack", errors.map { it.stackTraceToString() }.toString())
.build()
)
}
errors.isNotEmpty() -> Result.retry()
else -> {
preferencesRepository.lasSyncDate = LocalDateTime.now()
Result.success()
}
}
private fun notify(result: Result) {
notificationManager.notify(
Random.nextInt(Int.MAX_VALUE),

View File

@ -1,28 +1,25 @@
package io.github.wulkanowy.ui.base
import android.app.Dialog
import android.content.ClipData
import android.content.ClipboardManager
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.HorizontalScrollView
import android.widget.Toast
import android.widget.Toast.LENGTH_LONG
import androidx.appcompat.app.AlertDialog
import androidx.core.content.getSystemService
import androidx.core.os.bundleOf
import androidx.core.view.isGone
import androidx.fragment.app.DialogFragment
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
import io.github.wulkanowy.databinding.DialogErrorBinding
import io.github.wulkanowy.sdk.exception.FeatureNotAvailableException
import io.github.wulkanowy.sdk.scrapper.exception.FeatureDisabledException
import io.github.wulkanowy.sdk.scrapper.exception.ServiceUnavailableException
import io.github.wulkanowy.utils.AppInfo
import io.github.wulkanowy.utils.getString
import io.github.wulkanowy.utils.openAppInMarket
import io.github.wulkanowy.utils.openEmailClient
import io.github.wulkanowy.utils.openInternetBrowser
import io.github.wulkanowy.utils.*
import okhttp3.internal.http2.StreamResetException
import java.io.InterruptedIOException
import java.net.ConnectException
@ -31,72 +28,77 @@ import java.net.UnknownHostException
import javax.inject.Inject
@AndroidEntryPoint
class ErrorDialog : BaseDialogFragment<DialogErrorBinding>() {
private lateinit var error: Throwable
class ErrorDialog : DialogFragment() {
@Inject
lateinit var appInfo: AppInfo
companion object {
private const val ARGUMENT_KEY = "Data"
private const val ARGUMENT_KEY = "error"
fun newInstance(error: Throwable) = ErrorDialog().apply {
arguments = Bundle().apply { putSerializable(ARGUMENT_KEY, error) }
arguments = bundleOf(ARGUMENT_KEY to error)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStyle(STYLE_NO_TITLE, 0)
arguments?.run {
error = getSerializable(ARGUMENT_KEY) as Throwable
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val error = requireArguments().getSerializable(ARGUMENT_KEY) as Throwable
val binding = DialogErrorBinding.inflate(LayoutInflater.from(context))
binding.bindErrorDetails(error)
return getAlertDialog(binding, error).apply {
enableReportButtonIfErrorIsReportable(error)
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
) = DialogErrorBinding.inflate(inflater).apply { binding = this }.root
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val errorStacktrace = error.stackTraceToString()
with(binding) {
errorDialogContent.text = errorStacktrace.replace(": ${error.localizedMessage}", "")
with(errorDialogHorizontalScroll) {
post { fullScroll(HorizontalScrollView.FOCUS_LEFT) }
}
errorDialogCopy.setOnClickListener {
val clip = ClipData.newPlainText("Error details", errorStacktrace)
activity?.getSystemService<ClipboardManager>()?.setPrimaryClip(clip)
Toast.makeText(context, R.string.all_copied, LENGTH_LONG).show()
}
errorDialogCancel.setOnClickListener { dismiss() }
errorDialogReport.setOnClickListener {
private fun getAlertDialog(binding: DialogErrorBinding, error: Throwable): AlertDialog {
return MaterialAlertDialogBuilder(requireContext()).apply {
val errorStacktrace = error.stackTraceToString()
setTitle(R.string.all_details)
setView(binding.root)
setNeutralButton(R.string.about_feedback) { _, _ ->
openConfirmDialog { openEmailClient(errorStacktrace) }
}
setNegativeButton(android.R.string.cancel) { _, _ -> }
setPositiveButton(android.R.string.copy) { _, _ -> copyErrorToClipboard(errorStacktrace) }
}.create()
}
private fun DialogErrorBinding.bindErrorDetails(error: Throwable) {
return with(this) {
errorDialogHumanizedMessage.text = resources.getString(error)
errorDialogErrorMessage.text = error.localizedMessage
errorDialogErrorMessage.isGone = error.localizedMessage.isNullOrBlank()
errorDialogReport.isEnabled = when (error) {
is UnknownHostException,
is InterruptedIOException,
is ConnectException,
is StreamResetException,
is SocketTimeoutException,
is ServiceUnavailableException,
is FeatureDisabledException,
is FeatureNotAvailableException -> false
else -> true
}
errorDialogContent.text = error.stackTraceToString()
.replace(": ${error.localizedMessage}", "")
}
}
private fun AlertDialog.enableReportButtonIfErrorIsReportable(error: Throwable) {
setOnShowListener {
getButton(AlertDialog.BUTTON_NEUTRAL).isEnabled = isErrorShouldBeReported(error)
}
}
private fun isErrorShouldBeReported(error: Throwable): Boolean = when (error) {
is UnknownHostException,
is InterruptedIOException,
is ConnectException,
is StreamResetException,
is SocketTimeoutException,
is ServiceUnavailableException,
is FeatureDisabledException,
is FeatureNotAvailableException -> false
else -> true
}
private fun copyErrorToClipboard(errorStacktrace: String) {
val clip = ClipData.newPlainText("Error details", errorStacktrace)
requireActivity().getSystemService<ClipboardManager>()?.setPrimaryClip(clip)
Toast.makeText(requireContext(), R.string.all_copied, LENGTH_LONG).show()
}
private fun openConfirmDialog(callback: () -> Unit) {
AlertDialog.Builder(requireContext())
.setTitle(R.string.dialog_error_check_update)
@ -127,4 +129,8 @@ class ErrorDialog : BaseDialogFragment<DialogErrorBinding>() {
}
)
}
private fun showMessage(text: String) {
Toast.makeText(requireContext(), text, LENGTH_LONG).show()
}
}

View File

@ -716,7 +716,7 @@ class DashboardPresenter @Inject constructor(
itemsLoadedList.find { it.type == DashboardItem.Type.ACCOUNT }?.error != null
val isGeneralError =
filteredItems.none { it.error == null } && filteredItems.isNotEmpty() || isAccountItemError
val errorMessage = itemsLoadedList.map { it.error?.stackTraceToString() }.toString()
val firstError = itemsLoadedList.mapNotNull { it.error }.firstOrNull()
val filteredOriginalLoadedList =
dashboardItemLoadedList.filterNot { it.type == DashboardItem.Type.ACCOUNT }
@ -726,7 +726,7 @@ class DashboardPresenter @Inject constructor(
filteredOriginalLoadedList.none { it.error == null } && filteredOriginalLoadedList.isNotEmpty() || wasAccountItemError
if (isGeneralError && isItemsLoaded) {
lastError = Exception(errorMessage)
lastError = requireNotNull(firstError)
view?.run {
showProgress(false)

View File

@ -91,7 +91,7 @@ class SyncFragment : PreferenceFragmentCompat(),
}
override fun showErrorDetailsDialog(error: Throwable) {
ErrorDialog.newInstance(error).show(childFragmentManager, error.toString())
ErrorDialog.newInstance(error).show(childFragmentManager, "error_details")
}
override fun onResume() {

View File

@ -59,7 +59,10 @@ class SyncPresenter @Inject constructor(
WorkInfo.State.FAILED -> {
showError(
syncFailedString,
Throwable(workInfo.outputData.getString("error"))
Throwable(
message = workInfo.outputData.getString("error_message"),
cause = Throwable(workInfo.outputData.getString("error_stack"))
)
)
analytics.logEvent("sync_now", "status" to "failed")
}

View File

@ -7,15 +7,6 @@
android:orientation="vertical"
tools:context=".ui.base.ErrorDialog">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="20dp"
android:paddingTop="24dp"
android:text="@string/all_details"
android:textSize="21sp"
android:textStyle="bold" />
<TextView
android:id="@+id/errorDialogHumanizedMessage"
android:layout_width="match_parent"
@ -32,108 +23,31 @@
android:layout_marginBottom="5dp"
android:paddingHorizontal="20dp"
android:paddingTop="10dp"
android:textIsSelectable="true"
android:textColor="?android:textColorSecondary"
android:textIsSelectable="true"
tools:text="@tools:sample/lorem" />
<androidx.constraintlayout.widget.ConstraintLayout
<androidx.core.widget.NestedScrollView
android:id="@+id/errorDialogNestedScroll"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:orientation="vertical">
android:layout_height="200dp"
android:overScrollMode="ifContentScrolls"
android:paddingHorizontal="24dp"
app:layout_constraintTop_toTopOf="parent">
<androidx.core.widget.NestedScrollView
android:id="@+id/errorDialogNestedScroll"
<HorizontalScrollView
android:id="@+id/errorDialogHorizontalScroll"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:overScrollMode="ifContentScrolls"
android:paddingHorizontal="24dp"
app:layout_constrainedHeight="true"
app:layout_constraintHeight_max="300dp"
app:layout_constraintHeight_min="200dp"
app:layout_constraintTop_toTopOf="parent">
android:layout_height="wrap_content">
<HorizontalScrollView
android:id="@+id/errorDialogHorizontalScroll"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/errorDialogContent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="start"
android:textColor="?android:textColorTertiary"
android:textIsSelectable="true"
android:textSize="12sp"
tools:text="@tools:sample/lorem/random" />
</HorizontalScrollView>
</androidx.core.widget.NestedScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@drawable/ic_all_divider" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="end"
android:minHeight="52dp"
android:orientation="horizontal">
<com.google.android.material.button.MaterialButton
android:id="@+id/errorDialogReport"
style="@style/Widget.MaterialComponents.Button.TextButton.Dialog"
android:layout_width="wrap_content"
android:layout_height="36dp"
android:layout_gravity="center_vertical"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
android:insetLeft="0dp"
android:insetTop="0dp"
android:insetRight="0dp"
android:insetBottom="0dp"
android:minWidth="88dp"
android:text="@string/about_feedback" />
<Space
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" />
<com.google.android.material.button.MaterialButton
android:id="@+id/errorDialogCancel"
style="@style/Widget.MaterialComponents.Button.TextButton.Dialog"
android:layout_width="wrap_content"
android:layout_height="36dp"
android:layout_gravity="center_vertical"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
android:insetLeft="0dp"
android:insetTop="0dp"
android:insetRight="0dp"
android:insetBottom="0dp"
android:minWidth="88dp"
android:text="@android:string/cancel" />
<com.google.android.material.button.MaterialButton
android:id="@+id/errorDialogCopy"
style="@style/Widget.MaterialComponents.Button.TextButton.Dialog"
android:layout_width="wrap_content"
android:layout_height="36dp"
android:layout_gravity="center_vertical"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
android:insetLeft="0dp"
android:insetTop="0dp"
android:insetRight="0dp"
android:insetBottom="0dp"
android:minWidth="88dp"
android:text="@android:string/copy" />
</LinearLayout>
<TextView
android:id="@+id/errorDialogContent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="?android:textColorTertiary"
android:textIsSelectable="true"
android:textSize="12sp"
tools:text="Lorem ipsum\ndolor\nsit\namet" />
</HorizontalScrollView>
</androidx.core.widget.NestedScrollView>
</LinearLayout>