Phase 2: core-crash from floret-kit; complete the core-time wiring
All checks were successful
CI / ci (push) Successful in 9m16s

Adds crash reporting to Agendula by drawing the kit's new core-crash module
(first Android-library module in floret-kit), and finishes the core-time wiring
that a Phase 0 staging slip left uncommitted on main.

core-crash (NEW capability — Agendula had none):
- CrashReporter.install() in AgendulaApp captures uncaught exceptions on-device
  (allowlist-only report, chains to the platform handler, uploads nothing).
- MainActivity routes to a standalone CrashReportActivity on a startup crash-loop,
  and surfaces a single captured crash as a dialog on next launch; markHealthy on
  resume. App label + issue-tracker URL flow in via CrashConfig.
- Thin app-side CrashReportActivity uses AgendulaTheme; the reusable machinery
  (reporter, dialog, submit, config) lives in the kit. Submodule re-pinned to the
  core-crash kit commit.

Completes Phase 0 (was only partially committed in ec7b696):
- Repoint DayWindow + Instant formatting imports to de.jeanlucmakiola.floret.time
  (the app copies were deleted in ec7b696 but the imports/includeBuild/dependency
  were never staged, leaving main non-building).
- settings.gradle.kts includeBuild("floret-kit"); app depends on core-time.

Also fixes the CI checkout YAML: the submodules block was mis-indented under
'uses:' (invalid step mapping) — dedented to a proper sibling.

Clean composite build + unit tests + lintDebug + debug assemble green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-28 12:24:49 +02:00
parent ec7b696eb9
commit 152226c1b2
17 changed files with 151 additions and 11 deletions

View File

@@ -52,6 +52,14 @@
</intent-filter>
</activity>
<!-- Standalone crash-report surface; MainActivity routes here on a
startup crash-loop. Not exported, kept out of recents. -->
<activity
android:name=".ui.crash.CrashReportActivity"
android:exported="false"
android:excludeFromRecents="true"
android:launchMode="singleTask" />
<!-- Reminder alarm fires here (internal PendingIntent → not exported). -->
<receiver
android:name=".data.reminders.DueReminderReceiver"

View File

