feat: Allow the user to send an error report without a crash (#406)

Getting error reports with logs of strange behaviour is useful even if
the app doesn't crash.

Move crash reporting in to `core.activity`, and provide a menu option
(in orange builds) to trigger a non-fatal crash report that is handled
the same way (i.e., sent by e-mail) as a regular crash report.

`BaseActivity` has to be able to create and handle menus, so adjust
subclasses to call the superclass when necessary.

Update `tools/mvstring` to be able to move strings between different
flavour directories, not just `main`.
This commit is contained in:
Nik Clayton 2024-02-02 15:34:31 +01:00 committed by GitHub
parent 86d800b1c8
commit 1488c13c42
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 162 additions and 71 deletions

View File

@ -177,8 +177,6 @@ dependencies {
debugImplementation(libs.leakcanary)
orangeImplementation(libs.bundles.acra)
testImplementation(projects.core.testing)
testImplementation(libs.androidx.test.junit)
testImplementation(libs.robolectric)

View File

@ -382,6 +382,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
}
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
super.onCreateMenu(menu, menuInflater)
menuInflater.inflate(R.menu.activity_main, menu)
menu.findItem(R.id.action_search)?.apply {
icon = IconicsDrawable(this@MainActivity, GoogleMaterial.Icon.gmd_search).apply {
@ -392,20 +393,21 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
}
override fun onPrepareMenu(menu: Menu) {
super.onPrepareMenu(menu)
super<BottomSheetActivity>.onPrepareMenu(menu)
// If the main toolbar is hidden then there's no space in the top/bottomNav to show
// the menu items as icons, so forceably disable them
if (!binding.mainToolbar.isVisible) menu.forEach { it.setShowAsAction(SHOW_AS_ACTION_NEVER) }
}
override fun onMenuItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
super.onMenuItemSelected(menuItem)
return when (menuItem.itemId) {
R.id.action_search -> {
startActivity(SearchActivityIntent(this@MainActivity))
true
}
else -> super.onOptionsItemSelected(item)
else -> super.onOptionsItemSelected(menuItem)
}
}

View File

@ -25,6 +25,7 @@ import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import app.pachli.components.notifications.createWorkerNotificationChannel
import app.pachli.core.activity.initCrashReporter
import app.pachli.core.preferences.AppTheme
import app.pachli.core.preferences.NEW_INSTALL_SCHEMA_VERSION
import app.pachli.core.preferences.PrefKeys

View File

@ -805,6 +805,7 @@ class AccountActivity :
}
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
super.onCreateMenu(menu, menuInflater)
menuInflater.inflate(R.menu.account_toolbar, menu)
val openAsItem = menu.findItem(R.id.action_open_as)
@ -964,6 +965,7 @@ class AccountActivity :
}
override fun onMenuItemSelected(item: MenuItem): Boolean {
super.onMenuItemSelected(item)
when (item.itemId) {
R.id.action_open_in_web -> {
// If the account isn't loaded yet, eat the input.

View File

@ -139,6 +139,7 @@ class AnnouncementsActivity :
}
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
super.onCreateMenu(menu, menuInflater)
menuInflater.inflate(R.menu.activity_announcements, menu)
menu.findItem(R.id.action_search)?.apply {
icon = IconicsDrawable(this@AnnouncementsActivity, GoogleMaterial.Icon.gmd_search).apply {
@ -149,6 +150,7 @@ class AnnouncementsActivity :
}
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
super.onMenuItemSelected(menuItem)
return when (menuItem.itemId) {
R.id.action_refresh -> {
binding.swipeRefreshLayout.isRefreshing = true

View File

@ -125,6 +125,7 @@ class ScheduledStatusActivity :
}
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
super.onCreateMenu(menu, menuInflater)
menuInflater.inflate(R.menu.activity_announcements, menu)
menu.findItem(R.id.action_search)?.apply {
icon = IconicsDrawable(this@ScheduledStatusActivity, GoogleMaterial.Icon.gmd_search).apply {
@ -135,6 +136,7 @@ class ScheduledStatusActivity :
}
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
super.onMenuItemSelected(menuItem)
return when (menuItem.itemId) {
R.id.action_refresh -> {
binding.swipeRefreshLayout.isRefreshing = true

View File

@ -75,6 +75,7 @@ class SearchActivity : BottomSheetActivity(), MenuProvider, SearchView.OnQueryTe
}
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
super.onCreateMenu(menu, menuInflater)
menuInflater.inflate(R.menu.search_toolbar, menu)
val searchViewMenuItem = menu.findItem(R.id.action_search)
searchViewMenuItem.expandActionView()
@ -83,6 +84,7 @@ class SearchActivity : BottomSheetActivity(), MenuProvider, SearchView.OnQueryTe
}
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
super.onMenuItemSelected(menuItem)
return false
}

View File

@ -78,10 +78,12 @@ class TrendingActivity : BottomSheetActivity(), AppBarLayoutHost, MenuProvider {
}
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
super.onCreateMenu(menu, menuInflater)
menuInflater.inflate(R.menu.activity_trending, menu)
}
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
super.onMenuItemSelected(menuItem)
return super.onOptionsItemSelected(menuItem)
}
}

View File

@ -677,7 +677,6 @@
<string name="action_translate_undo">إلغاء الترجمة</string>
<string name="notification_prune_cache">صيانة ذاكرة التخزين المؤقتة…</string>
<string name="pref_ui_text_size">حجم نص واجهة المستخدم</string>
<string name="acra_dialog_title">لقد تحطّم %1$s</string>
<string name="load_newest_notifications">تحميل أحدث الإشعارات</string>
<string name="label_filter_context">السياقات التي تم تصفيتها</string>
<string name="dialog_follow_hashtag_hint">#وسم</string>

View File

@ -512,7 +512,6 @@
<string name="report_category_violation">Sääntörikkomus</string>
<string name="label_filter_keywords">Avainsanat tai jonot jotka suodatetaan</string>
<string name="drafts_post_reply_removed">Julkaisu, johon olit kirjoittanut vastausluonnoksen, on poistettu</string>
<string name="acra_dialog_text">Pyydämme anteeksi. Paina OK jos haluat lähettää kehittäjille yksityiskohtaiset tiedot</string>
<string name="limit_notifications">Rajoita aikajanan ilmoituksia</string>
<string name="account_date_joined">Liittyi %1$s</string>
<string name="ui_error_unknown">tuntematon syy</string>
@ -558,7 +557,6 @@
<string name="action_add">Lisää</string>
<string name="load_newest_notifications">Lataa uusimmat ilmoitukset</string>
<string name="pref_title_show_cards_in_timelines">Näytä linkkien esikatselut aikajanoissa</string>
<string name="acra_dialog_title">%1$s kaatui</string>
<string name="pref_title_enable_swipe_for_tabs">Vaihda välilehteä pyyhkäisemällä</string>
<string name="report_remote_instance">Lähetä edelleen %s</string>
<string name="poll_info_time_absolute">päättyy %s</string>
@ -635,15 +633,6 @@
\nJos haluat tutkia muita tilejä, voit löytää niitä toisesta aikajanasta, esimerkiksi oman instanssisi paikallisesta aikajanasta [iconics gmd_group]. Voit myös etsiä niitä nimeltä [iconics gmd_search]; etsi esimerkiksi sanalla ”Pachli” löytääksesi Mastodon-tilimme.</string>
<string name="notifications_clear">Poista ilmoitukset</string>
<string name="hint_list_name">Listan nimi</string>
<string name="acra_email_body">Nämä tiedot lähetetään kehittäjille.
\n
\nTarkista, että tiedot eivät sisällä mitään, mitä et halua jakaa, ja kerro, mitä teit, kun ohjelma kaatui.
\n
\n------
\nKirjoita kuvaus tähän:
\n
\n------
\n</string>
<string name="expand_collapse_all_posts">Avaa/Sulje kaikki julkasut</string>
<string name="select_list_title">Valitse lista</string>
<string name="duration_indefinite">Loputon</string>

View File

@ -645,7 +645,6 @@
<string name="filter_keyword_addition_title">Voeg sleutelwoord toe</string>
<string name="label_filter_keywords">Sleutelwoorden of zinnen om te filteren</string>
<string name="pref_title_show_self_boosts_description">Iemand die hun eigen post boost</string>
<string name="acra_dialog_text">Sorry daarvoor. Klik op OK om een e-mail op te stellen naar de ontwikkelaars met alle details</string>
<string name="ui_error_filter_v1_load">Laden van filters faalde: %s</string>
<string name="confirmation_hashtag_muted">#%s verborgen</string>
<string name="janky_animation_title">Mogelijk is het noodzakelijk om je apparaat opnieuw op te starten</string>
@ -656,15 +655,6 @@
<string name="help_empty_home">Dit is je <b>Thuis tijdlijn</b>. Het toont de meest recente posts van accounts die je volgt.
\n
\nOm meer accounts te vinden, kun je ze ontdekken in een van de andere tijdlijnen. Bijvoorbeeld de lokale tijdlijn van jouw instance [iconics gmd_group]. Of je kunt zoeken op naam [iconics gmd_search]; bijvoorbeeld als je zoekt op Pachli dan vind je ons Mastodon account.</string>
<string name="acra_email_body">Deze data wordt verzonden naar de ontwikkelaars.
\n
\nControleer alsjeblieft of er niks tussen zit dat je niet met ons wilt delen, omschrijf wat je deed wanneer de crash gebeurde.
\n
\n ----
\nJe omschrijving hier:
\n
\n ----
\n</string>
<string name="pref_update_notification_frequency_once_per_version">Eenmaal per versie</string>
<string name="error_404_not_found">Je server beschikt niet over ondersteuning voor deze feature</string>
<string name="pref_title_font_family">Lettertype-familie</string>
@ -683,5 +673,4 @@
<string name="action_translate_undo">Vertaling ongedaan maken</string>
<string name="notification_prune_cache">Cache onderhoud…</string>
<string name="pref_ui_text_size">UI tekst grootte</string>
<string name="acra_dialog_title">%1$s crashed</string>
</resources>

View File

@ -684,15 +684,4 @@
<string name="load_newest_notifications">Carregar notificações mais recentes</string>
<string name="action_discard">Descartar mudanças</string>
<string name="pref_ui_text_size">Tamanho do texto da UI</string>
<string name="acra_dialog_text">Desculpas por isso. Clique em OK para preparar uma mensagem aos desenvolvedores com detalhes</string>
<string name="acra_email_body">Esses dados serão enviados aos desenvolvedores.
\n
\nPor favor, verifique se ele não inclui algo que não queriras compartilhar e, por favor, descreva o que estavas fazendo quando a falha aconteceu.
\n
\n----
\nA tua descrição aqui:
\n
\n----
\n</string>
<string name="acra_dialog_title">%1$s travou</string>
</resources>

View File

@ -671,17 +671,6 @@
<string name="title_tab_public_trending_hashtags">Hashtaggar</string>
<string name="label_image">Bild</string>
<string name="action_translate_undo">Ångra översättning</string>
<string name="acra_dialog_text">Förlåt för det. Klicka på OK för att förbereda ett e-postmeddelande till utvecklarna med detaljer</string>
<string name="acra_email_body">Denna data kommer att skickas till utvecklarna.
\n
\nKontrollera att den inte innehåller något du inte vill dela, och beskriv vad du gjorde när kraschen inträffade.
\n
\n ----
\nDin beskrivning här:
\n
\n----
\n</string>
<string name="acra_dialog_title">%1$s kraschade</string>
<string name="server_repository_error">Kunde inte hämta serverinformation för %1$s: %2$s</string>
<string name="server_repository_error_get_well_known_node_info">misslyckades med att hämta /.well-known/nodeinfo: %1$s</string>
<string name="server_repository_error_unsupported_schema">/.well-known/nodeinfo innehöll inte begripliga scheman</string>

View File

@ -708,17 +708,6 @@
<string name="janky_animation_title">You may need to restart your device</string>
<string name="janky_animation_msg">This version of Pachli may trigger an Android bug on some devices, and show broken animations.\n\nFor example, when tapping a post to view a thread.\n\nIf you see this you will need to restart your device.\n\nYou only need to do this once.\n\nThis is Android bug, there is nothing Pachli can do.</string>
<string name="acra_dialog_title">%1$s crashed</string>
<string name="acra_dialog_text">Sorry about that. Click OK to prepare an e-mail to the developers with details</string>
<string name="acra_email_body">This data will be sent to the developers.\n\n
Please check to make sure it does not include anything you don\'t want to share, and please describe what you were doing when the crash happened.\n\n
----\n
Your description here:\n\n
----\n
</string>
<string name="server_repository_error">Could not fetch server info for %1$s: %2$s</string>
<string name="server_repository_error_get_well_known_node_info">fetching /.well-known/nodeinfo failed: %1$s</string>

View File

@ -47,6 +47,9 @@ dependencies {
// Loading avatars
implementation(libs.bundles.glide)
// Crash reporting in orange (Pachli Current) builds only
orangeImplementation(libs.bundles.acra)
// BottomSheetActivityTest uses mockito
testImplementation(libs.bundles.mockito)
}

View File

@ -19,7 +19,7 @@
errorLine2=" ~~~~~~~~~">
<location
file="src/main/kotlin/app/pachli/core/activity/BaseActivity.kt"
line="101"
line="104"
column="13"/>
</issue>

View File

@ -15,9 +15,10 @@
* see <http://www.gnu.org/licenses>.
*/
package app.pachli
package app.pachli.core.activity
import android.app.Application
/** Do nothing in blue builds */
fun initCrashReporter(app: Application) {}
fun triggerCrashReport() {}

View File

@ -24,16 +24,19 @@ import android.content.pm.PackageManager
import android.graphics.BitmapFactory
import android.graphics.Color
import android.os.Bundle
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import androidx.annotation.CallSuper
import androidx.annotation.StringRes
import androidx.annotation.StyleRes
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.view.MenuProvider
import app.pachli.core.accounts.AccountManager
import app.pachli.core.accounts.BuildConfig
import app.pachli.core.database.model.AccountEntity
import app.pachli.core.designsystem.EmbeddedFontFamily
import app.pachli.core.designsystem.R as DR
@ -53,7 +56,7 @@ import javax.inject.Inject
import timber.log.Timber
@AndroidEntryPoint
abstract class BaseActivity : AppCompatActivity() {
abstract class BaseActivity : AppCompatActivity(), MenuProvider {
@Inject
lateinit var accountManager: AccountManager
@ -294,6 +297,24 @@ abstract class BaseActivity : AppCompatActivity() {
ActivityCompat.requestPermissions(this, permissionsCopy, newKey)
}
@CallSuper
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
if (BuildConfig.FLAVOR_color != "orange") return
menuInflater.inflate(R.menu.activity_base, menu)
}
@CallSuper
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
if (BuildConfig.FLAVOR_color != "orange") return false
return when (menuItem.itemId) {
R.id.action_crash_report -> {
triggerCrashReport()
true
}
else -> false
}
}
companion object {
private const val REQUESTER_NONE = Int.MAX_VALUE

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2024 Pachli Association
~
~ This file is a part of Pachli.
~
~ 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.
~
~ Pachli 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 Pachli; if not,
~ see <http://www.gnu.org/licenses>.
-->
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_crash_report"
android:title="@string/send_error_report"
app:showAsAction="never" />
</menu>

View File

@ -3,4 +3,5 @@
<string name="post_lookup_error_format">Error looking up post %s</string>
<string name="action_open_as">Open as %s</string>
<string name="performing_lookup_title">Performing lookup…</string>
<string name="send_error_report">Send error report</string>
</resources>

View File

@ -15,10 +15,11 @@
* see <http://www.gnu.org/licenses>.
*/
package app.pachli
package app.pachli.core.activity
import android.app.Application
import app.pachli.core.designsystem.R as DR
import org.acra.ACRA
import org.acra.config.dialog
import org.acra.config.mailSender
import org.acra.data.StringFormat
@ -46,3 +47,10 @@ fun initCrashReporter(app: Application) {
}
}
}
/**
* Trigger a report without a crash.
*/
fun triggerCrashReport() {
ACRA.errorReporter.handleException(null, false)
}

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<resources>
<string name="acra_dialog_title">لقد تحطّم %1$s</string>
</resources>

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<resources>
<string name="acra_dialog_title">%1$s kaatui</string>
<string name="acra_dialog_text">Pyydämme anteeksi. Paina OK jos haluat lähettää kehittäjille yksityiskohtaiset tiedot</string>
<string name="acra_email_body">Nämä tiedot lähetetään kehittäjille.
\n
\nTarkista, että tiedot eivät sisällä mitään, mitä et halua jakaa, ja kerro, mitä teit, kun ohjelma kaatui.
\n
\n------
\nKirjoita kuvaus tähän:
\n
\n------
\n</string>
</resources>

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<resources>
<string name="acra_dialog_title">%1$s crashed</string>
<string name="acra_dialog_text">Sorry daarvoor. Klik op OK om een e-mail op te stellen naar de ontwikkelaars met alle details</string>
<string name="acra_email_body">Deze data wordt verzonden naar de ontwikkelaars.
\n
\nControleer alsjeblieft of er niks tussen zit dat je niet met ons wilt delen, omschrijf wat je deed wanneer de crash gebeurde.
\n
\n ----
\nJe omschrijving hier:
\n
\n ----
\n</string>
</resources>

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<resources>
<string name="acra_dialog_title">%1$s travou</string>
<string name="acra_dialog_text">Desculpas por isso. Clique em OK para preparar uma mensagem aos desenvolvedores com detalhes</string>
<string name="acra_email_body">Esses dados serão enviados aos desenvolvedores.
\n
\nPor favor, verifique se ele não inclui algo que não queriras compartilhar e, por favor, descreva o que estavas fazendo quando a falha aconteceu.
\n
\n----
\nA tua descrição aqui:
\n
\n----
\n</string>
</resources>

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<resources>
<string name="acra_dialog_title">%1$s kraschade</string>
<string name="acra_dialog_text">Förlåt för det. Klicka på OK för att förbereda ett e-postmeddelande till utvecklarna med detaljer</string>
<string name="acra_email_body">Denna data kommer att skickas till utvecklarna.
\n
\nKontrollera att den inte innehåller något du inte vill dela, och beskriv vad du gjorde när kraschen inträffade.
\n
\n ----
\nDin beskrivning här:
\n
\n----
\n</string>
</resources>

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<resources>
<string name="acra_dialog_title">%1$s error report</string>
<string name="acra_dialog_text">Click OK to prepare an e-mail to the developers with details</string>
<string name="acra_email_body">This data will be sent to the developers.\n\n
Please check to make sure it does not include anything you don\'t want to share, and please describe what you were doing when the crash happened.\n\n
----\n
Your description here:\n\n
----\n
</string>
</resources>

