fix: Generate useful error messages for all errors (#719)

Previous code was inconsistent about using getServerErrorMessage() and
whether or not the case where getServerErrorMessage() returns null was
handled.

Switch to using getErrorString() which does handle the null case and
always returns a usable string.

Some string resources are rendered temporarily unused by this change.
They will be used again soon, so configure lint to ignore them at the
moment.
This commit is contained in:
Nik Clayton 2024-05-30 19:14:43 +02:00 committed by GitHub
parent 51c4469e85
commit 4520a29c74
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 70 additions and 53 deletions

View File

@ -1481,6 +1481,28 @@
column="13"/> column="13"/>
</issue> </issue>
<issue
id="UnusedResources"
message="The resource `R.string.failed_to_pin` appears to be unused"
errorLine1=" &lt;string name=&quot;failed_to_pin&quot;>Failed to Pin&lt;/string>"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/strings.xml"
line="444"
column="13"/>
</issue>
<issue
id="UnusedResources"
message="The resource `R.string.failed_to_unpin` appears to be unused"
errorLine1=" &lt;string name=&quot;failed_to_unpin&quot;>Failed to Unpin&lt;/string>"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/strings.xml"
line="445"
column="13"/>
</issue>
<issue <issue
id="UnusedResources" id="UnusedResources"
message="The resource `R.string.description_login` appears to be unused" message="The resource `R.string.description_login` appears to be unused"
@ -1591,6 +1613,17 @@
column="13"/> column="13"/>
</issue> </issue>
<issue
id="UnusedResources"
message="The resource `R.string.ui_error_unknown` appears to be unused"
errorLine1=" &lt;string name=&quot;ui_error_unknown&quot;>unknown reason&lt;/string>"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/strings.xml"
line="640"
column="13"/>
</issue>
<issue <issue
id="UnusedResources" id="UnusedResources"
message="The resource `R.string.hint_filter_title` appears to be unused" message="The resource `R.string.hint_filter_title` appears to be unused"

View File

@ -41,6 +41,7 @@ import app.pachli.core.common.extensions.viewBinding
import app.pachli.core.data.model.InstanceInfo.Companion.DEFAULT_MAX_ACCOUNT_FIELDS import app.pachli.core.data.model.InstanceInfo.Companion.DEFAULT_MAX_ACCOUNT_FIELDS
import app.pachli.core.designsystem.R as DR import app.pachli.core.designsystem.R as DR
import app.pachli.core.ui.extensions.await import app.pachli.core.ui.extensions.await
import app.pachli.core.ui.extensions.getErrorString
import app.pachli.databinding.ActivityEditProfileBinding import app.pachli.databinding.ActivityEditProfileBinding
import app.pachli.util.Error import app.pachli.util.Error
import app.pachli.util.Loading import app.pachli.util.Loading
@ -221,7 +222,7 @@ class EditProfileActivity : BaseActivity() {
binding.saveProgressBar.visibility = View.VISIBLE binding.saveProgressBar.visibility = View.VISIBLE
} }
is Error -> { is Error -> {
onSaveFailure(it.errorMessage) onSaveFailure(it.cause?.getErrorString(this))
} }
} }
} }

View File

