Implements #32 (error/crash capture + entry point) and #33 (review & opt-in submit) in one PR. The design is strictly opt-in / user-initiated (F-Droid-safe): nothing is ever sent automatically — a report only leaves the device when the user taps Submitand an ingest endpoint is configured.
CrashReporter installs Thread.setDefaultUncaughtExceptionHandler (wired in LibreMailApplication), persists a structured crash record (stack trace + app/version/device metadata + recent logs) locally, then delegates to the previous handler so the normal crash flow still runs. Never auto-sent.
RingLogBuffer + AppLog — a bounded, in-memory, non-PII log ring buffer mirrored from Logcat.
DiagnosticsCollector assembles a minimal bundle: app version, Android version/device, a fixed non-PII settings allow-list (booleans/enum only — no emails or server names), and the log buffer.
ReportStore persists pending reports as JSON files under filesDir/debug_reports/ — deliberately not Room, so crash-time saves are simple and independent of the (possibly encrypted / mid-migration) app DB.
"Report a problem" entry in SettingsScreen creates a report on demand.
ReportReviewScreen shows the full payload verbatim (DebugReport.toSubmissionPayload() — the single rendering used for the preview, Copy, Save, and the POST body, so what the user sees is byte-for-byte what is sent), a free-text comment field, and a prominent PII disclaimer.
Explicit Submit / Discard, plus Copy and Save-to-file (SAF) alternatives.
On next launch a saved crash report is offered for review via a dialog (Review / Not now / Discard).
Acceptance
A forced crash produces a saved report offered for review on next launch (unit-tested).
"Report a problem" creates a report on demand.
The user can read the entire payload, add comments, see the PII disclaimer, and choose Submit or Discard.
Nothing leaves the device without an explicit Submit tap — the submit path (ReportSubmitter → WorkManager) is the only sender and is referenced only from ReportReviewViewModel.submit(); capture never touches it (unit-tested invariant).
Testing
Fast CI gate green locally (JDK 21): assembleDebug + testDebugUnitTest + lintDebug + ktlintCheck + detekt. 23 new JVM unit tests cover crash capture/persistence (offered next launch), diagnostic-bundle assembly (minimal, non-PII), JSON round-trip, and the "nothing sent without Submit" invariant.
Limitations / out of scope
Server delivery is not exercised here.BuildConfig.DEBUG_REPORT_ENDPOINT is empty by default; the Cloudflare Worker ingest (#34) is out of scope for this repo. With no endpoint, Submit short-circuits to an "online submission unavailable — use Copy/Save" state and never enqueues an upload. Real end-to-end delivery (and its scrubbing) is verified against #34.
No emulator here, so the capture→review logic is verified via unit tests only.
Implements **#32** (error/crash capture + entry point) and **#33** (review & opt-in submit) in one PR. The design is strictly **opt-in / user-initiated (F-Droid-safe)**: nothing is ever sent automatically — a report only leaves the device when the user taps **Submit** *and* an ingest endpoint is configured.
## #32 — Capture + entry point
- **`CrashReporter`** installs `Thread.setDefaultUncaughtExceptionHandler` (wired in `LibreMailApplication`), persists a structured crash record (stack trace + app/version/device metadata + recent logs) **locally**, then delegates to the previous handler so the normal crash flow still runs. Never auto-sent.
- **`RingLogBuffer` + `AppLog`** — a bounded, in-memory, non-PII log ring buffer mirrored from Logcat.
- **`DiagnosticsCollector`** assembles a minimal bundle: app version, Android version/device, a **fixed non-PII settings allow-list** (booleans/enum only — no emails or server names), and the log buffer.
- **`ReportStore`** persists pending reports as JSON files under `filesDir/debug_reports/` — deliberately **not** Room, so crash-time saves are simple and independent of the (possibly encrypted / mid-migration) app DB.
- **"Report a problem"** entry in `SettingsScreen` creates a report on demand.
## #33 — Review & opt-in submit
- **`ReportReviewScreen`** shows the **full payload verbatim** (`DebugReport.toSubmissionPayload()` — the single rendering used for the preview, Copy, Save, *and* the POST body, so what the user sees is byte-for-byte what is sent), a free-text **comment** field, and a **prominent PII disclaimer**.
- Explicit **Submit** / **Discard**, plus **Copy** and **Save-to-file** (SAF) alternatives.
- **`ReportUploadWorker`** (WorkManager, network constraint, exponential backoff + retry cap, success/failure surfaced) POSTs to `BuildConfig.DEBUG_REPORT_ENDPOINT`.
- On next launch a saved **crash report is offered for review** via a dialog (Review / Not now / Discard).
## Acceptance
- [x] A forced crash produces a saved report offered for review on next launch (unit-tested).
- [x] "Report a problem" creates a report on demand.
- [x] The user can read the entire payload, add comments, see the PII disclaimer, and choose Submit or Discard.
- [x] **Nothing leaves the device without an explicit Submit tap** — the submit path (`ReportSubmitter` → WorkManager) is the only sender and is referenced only from `ReportReviewViewModel.submit()`; capture never touches it (unit-tested invariant).
## Testing
Fast CI gate green locally (JDK 21): `assembleDebug` + `testDebugUnitTest` + `lintDebug` + `ktlintCheck` + `detekt`. 23 new JVM unit tests cover crash capture/persistence (offered next launch), diagnostic-bundle assembly (minimal, non-PII), JSON round-trip, and the "nothing sent without Submit" invariant.
## Limitations / out of scope
- **Server delivery is not exercised here.** `BuildConfig.DEBUG_REPORT_ENDPOINT` is **empty by default**; the Cloudflare Worker ingest (#34) is out of scope for this repo. With no endpoint, Submit short-circuits to an "online submission unavailable — use Copy/Save" state and never enqueues an upload. Real end-to-end delivery (and its scrubbing) is verified against #34.
- No emulator here, so the capture→review logic is verified via unit tests only.
Closes #32
Closes #33
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Implements #32 (error/crash capture + entry point) and #33 (review & opt-in submit) in one PR. The design is strictly opt-in / user-initiated (F-Droid-safe): nothing is ever sent automatically — a report only leaves the device when the user taps Submit and an ingest endpoint is configured.
#32 — Capture + entry point
CrashReporterinstallsThread.setDefaultUncaughtExceptionHandler(wired inLibreMailApplication), persists a structured crash record (stack trace + app/version/device metadata + recent logs) locally, then delegates to the previous handler so the normal crash flow still runs. Never auto-sent.RingLogBuffer+AppLog— a bounded, in-memory, non-PII log ring buffer mirrored from Logcat.DiagnosticsCollectorassembles a minimal bundle: app version, Android version/device, a fixed non-PII settings allow-list (booleans/enum only — no emails or server names), and the log buffer.ReportStorepersists pending reports as JSON files underfilesDir/debug_reports/— deliberately not Room, so crash-time saves are simple and independent of the (possibly encrypted / mid-migration) app DB.SettingsScreencreates a report on demand.#33 — Review & opt-in submit
ReportReviewScreenshows the full payload verbatim (DebugReport.toSubmissionPayload()— the single rendering used for the preview, Copy, Save, and the POST body, so what the user sees is byte-for-byte what is sent), a free-text comment field, and a prominent PII disclaimer.ReportUploadWorker(WorkManager, network constraint, exponential backoff + retry cap, success/failure surfaced) POSTs toBuildConfig.DEBUG_REPORT_ENDPOINT.Acceptance
ReportSubmitter→ WorkManager) is the only sender and is referenced only fromReportReviewViewModel.submit(); capture never touches it (unit-tested invariant).Testing
Fast CI gate green locally (JDK 21):
assembleDebug+testDebugUnitTest+lintDebug+ktlintCheck+detekt. 23 new JVM unit tests cover crash capture/persistence (offered next launch), diagnostic-bundle assembly (minimal, non-PII), JSON round-trip, and the "nothing sent without Submit" invariant.Limitations / out of scope
BuildConfig.DEBUG_REPORT_ENDPOINTis empty by default; the Cloudflare Worker ingest (#34) is out of scope for this repo. With no endpoint, Submit short-circuits to an "online submission unavailable — use Copy/Save" state and never enqueues an upload. Real end-to-end delivery (and its scrubbing) is verified against #34.Closes #32
Closes #33
🤖 Generated with Claude Code