@@ -7,6 +7,8 @@ import dagger.hilt.android.EntryPointAccessors
import dagger.hilt.android.HiltAndroidApp
import dagger.hilt.components.SingletonComponent
import de.jeanlucmakiola.agendula.data.reminders.ReminderScheduler
import de.jeanlucmakiola.floret.crash.CrashConfig
import de.jeanlucmakiola.floret.crash.CrashReporter
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
@@ -22,6 +24,17 @@ class AgendulaApp : Application() {
override fun onCreate() {
super.onCreate()
// Install first thing so startup crashes are captured too (privacy-
// respecting, on-device; the user submits the report by hand).
CrashReporter.install(
this,
CrashConfig(
appLabel = getString(R.string.app_name),
newIssueUrl = getString(R.string.report_issue_url),
chooseIssueUrl = getString(R.string.report_issue_url),
issueTitle = getString(R.string.crash_report_issue_title),
),
)
val scheduler = EntryPointAccessors
.fromApplication(this, ReminderEntryPoint::class.java)
.reminderScheduler()

View File

@@ -9,6 +9,8 @@ import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@@ -17,8 +19,12 @@ import dagger.hilt.android.AndroidEntryPoint
import de.jeanlucmakiola.agendula.data.demo.DemoSeeder
import de.jeanlucmakiola.agendula.data.prefs.ThemeMode
import de.jeanlucmakiola.agendula.ui.RootScreen
import de.jeanlucmakiola.agendula.ui.crash.CrashReportActivity
import de.jeanlucmakiola.agendula.ui.settings.SettingsViewModel
import de.jeanlucmakiola.agendula.ui.theme.AgendulaTheme
import de.jeanlucmakiola.floret.crash.CrashReportDialog
import de.jeanlucmakiola.floret.crash.CrashReporter
import de.jeanlucmakiola.floret.crash.submitCrashReport
import kotlinx.coroutines.launch
import javax.inject.Inject
@@ -32,10 +38,28 @@ class MainActivity : ComponentActivity() {
@Inject lateinit var demoSeeder: DemoSeeder
// A captured crash report awaiting the user's decision, surfaced as a dialog
// over the app on the next launch (the single-crash path). A startup
// crash-loop is handled out of band, before setContent — see below.
private var pendingCrashReport by mutableStateOf<String?>(null)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// If the app keeps crashing as it starts, the main UI can't be trusted
// to come up. Route to the standalone report screen instead of
// re-entering the crashing graph.
if (CrashReporter.isCrashLoop(this)) {
startActivity(Intent(this, CrashReportActivity::class.java))
finish()
return
}
enableEdgeToEdge()
// Surface a single captured crash as a dialog on the next launch.
if (CrashReporter.shouldPrompt(this)) pendingCrashReport = CrashReporter.pendingReport(this)
// Debug-only sample data: `am start ... --ez agendula_seed true`. Seeds a
// local (non-syncing) demo list once; no-op without the extra.
if (BuildConfig.DEBUG && intent.getBooleanExtra(EXTRA_SEED, false)) {
@@ -51,10 +75,31 @@ class MainActivity : ComponentActivity() {
}
AgendulaTheme(darkTheme = darkTheme, dynamicColor = ui.settings.dynamicColor) {
RootScreen(modifier = Modifier.fillMaxSize())
pendingCrashReport?.let { report ->
CrashReportDialog(
report = report,
onSend = {
submitCrashReport(this@MainActivity, report)
CrashReporter.clearReport(this@MainActivity)
pendingCrashReport = null
},
onDismiss = {
CrashReporter.dismissPrompt(this@MainActivity)
pendingCrashReport = null
},
)
}
}
}
}
override fun onResume() {
super.onResume()
// A successful start breaks any loop; reset the timing trail so a later
// ordinary crash isn't mistaken for a loop.
CrashReporter.markHealthy(this)
}
companion object {
const val EXTRA_TASK_ID = "de.jeanlucmakiola.agendula.extra.TASK_ID"
private const val EXTRA_SEED = "agendula_seed"

View File

@@ -2,7 +2,7 @@ package de.jeanlucmakiola.agendula.data.demo
import de.jeanlucmakiola.agendula.data.di.IoDispatcher
import de.jeanlucmakiola.agendula.data.tasks.TasksRepository
import de.jeanlucmakiola.agendula.domain.DayWindow
import de.jeanlucmakiola.floret.time.DayWindow
import de.jeanlucmakiola.agendula.domain.Priority
import de.jeanlucmakiola.agendula.domain.TaskForm
import kotlinx.coroutines.CoroutineDispatcher

View File

@@ -1,7 +1,7 @@
package de.jeanlucmakiola.agendula.data.tasks
import de.jeanlucmakiola.agendula.data.di.IoDispatcher
import de.jeanlucmakiola.agendula.domain.DayWindow
import de.jeanlucmakiola.floret.time.DayWindow
import de.jeanlucmakiola.agendula.domain.Task
import de.jeanlucmakiola.agendula.domain.TaskDetail
import de.jeanlucmakiola.agendula.domain.TaskFilter

View File

@@ -1,5 +1,6 @@
package de.jeanlucmakiola.agendula.ui.common
import de.jeanlucmakiola.floret.time.formatDateTime
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row

View File

@@ -0,0 +1,61 @@
package de.jeanlucmakiola.agendula.ui.crash
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.ui.Modifier
import de.jeanlucmakiola.agendula.ui.theme.AgendulaTheme
import de.jeanlucmakiola.floret.crash.CrashReportDialog
import de.jeanlucmakiola.floret.crash.CrashReporter
import de.jeanlucmakiola.floret.crash.submitCrashReport
/**
* Standalone surface for a captured crash report. [MainActivity] routes here
* when it detects a startup crash-loop (see [CrashReporter.isCrashLoop]): the
* main UI can't be trusted to start, so this screen stays clear of the app's
* Hilt graph, DataStore-backed theme and Compose content — it only reads the
* report file and shows the kit's report dialog. Plain [AgendulaTheme] defaults
* (follow-system, dynamic colour) avoid touching anything that might be the
* cause of the crash. The reusable machinery lives in floret-kit's core-crash.
*/
class CrashReportActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val report = CrashReporter.pendingReport(this)
if (report == null) {
finish()
return
}
enableEdgeToEdge()
setContent {
AgendulaTheme {
// Opaque backdrop so the dialog doesn't float over a bare task.
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.surface) {}
CrashReportDialog(
report = report,
onSend = {
submitCrashReport(this, report)
CrashReporter.clearReport(this)
finish()
},
onDismiss = {
CrashReporter.clearReport(this)
finish()
},
)
}
}
}
override fun onResume() {
super.onResume()
// Reaching this screen breaks the loop; reset the timing trail so a
// later ordinary crash isn't mistaken for a loop.
CrashReporter.markHealthy(this)
}
}