View File

@ -23,6 +23,7 @@ import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.core.UsageError
import com.github.ajalt.clikt.parameters.arguments.argument
import com.github.ajalt.clikt.parameters.arguments.multiple
import com.github.ajalt.clikt.parameters.options.default
import com.github.ajalt.clikt.parameters.options.flag
import com.github.ajalt.clikt.parameters.options.option
import io.github.oshai.kotlinlogging.DelegatingKLogger
@ -52,7 +53,9 @@ private val log = KotlinLogging.logger {}
class App : CliktCommand(help = """Move string resources between modules""") {
private val args by argument().multiple()
private val verbose by option("-n", "--verbose", help = "show additional information").flag()
private val verbose by option("-v", "--verbose", help = "show additional information").flag()
private val srcVariant by option("--srcVariant").default("main")
private val dstVariant by option("--dstVariant").default("main")
/**
* Returns the full path to a module's `.../src/main/res` directory, starting from the
@ -60,7 +63,7 @@ class App : CliktCommand(help = """Move string resources between modules""") {
*
* @return the path, or null if it's not a subtree of [start] or any of its parents.
*/
private fun findResourcePath(start: Path, variant: String = "main"): Path? {
private fun findResourcePath(start: Path, variant: String): Path? {
val suffix = Path("src/$variant/res")
var prefix = start
@ -89,8 +92,8 @@ class App : CliktCommand(help = """Move string resources between modules""") {
val cwd = Paths.get("").toAbsolutePath()
log.info { "working directory: $cwd" }
val srcRes = findResourcePath(Path(src)) ?: throw UsageError("no resources in $src")
val dstRes = findResourcePath(Path(dst)) ?: throw UsageError("no resources in $dst")
val srcRes = findResourcePath(Path(src), srcVariant) ?: throw UsageError("no resources in $src")
val dstRes = findResourcePath(Path(dst), dstVariant) ?: throw UsageError("no resources in $dst")
// Enumerate all the values-* directories that contain a strings.xml file
val resourceDirs =