Merge remote-tracking branch 'origin/main' into feat/agendula-1.0

# Conflicts:
#	CHANGELOG.md
This commit is contained in:
2026-09-24 16:24:52 +02:00
5 changed files with 92 additions and 16 deletions
+11
View File
@@ -37,6 +37,17 @@ are no binary releases; "version" tracks the shared `version` in the root build
for the quick-add bar; the viewport height the app scales from is a parameter
rather than a calendar-shaped constant.
### Fixed
- **`core-crash`** — The prefilled crash issue is filed in English again. Its
title and body came from string resources, so a crash on a French phone
opened the tracker with a French issue — unreadable to whoever has to triage
it. That text is now fixed English in `CrashReportSubmit`, out of resources
altogether, and `CrashConfig` no longer takes an `issueTitle` (apps drop the
argument, and their own `crash_report_issue_title` / `crash_report_body_*`
strings with it). Everything the reporter *sees* — the dialog, the toasts,
the clipboard label — stays translated.
## [0.3.0] - 2026-08-10
### Added
@@ -8,6 +8,10 @@ package de.jeanlucmakiola.floret.crash
*
* URLs are passed as plain strings (resolved from each app's own resources)
* rather than kit resources, since they differ per app and are not translated.
*
* The prefilled issue's title and body are *not* here: they are posted to the
* tracker rather than shown in the app, so they are fixed English text owned by
* the kit (see [submitCrashReport]) and cannot pick up the device language.
*/
data class CrashConfig(
/** Display name, e.g. "Agendula". Used in the report header and dialog copy. */
@@ -16,6 +20,4 @@ data class CrashConfig(
val newIssueUrl: String,
/** Issue tracker entry point for a manual (non-crash) report — e.g. a chooser. */
val chooseIssueUrl: String,
/** Title used for the prefilled crash issue, e.g. "Crash report". */
val issueTitle: String,
)
@@ -14,12 +14,15 @@ import androidx.core.net.toUri
* automatically — the apps have no network access; the user reviews and submits
* the issue themselves. Issue-tracker URLs and the app label come from the
* installed [CrashReporter.config].
*
* What the user *sees* here — the dialog, the toasts, the clipboard label — is
* translated. What is *posted* is not: see [buildIssueBody].
*/
fun submitCrashReport(context: Context, report: String) {
val config = CrashReporter.config ?: return
copyReportToClipboard(context, config.appLabel, report)
val opened = runCatching {
context.startActivity(Intent(Intent.ACTION_VIEW, buildIssueUri(context, config, report)))
context.startActivity(Intent(Intent.ACTION_VIEW, buildIssueUri(config, report)))
}.isSuccess
val message = if (opened) R.string.crash_report_copied else R.string.crash_report_open_failed
Toast.makeText(context, message, Toast.LENGTH_LONG).show()
@@ -43,20 +46,33 @@ private fun copyReportToClipboard(context: Context, appLabel: String, report: St
* "paste from clipboard" placeholder) — the clipboard copy is the source of
* truth in that case.
*/
private fun buildIssueUri(context: Context, config: CrashConfig, report: String) =
private fun buildIssueUri(config: CrashConfig, report: String) =
config.newIssueUrl.toUri().buildUpon()
.appendQueryParameter("title", config.issueTitle)
.appendQueryParameter("body", buildIssueBody(context, config.appLabel, report))
.appendQueryParameter("title", ISSUE_TITLE)
.appendQueryParameter("body", buildIssueBody(config.appLabel, report))
.build()
private fun buildIssueBody(context: Context, appLabel: String, report: String): String {
val block = if (report.length > MAX_URL_REPORT_CHARS) {
context.getString(R.string.crash_report_body_paste)
} else {
"```\n$report\n```"
}
return context.getString(R.string.crash_report_body_template, appLabel, block)
/**
* The prefilled issue body. Deliberately hard-coded English rather than a string
* resource: this text is filed in the project's issue tracker, where maintainers
* read it, so it must not follow the device language — a report from a French
* phone used to arrive as a French issue. Everything the reporter sees inside
* the app stays translated.
*/
internal fun buildIssueBody(appLabel: String, report: String): String {
val block = if (report.length > MAX_URL_REPORT_CHARS) REPORT_TOO_LONG else "```\n$report\n```"
return "Thanks for reporting a crash in $appLabel. Please add anything you remember " +
"about what you were doing, then submit.\n\n" +
"### What happened\n\n\n" +
"### Crash report\n$block\n"
}
/** Title of the prefilled issue. English, for the same reason as the body. */
internal const val ISSUE_TITLE = "Crash report"
/** Stands in for a report too long to carry in the link; it is on the clipboard. */
private const val REPORT_TOO_LONG =
"_(The report was too long for this link — paste it from your clipboard here.)_"
/** Keep the prefilled body comfortably under common URL-length ceilings. */
private const val MAX_URL_REPORT_CHARS = 6_000
+4 -3
View File
@@ -3,7 +3,10 @@
Generic crash-report UI strings shared by the family. App-agnostic: the
app's display name is injected as %1$s from CrashConfig.appLabel, so these
read correctly for any app. Issue-tracker URLs are NOT here — they are
per-app and come from CrashConfig. Apps may override any of these strings.
per-app and come from CrashConfig. Neither is the prefilled issue's title
or body: that text is posted to the tracker rather than shown in the app,
so it is fixed English in CrashReportSubmit.kt and never translated.
Apps may override any of the strings below.
-->
<string name="crash_dialog_title">%1$s crashed</string>
<string name="crash_dialog_message">%1$s closed unexpectedly last time. You can help fix it by sending this report as an issue. It stays on your device until you choose to share it, and includes no personal data or app content — only the technical details below.</string>
@@ -12,8 +15,6 @@
<string name="crash_report_clip_label">%1$s crash report</string>
<string name="crash_report_copied">Report copied to your clipboard</string>
<string name="crash_report_open_failed">Couldn\'t open the issue tracker. The report is on your clipboard.</string>
<string name="crash_report_body_template">Thanks for reporting a crash in %1$s. Please add anything you remember about what you were doing, then submit.\n\n### What happened\n\n\n### Crash report\n%2$s\n</string>
<string name="crash_report_body_paste">_(The report was too long for this link — paste it from your clipboard here.)_</string>
<!-- Fallback app label if the dialog is shown before install() (should not happen). -->
<string name="crash_app_fallback">The app</string>
</resources>
@@ -0,0 +1,46 @@
package de.jeanlucmakiola.floret.crash
import com.google.common.truth.Truth.assertThat
import org.junit.jupiter.api.Test
import java.util.Locale
/**
* The issue title and body are posted to the tracker, not shown in the app, so
* they must stay English whatever language the device is in — a crash from a
* French phone once filed a French issue. Pinning them as plain text (rather
* than string resources) is what guarantees it; these tests hold that line.
*/
class IssueBodyTest {
@Test
fun `body is English regardless of the device locale`() {
val body = withLocale(Locale.FRANCE) { buildIssueBody("Calendula", "boom") }
assertThat(body).contains("Thanks for reporting a crash in Calendula.")
assertThat(body).contains("### What happened")
assertThat(body).contains("### Crash report")
assertThat(ISSUE_TITLE).isEqualTo("Crash report")
}
@Test
fun `a short report rides along in a fenced block`() {
val body = buildIssueBody("Agendula", "stack trace here")
assertThat(body).contains("```\nstack trace here\n```")
}
@Test
fun `an over-long report is replaced by the clipboard placeholder`() {
val body = buildIssueBody("Clockula", "x".repeat(7_000))
assertThat(body).doesNotContain("xxxx")
assertThat(body).contains("paste it from your clipboard here")
}
private fun <T> withLocale(locale: Locale, block: () -> T): T {
val previous = Locale.getDefault()
Locale.setDefault(locale)
return try {
block()
} finally {
Locale.setDefault(previous)
}
}
}