1
0
mirror of https://github.com/wulkanowy/wulkanowy.git synced 2025-01-31 15:28:20 +01:00

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 private val dispatchersProvider: DispatchersProvider
) : CoroutineWorker(appContext, workerParameters) { ) : CoroutineWorker(appContext, workerParameters) {
override suspend fun doWork() = withContext(dispatchersProvider.io) { override suspend fun doWork(): Result = withContext(dispatchersProvider.io) {
Timber.i("SyncWorker is starting") Timber.i("SyncWorker is starting")
if (!studentRepository.isCurrentStudentSet()) return@withContext Result.failure() if (!studentRepository.isCurrentStudentSet()) return@withContext Result.failure()
val student = studentRepository.getCurrentStudent() val (student, semester) = try {
val semester = semesterRepository.getCurrentSemester(student, true) 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 -> val exceptions = works.mapNotNull { work ->
try { try {
@ -62,20 +67,7 @@ class SyncWorker @AssistedInject constructor(
} }
} }
} }
val result = when { val result = getResultFromErrors(exceptions)
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()
}
}
if (preferencesRepository.isDebugNotificationEnable) notify(result) if (preferencesRepository.isDebugNotificationEnable) notify(result)
Timber.i("SyncWorker result: $result") Timber.i("SyncWorker result: $result")
@ -83,6 +75,22 @@ class SyncWorker @AssistedInject constructor(
return@withContext result 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) { private fun notify(result: Result) {
notificationManager.notify( notificationManager.notify(
Random.nextInt(Int.MAX_VALUE), Random.nextInt(Int.MAX_VALUE),

View File

@ -1,28 +1,25 @@
package io.github.wulkanowy.ui.base package io.github.wulkanowy.ui.base
import android.app.Dialog
import android.content.ClipData import android.content.ClipData
import android.content.ClipboardManager import android.content.ClipboardManager
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.HorizontalScrollView
import android.widget.Toast import android.widget.Toast
import android.widget.Toast.LENGTH_LONG import android.widget.Toast.LENGTH_LONG
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import androidx.core.os.bundleOf
import androidx.core.view.isGone import androidx.core.view.isGone
import androidx.fragment.app.DialogFragment
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.databinding.DialogErrorBinding import io.github.wulkanowy.databinding.DialogErrorBinding
import io.github.wulkanowy.sdk.exception.FeatureNotAvailableException import io.github.wulkanowy.sdk.exception.FeatureNotAvailableException
import io.github.wulkanowy.sdk.scrapper.exception.FeatureDisabledException import io.github.wulkanowy.sdk.scrapper.exception.FeatureDisabledException
import io.github.wulkanowy.sdk.scrapper.exception.ServiceUnavailableException import io.github.wulkanowy.sdk.scrapper.exception.ServiceUnavailableException
import io.github.wulkanowy.utils.AppInfo import io.github.wulkanowy.utils.*
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 okhttp3.internal.http2.StreamResetException import okhttp3.internal.http2.StreamResetException
import java.io.InterruptedIOException import java.io.InterruptedIOException
import java.net.ConnectException import java.net.ConnectException
@ -31,72 +28,77 @@ import java.net.UnknownHostException
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class ErrorDialog : BaseDialogFragment<DialogErrorBinding>() { class ErrorDialog : DialogFragment() {
private lateinit var error: Throwable
@Inject @Inject
lateinit var appInfo: AppInfo lateinit var appInfo: AppInfo
companion object { companion object {
private const val ARGUMENT_KEY = "Data" private const val ARGUMENT_KEY = "error"
fun newInstance(error: Throwable) = ErrorDialog().apply { fun newInstance(error: Throwable) = ErrorDialog().apply {
arguments = Bundle().apply { putSerializable(ARGUMENT_KEY, error) } arguments = bundleOf(ARGUMENT_KEY to error)
} }
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
super.onCreate(savedInstanceState) val error = requireArguments().getSerializable(ARGUMENT_KEY) as Throwable
setStyle(STYLE_NO_TITLE, 0)
arguments?.run { val binding = DialogErrorBinding.inflate(LayoutInflater.from(context))
error = getSerializable(ARGUMENT_KEY) as Throwable binding.bindErrorDetails(error)
return getAlertDialog(binding, error).apply {
enableReportButtonIfErrorIsReportable(error)
} }
} }
override fun onCreateView( private fun getAlertDialog(binding: DialogErrorBinding, error: Throwable): AlertDialog {
inflater: LayoutInflater, return MaterialAlertDialogBuilder(requireContext()).apply {
container: ViewGroup?, val errorStacktrace = error.stackTraceToString()
savedInstanceState: Bundle? setTitle(R.string.all_details)
) = DialogErrorBinding.inflate(inflater).apply { binding = this }.root setView(binding.root)
setNeutralButton(R.string.about_feedback) { _, _ ->
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 {
openConfirmDialog { openEmailClient(errorStacktrace) } 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) errorDialogHumanizedMessage.text = resources.getString(error)
errorDialogErrorMessage.text = error.localizedMessage errorDialogErrorMessage.text = error.localizedMessage
errorDialogErrorMessage.isGone = error.localizedMessage.isNullOrBlank() errorDialogErrorMessage.isGone = error.localizedMessage.isNullOrBlank()
errorDialogReport.isEnabled = when (error) { errorDialogContent.text = error.stackTraceToString()
is UnknownHostException, .replace(": ${error.localizedMessage}", "")
is InterruptedIOException,
is ConnectException,
is StreamResetException,
is SocketTimeoutException,
is ServiceUnavailableException,
is FeatureDisabledException,
is FeatureNotAvailableException -> false
else -> true
}
} }
} }
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) { private fun openConfirmDialog(callback: () -> Unit) {
AlertDialog.Builder(requireContext()) AlertDialog.Builder(requireContext())
.setTitle(R.string.dialog_error_check_update) .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 itemsLoadedList.find { it.type == DashboardItem.Type.ACCOUNT }?.error != null
val isGeneralError = val isGeneralError =
filteredItems.none { it.error == null } && filteredItems.isNotEmpty() || isAccountItemError 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 = val filteredOriginalLoadedList =
dashboardItemLoadedList.filterNot { it.type == DashboardItem.Type.ACCOUNT } dashboardItemLoadedList.filterNot { it.type == DashboardItem.Type.ACCOUNT }
@ -726,7 +726,7 @@ class DashboardPresenter @Inject constructor(
filteredOriginalLoadedList.none { it.error == null } && filteredOriginalLoadedList.isNotEmpty() || wasAccountItemError filteredOriginalLoadedList.none { it.error == null } && filteredOriginalLoadedList.isNotEmpty() || wasAccountItemError
if (isGeneralError && isItemsLoaded) { if (isGeneralError && isItemsLoaded) {
lastError = Exception(errorMessage) lastError = requireNotNull(firstError)
view?.run { view?.run {
showProgress(false) showProgress(false)

View File

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

View File

@ -59,7 +59,10 @@ class SyncPresenter @Inject constructor(
WorkInfo.State.FAILED -> { WorkInfo.State.FAILED -> {
showError( showError(
syncFailedString, 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") analytics.logEvent("sync_now", "status" to "failed")
} }

View File

@ -7,15 +7,6 @@
android:orientation="vertical" android:orientation="vertical"
tools:context=".ui.base.ErrorDialog"> 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 <TextView
android:id="@+id/errorDialogHumanizedMessage" android:id="@+id/errorDialogHumanizedMessage"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -32,108 +23,31 @@
android:layout_marginBottom="5dp" android:layout_marginBottom="5dp"
android:paddingHorizontal="20dp" android:paddingHorizontal="20dp"
android:paddingTop="10dp" android:paddingTop="10dp"
android:textIsSelectable="true"
android:textColor="?android:textColorSecondary" android:textColor="?android:textColorSecondary"
android:textIsSelectable="true"
tools:text="@tools:sample/lorem" /> tools:text="@tools:sample/lorem" />
<androidx.constraintlayout.widget.ConstraintLayout <androidx.core.widget.NestedScrollView
android:id="@+id/errorDialogNestedScroll"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="200dp"
android:layout_weight="1" android:overScrollMode="ifContentScrolls"
android:orientation="vertical"> android:paddingHorizontal="24dp"
app:layout_constraintTop_toTopOf="parent">
<androidx.core.widget.NestedScrollView <HorizontalScrollView
android:id="@+id/errorDialogNestedScroll" android:id="@+id/errorDialogHorizontalScroll"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" 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">
<HorizontalScrollView <TextView
android:id="@+id/errorDialogHorizontalScroll" android:id="@+id/errorDialogContent"
android:layout_width="match_parent" android:layout_width="wrap_content"
android:layout_height="wrap_content"> android:layout_height="wrap_content"
android:textColor="?android:textColorTertiary"
<TextView android:textIsSelectable="true"
android:id="@+id/errorDialogContent" android:textSize="12sp"
android:layout_width="wrap_content" tools:text="Lorem ipsum\ndolor\nsit\namet" />
android:layout_height="wrap_content" </HorizontalScrollView>
android:gravity="start" </androidx.core.widget.NestedScrollView>
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>
</LinearLayout> </LinearLayout>