@ -80,7 +80,6 @@ import app.pachli.core.common.extensions.show
import app.pachli.core.common.extensions.viewBinding import app.pachli.core.common.extensions.viewBinding
import app.pachli.core.common.extensions.visible import app.pachli.core.common.extensions.visible
import app.pachli.core.common.string.mastodonLength import app.pachli.core.common.string.mastodonLength
import app.pachli.core.common.string.unicodeWrap
import app.pachli.core.data.model.InstanceInfo.Companion.DEFAULT_CHARACTER_LIMIT import app.pachli.core.data.model.InstanceInfo.Companion.DEFAULT_CHARACTER_LIMIT
import app.pachli.core.data.model.InstanceInfo.Companion.DEFAULT_MAX_MEDIA_ATTACHMENTS import app.pachli.core.data.model.InstanceInfo.Companion.DEFAULT_MAX_MEDIA_ATTACHMENTS
import app.pachli.core.database.model.AccountEntity import app.pachli.core.database.model.AccountEntity
@ -88,13 +87,13 @@ import app.pachli.core.designsystem.R as DR
import app.pachli.core.navigation.ComposeActivityIntent import app.pachli.core.navigation.ComposeActivityIntent
import app.pachli.core.navigation.ComposeActivityIntent.ComposeOptions import app.pachli.core.navigation.ComposeActivityIntent.ComposeOptions
import app.pachli.core.navigation.ComposeActivityIntent.ComposeOptions.InitialCursorPosition import app.pachli.core.navigation.ComposeActivityIntent.ComposeOptions.InitialCursorPosition
import app.pachli.core.network.extensions.getServerErrorMessage
import app.pachli.core.network.model.Attachment import app.pachli.core.network.model.Attachment
import app.pachli.core.network.model.Emoji import app.pachli.core.network.model.Emoji
import app.pachli.core.network.model.Status import app.pachli.core.network.model.Status
import app.pachli.core.preferences.AppTheme import app.pachli.core.preferences.AppTheme
import app.pachli.core.preferences.PrefKeys import app.pachli.core.preferences.PrefKeys
import app.pachli.core.preferences.SharedPreferencesRepository import app.pachli.core.preferences.SharedPreferencesRepository
import app.pachli.core.ui.extensions.getErrorString
import app.pachli.databinding.ActivityComposeBinding import app.pachli.databinding.ActivityComposeBinding
import app.pachli.util.PickMediaFiles import app.pachli.util.PickMediaFiles
import app.pachli.util.getInitialLanguages import app.pachli.util.getInitialLanguages
@ -513,10 +512,7 @@ class ComposeActivity :
displayTransientMessage(throwable.errorMessage) displayTransientMessage(throwable.errorMessage)
} else { } else {
displayTransientMessage( displayTransientMessage(
getString( getString(R.string.error_media_upload_sending_fmt, throwable.getErrorString(this@ComposeActivity)),
R.string.error_media_upload_sending_fmt,
throwable.getServerErrorMessage().unicodeWrap(),
),
) )
} }
} }

View File

@ -31,8 +31,8 @@ import app.pachli.R
import app.pachli.components.compose.ComposeActivity.QueuedMedia import app.pachli.components.compose.ComposeActivity.QueuedMedia
import app.pachli.core.common.string.randomAlphanumericString import app.pachli.core.common.string.randomAlphanumericString
import app.pachli.core.data.model.InstanceInfo import app.pachli.core.data.model.InstanceInfo
import app.pachli.core.network.extensions.getServerErrorMessage
import app.pachli.core.network.model.MediaUploadApi import app.pachli.core.network.model.MediaUploadApi
import app.pachli.core.ui.extensions.getErrorString
import app.pachli.network.ProgressRequestBody import app.pachli.network.ProgressRequestBody
import app.pachli.util.MEDIA_SIZE_UNKNOWN import app.pachli.util.MEDIA_SIZE_UNKNOWN
import app.pachli.util.getImageSquarePixels import app.pachli.util.getImageSquarePixels
@ -313,12 +313,7 @@ class MediaUploader @Inject constructor(
send(UploadEvent.FinishedEvent(responseBody.id, uploadResponse.code() == 200)) send(UploadEvent.FinishedEvent(responseBody.id, uploadResponse.code() == 200))
} else { } else {
val error = HttpException(uploadResponse) val error = HttpException(uploadResponse)
val errorMessage = error.getServerErrorMessage() throw UploadServerError(error.getErrorString(context))
if (errorMessage == null) {
throw error
} else {
throw UploadServerError(errorMessage)
}
} }
awaitClose() awaitClose()

View File