View File

@@ -72,8 +72,8 @@ import de.jeanlucmakiola.agendula.domain.Task
import de.jeanlucmakiola.agendula.domain.TaskDetail
import de.jeanlucmakiola.agendula.ui.common.GroupedSurface
import de.jeanlucmakiola.agendula.ui.common.PriorityChip
import de.jeanlucmakiola.agendula.ui.common.formatDate
import de.jeanlucmakiola.agendula.ui.common.formatTime
import de.jeanlucmakiola.floret.time.formatDate
import de.jeanlucmakiola.floret.time.formatTime
import de.jeanlucmakiola.agendula.ui.common.pastelize
import de.jeanlucmakiola.agendula.ui.common.positionOf
import de.jeanlucmakiola.agendula.ui.tasklist.priorityLabel

View File

@@ -86,7 +86,7 @@ import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import de.jeanlucmakiola.agendula.R
import de.jeanlucmakiola.agendula.domain.DayWindow
import de.jeanlucmakiola.floret.time.DayWindow
import de.jeanlucmakiola.agendula.domain.Priority
import de.jeanlucmakiola.agendula.domain.Task
import de.jeanlucmakiola.agendula.domain.TaskFormError
@@ -98,8 +98,8 @@ import de.jeanlucmakiola.agendula.ui.common.GroupedRow
import de.jeanlucmakiola.agendula.ui.common.InlineTextField
import de.jeanlucmakiola.agendula.ui.common.OptionCard
import de.jeanlucmakiola.agendula.ui.common.priorityFill
import de.jeanlucmakiola.agendula.ui.common.formatDate
import de.jeanlucmakiola.agendula.ui.common.formatTime
import de.jeanlucmakiola.floret.time.formatDate
import de.jeanlucmakiola.floret.time.formatTime
import de.jeanlucmakiola.agendula.ui.common.localToInstant
import de.jeanlucmakiola.agendula.ui.common.pastelize
import de.jeanlucmakiola.agendula.ui.common.positionOf

View File

@@ -4,7 +4,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import de.jeanlucmakiola.agendula.data.tasks.TasksRepository
import de.jeanlucmakiola.agendula.domain.DayWindow
import de.jeanlucmakiola.floret.time.DayWindow
import de.jeanlucmakiola.agendula.domain.SmartList
import de.jeanlucmakiola.agendula.domain.Task
import de.jeanlucmakiola.agendula.domain.TaskFilter

View File

@@ -81,7 +81,7 @@ import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import de.jeanlucmakiola.agendula.R
import de.jeanlucmakiola.agendula.domain.DayWindow
import de.jeanlucmakiola.floret.time.DayWindow
import de.jeanlucmakiola.agendula.domain.Priority
import de.jeanlucmakiola.agendula.domain.SmartList
import de.jeanlucmakiola.agendula.domain.Task
@@ -91,7 +91,7 @@ import de.jeanlucmakiola.agendula.domain.TaskSections
import de.jeanlucmakiola.agendula.ui.common.ListNameChip
import de.jeanlucmakiola.agendula.ui.common.Position
import de.jeanlucmakiola.agendula.ui.common.PriorityChip
import de.jeanlucmakiola.agendula.ui.common.formatDateTime
import de.jeanlucmakiola.floret.time.formatDateTime
import de.jeanlucmakiola.agendula.ui.common.pastelize
import de.jeanlucmakiola.agendula.ui.common.positionOf
import java.time.ZoneId

View File

@@ -176,6 +176,7 @@
<string name="settings_report_problem">Report a problem</string>
<string name="settings_report_problem_hint">Open the issue tracker</string>
<string name="about_source_url" translatable="false">https://gitea.jeanlucmakiola.de/makiolaj/agendula</string>
<string name="crash_report_issue_title">Crash report</string>
<string name="report_issue_url" translatable="false">https://gitea.jeanlucmakiola.de/makiolaj/agendula/issues/new</string>
<string name="about_license_url" translatable="false">https://gitea.jeanlucmakiola.de/makiolaj/agendula/src/branch/main/LICENSE</string>
<string name="about_support_url" translatable="false">https://ko-fi.com/jeanlucmakiola</string>