Phase 2: core-crash from floret-kit; complete the core-time wiring
All checks were successful
CI / ci (push) Successful in 9m16s
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 inec7b696): - Repoint DayWindow + Instant formatting imports to de.jeanlucmakiola.floret.time (the app copies were deleted inec7b696but 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:
@@ -21,6 +21,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v4
|
||||
|
||||
@@ -15,6 +15,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v4
|
||||
@@ -73,6 +75,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v4
|
||||
@@ -322,6 +326,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Extract changelog section for this tag
|
||||
run: |
|
||||
|
||||
@@ -120,6 +120,8 @@ dependencies {
|
||||
|
||||
implementation(libs.kotlinx.datetime)
|
||||
implementation(libs.kotlinx.coroutines.core)
|
||||
implementation("de.jeanlucmakiola.floret:core-time")
|
||||
implementation("de.jeanlucmakiola.floret:core-crash")
|
||||
|
||||
debugImplementation(libs.androidx.ui.tooling)
|
||||
debugImplementation(libs.androidx.ui.test.manifest)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
Submodule floret-kit updated: d3ae83f1a4...19c2f8a677
@@ -25,3 +25,4 @@ dependencyResolutionManagement {
|
||||
|
||||
rootProject.name = "Agendula"
|
||||
include(":app")
|
||||
includeBuild("floret-kit")
|
||||
|
||||
Reference in New Issue
Block a user