@ -52,15 +52,14 @@ import app.pachli.core.activity.openLink
import app.pachli.core.common.extensions.hide import app.pachli.core.common.extensions.hide
import app.pachli.core.common.extensions.show import app.pachli.core.common.extensions.show
import app.pachli.core.common.extensions.viewBinding import app.pachli.core.common.extensions.viewBinding
import app.pachli.core.common.string.unicodeWrap
import app.pachli.core.navigation.AttachmentViewData.Companion.list import app.pachli.core.navigation.AttachmentViewData.Companion.list
import app.pachli.core.network.extensions.getServerErrorMessage
import app.pachli.core.network.model.Filter import app.pachli.core.network.model.Filter
import app.pachli.core.network.model.Notification import app.pachli.core.network.model.Notification
import app.pachli.core.network.model.Poll import app.pachli.core.network.model.Poll
import app.pachli.core.network.model.Status import app.pachli.core.network.model.Status
import app.pachli.core.ui.ActionButtonScrollListener import app.pachli.core.ui.ActionButtonScrollListener
import app.pachli.core.ui.BackgroundMessage import app.pachli.core.ui.BackgroundMessage
import app.pachli.core.ui.extensions.getErrorString
import app.pachli.databinding.FragmentTimelineNotificationsBinding import app.pachli.databinding.FragmentTimelineNotificationsBinding
import app.pachli.fragment.SFragment import app.pachli.fragment.SFragment
import app.pachli.interfaces.AccountActionListener import app.pachli.interfaces.AccountActionListener
@ -235,10 +234,7 @@ class NotificationsFragment :
viewModel.uiError.collect { error -> viewModel.uiError.collect { error ->
val message = getString( val message = getString(
error.message, error.message,
( error.throwable.getErrorString(requireContext()),
error.throwable.getServerErrorMessage() ?: error.throwable.localizedMessage
?: getString(R.string.ui_error_unknown)
).unicodeWrap(),
) )
Timber.d(error.throwable, message) Timber.d(error.throwable, message)
val snackbar = Snackbar.make( val snackbar = Snackbar.make(

View File

@ -54,12 +54,10 @@ import app.pachli.core.activity.extensions.startActivityWithDefaultTransition
import app.pachli.core.common.extensions.hide import app.pachli.core.common.extensions.hide
import app.pachli.core.common.extensions.show import app.pachli.core.common.extensions.show
import app.pachli.core.common.extensions.viewBinding import app.pachli.core.common.extensions.viewBinding
import app.pachli.core.common.string.unicodeWrap
import app.pachli.core.database.model.TranslationState import app.pachli.core.database.model.TranslationState
import app.pachli.core.model.Timeline import app.pachli.core.model.Timeline
import app.pachli.core.navigation.AccountListActivityIntent import app.pachli.core.navigation.AccountListActivityIntent
import app.pachli.core.navigation.AttachmentViewData import app.pachli.core.navigation.AttachmentViewData
import app.pachli.core.network.extensions.getServerErrorMessage
import app.pachli.core.network.model.Poll import app.pachli.core.network.model.Poll
import app.pachli.core.network.model.Status import app.pachli.core.network.model.Status
import app.pachli.core.ui.ActionButtonScrollListener import app.pachli.core.ui.ActionButtonScrollListener
@ -237,10 +235,7 @@ class TimelineFragment :
viewModel.uiError.collect { error -> viewModel.uiError.collect { error ->
val message = getString( val message = getString(
error.message, error.message,
( error.throwable.getErrorString(requireContext()),
error.throwable.getServerErrorMessage() ?: error.throwable.localizedMessage
?: getString(R.string.ui_error_unknown)
).unicodeWrap(),
) )
Timber.d(error.throwable, message) Timber.d(error.throwable, message)
snackbar = Snackbar.make( snackbar = Snackbar.make(

View File

@ -38,13 +38,12 @@ import app.pachli.core.activity.openLink
import app.pachli.core.common.extensions.hide import app.pachli.core.common.extensions.hide
import app.pachli.core.common.extensions.show import app.pachli.core.common.extensions.show
import app.pachli.core.common.extensions.viewBinding import app.pachli.core.common.extensions.viewBinding
import app.pachli.core.common.string.unicodeWrap
import app.pachli.core.designsystem.R as DR import app.pachli.core.designsystem.R as DR
import app.pachli.core.navigation.AccountListActivityIntent import app.pachli.core.navigation.AccountListActivityIntent
import app.pachli.core.navigation.AttachmentViewData.Companion.list import app.pachli.core.navigation.AttachmentViewData.Companion.list
import app.pachli.core.network.extensions.getServerErrorMessage
import app.pachli.core.network.model.Poll import app.pachli.core.network.model.Poll
import app.pachli.core.network.model.Status import app.pachli.core.network.model.Status
import app.pachli.core.ui.extensions.getErrorString
import app.pachli.databinding.FragmentViewThreadBinding import app.pachli.databinding.FragmentViewThreadBinding
import app.pachli.fragment.SFragment import app.pachli.fragment.SFragment
import app.pachli.interfaces.StatusActionListener import app.pachli.interfaces.StatusActionListener
@ -211,10 +210,7 @@ class ViewThreadFragment :
lifecycleScope.launch { lifecycleScope.launch {
viewModel.errors.collect { throwable -> viewModel.errors.collect { throwable ->
Timber.w(throwable, "failed to load status context") Timber.w(throwable, "failed to load status context")
val msg = view.context.getString( val msg = throwable.getErrorString(view.context)
app.pachli.core.ui.R.string.error_generic_fmt,
throwable.getServerErrorMessage().unicodeWrap(),
)
Snackbar.make(binding.root, msg, Snackbar.LENGTH_INDEFINITE) Snackbar.make(binding.root, msg, Snackbar.LENGTH_INDEFINITE)
.setAction(app.pachli.core.ui.R.string.action_retry) { .setAction(app.pachli.core.ui.R.string.action_retry) {
viewModel.retry(thisThreadsStatusId) viewModel.retry(thisThreadsStatusId)

View File

@ -60,6 +60,7 @@ import app.pachli.core.network.model.Attachment
import app.pachli.core.network.model.Status import app.pachli.core.network.model.Status
import app.pachli.core.network.parseAsMastodonHtml import app.pachli.core.network.parseAsMastodonHtml
import app.pachli.core.network.retrofit.MastodonApi import app.pachli.core.network.retrofit.MastodonApi
import app.pachli.core.ui.extensions.getErrorString
import app.pachli.interfaces.StatusActionListener import app.pachli.interfaces.StatusActionListener
import app.pachli.network.ServerRepository import app.pachli.network.ServerRepository
import app.pachli.usecase.TimelineCases import app.pachli.usecase.TimelineCases
@ -339,8 +340,7 @@ abstract class SFragment<T : IStatusViewData> : Fragment(), StatusActionListener
R.id.pin -> { R.id.pin -> {
lifecycleScope.launch { lifecycleScope.launch {
timelineCases.pin(status.id, !status.isPinned()).onFailure { e: Throwable -> timelineCases.pin(status.id, !status.isPinned()).onFailure { e: Throwable ->
val message = e.message val message = e.getErrorString(requireContext())
?: getString(if (status.isPinned()) R.string.failed_to_unpin else R.string.failed_to_pin)
Snackbar.make(requireView(), message, Snackbar.LENGTH_LONG).show() Snackbar.make(requireView(), message, Snackbar.LENGTH_LONG).show()
} }
} }

View File

@ -27,7 +27,6 @@ import app.pachli.appstore.PollVoteEvent
import app.pachli.appstore.ReblogEvent import app.pachli.appstore.ReblogEvent
import app.pachli.appstore.StatusDeletedEvent import app.pachli.appstore.StatusDeletedEvent
import app.pachli.components.timeline.CachedTimelineRepository import app.pachli.components.timeline.CachedTimelineRepository
import app.pachli.core.network.extensions.getServerErrorMessage
import app.pachli.core.network.model.DeletedStatus import app.pachli.core.network.model.DeletedStatus
import app.pachli.core.network.model.Poll import app.pachli.core.network.model.Poll
import app.pachli.core.network.model.Relationship import app.pachli.core.network.model.Relationship
@ -122,7 +121,7 @@ class TimelineCases @Inject constructor(
NetworkResult.success(status) NetworkResult.success(status)
}, { e -> }, { e ->
Timber.w(e, "Failed to change pin state") Timber.w(e, "Failed to change pin state")
NetworkResult.failure(TimelineError(e.getServerErrorMessage())) NetworkResult.failure(e)
}) })
} }
@ -152,5 +151,3 @@ class TimelineCases @Inject constructor(
cachedTimelineRepository.translateUndo(statusViewData) cachedTimelineRepository.translateUndo(statusViewData)
} }
} }
class TimelineError(message: String?) : RuntimeException(message)

View File

@ -26,7 +26,6 @@ import app.pachli.appstore.EventHub
import app.pachli.appstore.ProfileEditedEvent import app.pachli.appstore.ProfileEditedEvent
import app.pachli.core.common.string.randomAlphanumericString import app.pachli.core.common.string.randomAlphanumericString
import app.pachli.core.data.repository.InstanceInfoRepository import app.pachli.core.data.repository.InstanceInfoRepository
import app.pachli.core.network.extensions.getServerErrorMessage
import app.pachli.core.network.model.Account import app.pachli.core.network.model.Account
import app.pachli.core.network.model.StringField import app.pachli.core.network.model.StringField
import app.pachli.core.network.retrofit.MastodonApi import app.pachli.core.network.retrofit.MastodonApi
@ -151,7 +150,7 @@ class EditProfileViewModel @Inject constructor(
eventHub.dispatch(ProfileEditedEvent(newAccountData)) eventHub.dispatch(ProfileEditedEvent(newAccountData))
}, },
{ throwable -> { throwable ->
saveData.postValue(Error(errorMessage = throwable.getServerErrorMessage())) saveData.postValue(Error(cause = throwable))
}, },
) )
} }

View File

@ -5,6 +5,7 @@ import app.cash.turbine.test
import app.pachli.appstore.EventHub import app.pachli.appstore.EventHub
import app.pachli.appstore.PinEvent import app.pachli.appstore.PinEvent
import app.pachli.components.timeline.CachedTimelineRepository import app.pachli.components.timeline.CachedTimelineRepository
import app.pachli.core.network.extensions.getServerErrorMessage
import app.pachli.core.network.model.Status import app.pachli.core.network.model.Status
import app.pachli.core.network.retrofit.MastodonApi import app.pachli.core.network.retrofit.MastodonApi
import at.connyduck.calladapter.networkresult.NetworkResult import at.connyduck.calladapter.networkresult.NetworkResult
@ -54,7 +55,7 @@ class TimelineCasesTest {
} }
@Test @Test
fun `pin failure with server error throws TimelineError with server message`() { fun `pin failure with server error returns failure with server message`() {
api.stub { api.stub {
onBlocking { pinStatus(statusId) } doReturn NetworkResult.failure( onBlocking { pinStatus(statusId) } doReturn NetworkResult.failure(
HttpException( HttpException(
@ -68,7 +69,7 @@ class TimelineCasesTest {
runBlocking { runBlocking {
assertEquals( assertEquals(
"Validation Failed: You have already pinned the maximum number of toots", "Validation Failed: You have already pinned the maximum number of toots",
timelineCases.pin(statusId, true).exceptionOrNull()?.message, timelineCases.pin(statusId, true).exceptionOrNull()?.getServerErrorMessage(),
) )
} }
} }

View File

@ -29,7 +29,7 @@ import retrofit2.HttpException
fun Throwable.getDrawableRes(): Int = when (this) { fun Throwable.getDrawableRes(): Int = when (this) {
is IOException -> R.drawable.errorphant_offline is IOException -> R.drawable.errorphant_offline
is HttpException -> { is HttpException -> {
if (this.code() == 404) { if (code() == 404) {
R.drawable.elephant_friend_empty R.drawable.elephant_friend_empty
} else { } else {
R.drawable.errorphant_offline R.drawable.errorphant_offline
@ -38,10 +38,18 @@ fun Throwable.getDrawableRes(): Int = when (this) {
else -> R.drawable.errorphant_error else -> R.drawable.errorphant_error
} }
/** @return A string error message for this throwable */ /** @return A unicode-wrapped string error message for this throwable */
fun Throwable.getErrorString(context: Context): String = getServerErrorMessage() ?: when (this) { fun Throwable.getErrorString(context: Context): String = (
is IOException -> String.format(context.getString(R.string.error_network_fmt), this.localizedMessage.unicodeWrap()) getServerErrorMessage() ?: when (this) {
is HttpException -> if (this.code() == 404) String.format(context.getString(R.string.error_404_not_found_fmt), this.message.unicodeWrap()) else String.format(context.getString(R.string.error_generic_fmt), this.message.unicodeWrap()) is IOException -> String.format(context.getString(R.string.error_network_fmt), localizedMessage)
is JsonDataException -> String.format(context.getString(R.string.error_json_data_fmt), this.message.unicodeWrap()) is HttpException -> {
else -> String.format(context.getString(R.string.error_generic_fmt), this.message.unicodeWrap()) if (code() == 404) {
} String.format(context.getString(R.string.error_404_not_found_fmt), localizedMessage)
} else {
String.format(context.getString(R.string.error_generic_fmt), localizedMessage)
}
}
is JsonDataException -> String.format(context.getString(R.string.error_json_data_fmt), localizedMessage)
else -> String.format(context.getString(R.string.error_generic_fmt), localizedMessage)
}
).unicodeWrap()

View File

@ -37,6 +37,7 @@ dependencies {
implementation(projects.core.navigation) implementation(projects.core.navigation)
implementation(projects.core.network) implementation(projects.core.network)
implementation(projects.core.preferences) implementation(projects.core.preferences)
implementation(projects.core.ui)
implementation(libs.bundles.androidx) implementation(libs.bundles.androidx)
implementation(libs.androidx.webkit) implementation(libs.androidx.webkit)

View File

@ -34,13 +34,12 @@ import app.pachli.core.activity.extensions.setCloseTransition
import app.pachli.core.activity.extensions.startActivityWithTransition import app.pachli.core.activity.extensions.startActivityWithTransition
import app.pachli.core.activity.openLinkInCustomTab import app.pachli.core.activity.openLinkInCustomTab
import app.pachli.core.common.extensions.viewBinding import app.pachli.core.common.extensions.viewBinding
import app.pachli.core.common.string.unicodeWrap
import app.pachli.core.navigation.LoginActivityIntent import app.pachli.core.navigation.LoginActivityIntent
import app.pachli.core.navigation.MainActivityIntent import app.pachli.core.navigation.MainActivityIntent
import app.pachli.core.network.extensions.getServerErrorMessage
import app.pachli.core.network.model.AccessToken import app.pachli.core.network.model.AccessToken
import app.pachli.core.network.retrofit.MastodonApi import app.pachli.core.network.retrofit.MastodonApi
import app.pachli.core.preferences.getNonNullString import app.pachli.core.preferences.getNonNullString
import app.pachli.core.ui.extensions.getErrorString
import app.pachli.feature.login.databinding.ActivityLoginBinding import app.pachli.feature.login.databinding.ActivityLoginBinding
import at.connyduck.calladapter.networkresult.fold import at.connyduck.calladapter.networkresult.fold
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
@ -196,7 +195,7 @@ class LoginActivity : BaseActivity() {
binding.domainTextInputLayout.error = binding.domainTextInputLayout.error =
String.format( String.format(
getString(R.string.error_failed_app_registration_fmt), getString(R.string.error_failed_app_registration_fmt),
(e.getServerErrorMessage() ?: e.localizedMessage).unicodeWrap(), e.getErrorString(this@LoginActivity),
) )
setLoading(false) setLoading(false)
Timber.e(e, "Error when creating/registing app") Timber.e(e, "Error when creating/registing app")