From 14b19cd902651773b28781c71a398ef11a34f13e Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Wed, 1 Jul 2026 12:02:14 +0200 Subject: [PATCH 1/4] fix(widget): keep Glance ActionCallback classes under R8 (#18) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The month widget's prev/next/today controls and the agenda widget's refresh run through Glance `actionRunCallback()`, which persists the callback's class name into the click PendingIntent and instantiates it reflectively when tapped. Under R8 full mode (AGP 9 default) those callback classes — only ever referenced reflectively — were renamed and lost their no-arg constructor, so the lookup failed silently and the arrows did nothing on release builds (while actionStartActivity taps, which need no reflection, kept working). Keep every ActionCallback's name and constructor. Verified against the release mapping: ShiftMonthAction/ResetMonthAction/RefreshAgendaAction now retain their original fully-qualified names. Co-Authored-By: Claude Opus 4.8 (1M context) --- app/proguard-rules.pro | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index fd9c2c8..8b1a5d3 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -16,6 +16,18 @@ -keep class * extends androidx.room.RoomDatabase { *; } -dontwarn androidx.room.paging.** +# Glance runs an @Composable's `actionRunCallback()` by persisting the +# callback's fully-qualified class name into the click PendingIntent, then +# reflectively instantiating it (Class.forName(name).newInstance()) when the tap +# fires. Under R8 full mode (AGP 9 default) these ActionCallback classes — only +# ever referenced reflectively — get renamed or have their no-arg constructor +# stripped, so the lookup fails silently and the tap does nothing. In the month +# and agenda widgets that broke every run-callback control (the prev/next/today +# month arrows and the agenda refresh) in release builds while actionStartActivity +# taps, which ride a PendingIntent and need no reflection, kept working. Keep +# every ActionCallback's name and constructor intact. +-keep class * implements androidx.glance.appwidget.action.ActionCallback { (...); } + # WorkManager instantiates an InputMerger reflectively (Class.newInstance) from # the fully-qualified class name persisted in the WorkSpec, so the class must # keep both its name and a no-arg constructor. Glance renders every widget -- 2.49.1 From 40d70d9f2b8c149149e816b97bb2e9010cd4f445 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Wed, 1 Jul 2026 12:02:14 +0200 Subject: [PATCH 2/4] feat(widget): open the app from the widget headers (#18, #20) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tapping a widget header now opens the app on a sensible top-level view, via a shared WidgetNavRequest.OpenView(view?) — a concrete view roots there over the default home, a null view resolves to the default home: - Month widget: the month/year title opens the month view (#18). Its "today" button now snaps the grid back to the current month in place (the reset that used to sit on the title), so paging + jump-to-today are both reachable and the title is free to open the app. - Agenda widget: the "Upcoming" title opens the default view (#20), so users whose home view is Week/Month can reach it in one tap instead of drilling through a day or event. Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 13 ++++++++++ .../jeanlucmakiola/calendula/MainActivity.kt | 24 +++++++++++++++++++ .../calendula/ui/CalendarHost.kt | 11 +++++++++ .../calendula/ui/WidgetNavRequest.kt | 9 +++++++ .../calendula/widget/agenda/AgendaWidget.kt | 8 ++++++- .../calendula/widget/month/MonthWidget.kt | 15 +++++++----- 6 files changed, 73 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d973ab..5e20ae8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,8 +14,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 a reminder a week before *and* one on the day. The reminder pickers are now multi-select; per-calendar overrides can still inherit the global default or turn reminders off entirely. Thanks to @moonj for the suggestion ([#14]). +- Widget headers now open the app. Tapping the month/year title on the month + widget opens the app on the month view, and tapping the "Upcoming" title on + the agenda widget opens it on your default view — so there's a one-tap way + back into the app that lands where you'd expect, instead of only through a day + or event. The month widget's "today" button now snaps the grid back to the + current month in place. Thanks to @rgz46vic and @ptab for the suggestions + ([#18], [#20]). ### Fixed +- Month widget arrows and "today" button work again. On release builds the + prev/next-month arrows and the jump-to-today control on the month widget did + nothing when tapped — code shrinking had stripped the tap handlers behind + them. They respond again. Thanks to @rgz46vic for the report ([#18]). - Disabled calendars no longer notify. Reminders for events in a calendar you have disabled (Settings → Calendars) are now suppressed instead of still popping up — matching how a disabled calendar's events already stay hidden @@ -754,3 +765,5 @@ automatically, with zero telemetry and no internet permission. [#14]: https://codeberg.org/jlmakiola/calendula/issues/14 [#16]: https://codeberg.org/jlmakiola/calendula/issues/16 [#17]: https://codeberg.org/jlmakiola/calendula/issues/17 +[#18]: https://codeberg.org/jlmakiola/calendula/issues/18 +[#20]: https://codeberg.org/jlmakiola/calendula/issues/20 diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/MainActivity.kt b/app/src/main/java/de/jeanlucmakiola/calendula/MainActivity.kt index ede492a..b74872a 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/MainActivity.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/MainActivity.kt @@ -193,6 +193,12 @@ class MainActivity : AppCompatActivity() { // An external date tap (launcher/clock) has no widget source, so it opens // the day view rooted over the default home view (OpenDate source = null). calendarTimeDateOrNull()?.let { return WidgetNavRequest.OpenDate(it.toString(), source = null) } + // A widget header tap: open a top-level view with no date drill-in. The + // empty string carried by [openViewIntent] means "the default home view". + if (hasExtra(EXTRA_OPEN_VIEW)) { + val name = getStringExtra(EXTRA_OPEN_VIEW).orEmpty() + return WidgetNavRequest.OpenView(CalendarView.entries.firstOrNull { it.name == name }) + } val source = sourceViewOrNull() val eventId = getLongExtra(EXTRA_EVENT_ID, -1L) return when { @@ -244,6 +250,10 @@ class MainActivity : AppCompatActivity() { private const val EXTRA_DATE_ISO = "de.jeanlucmakiola.calendula.extra.DATE_ISO" private const val EXTRA_CREATE = "de.jeanlucmakiola.calendula.extra.CREATE" + // A widget header tap asking to open a top-level view (no date drill-in). + // Its value is the target [CalendarView] name, or "" for the default view. + private const val EXTRA_OPEN_VIEW = "de.jeanlucmakiola.calendula.extra.OPEN_VIEW" + // The [CalendarView] (by name) of the widget a launch came from. Roots the // in-app back stack in that view; absent for non-widget launches (reminders). private const val EXTRA_SOURCE_VIEW = "de.jeanlucmakiola.calendula.extra.SOURCE_VIEW" @@ -307,5 +317,19 @@ class MainActivity : AppCompatActivity() { putExtra(EXTRA_DATE_ISO, date.toString()) addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) } + + /** + * Open the app on a top-level [view] with no date drill-in — a widget + * header tap. A null [view] opens the user's default home view (the agenda + * widget's "Upcoming" title); a concrete view roots there over the default + * home (the month widget's month/year title → [CalendarView.Month]). The + * per-view data URI keeps distinct headers' PendingIntents from collapsing. + */ + fun openViewIntent(context: Context, view: CalendarView?): Intent = + Intent(context, MainActivity::class.java).apply { + data = "calendula://view/${view?.name ?: "default"}".toUri() + putExtra(EXTRA_OPEN_VIEW, view?.name ?: "") + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } } } diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/CalendarHost.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/CalendarHost.kt index 50ee186..dc2cc4f 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/CalendarHost.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/CalendarHost.kt @@ -212,6 +212,17 @@ fun CalendarHost( detailKey = key onWidgetNavConsumed() } + is WidgetNavRequest.OpenView -> { + // A widget header tap: land on a top-level view with no date + // drill-in. Reveal it by dropping any covering overlay, then root + // the stack on the target (null → the default home) over the + // default home — so backing out returns to the default, then exits. + dismissCoveringOverlays() + createDateIso = null + pendingDayIso = null + viewStack = viewBaseStack(defaultView, req.view ?: defaultView) + onWidgetNavConsumed() + } is WidgetNavRequest.Create -> { // External "new event" entries (QS tile / launcher shortcut / // widget) must land on top of whatever is open — the form overlay diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/WidgetNavRequest.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/WidgetNavRequest.kt index 6588d10..985806b 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/WidgetNavRequest.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/WidgetNavRequest.kt @@ -31,4 +31,13 @@ sealed interface WidgetNavRequest { /** Open the create-event form prefilled for [dateIso] (today when null). */ data class Create(val dateIso: String?) : WidgetNavRequest + + /** + * Open the app rooted on a top-level [view] with no date drill-in — a widget + * header tap. A null [view] means "the user's default home view" (the agenda + * widget's "Upcoming" title, issue #20); a concrete view roots there over the + * default home (the month widget's month/year title → [CalendarView.Month], + * issue #18), so backing out returns to the default view, then exits. + */ + data class OpenView(val view: CalendarView?) : WidgetNavRequest } diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/widget/agenda/AgendaWidget.kt b/app/src/main/java/de/jeanlucmakiola/calendula/widget/agenda/AgendaWidget.kt index 19c06c5..0174eed 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/widget/agenda/AgendaWidget.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/widget/agenda/AgendaWidget.kt @@ -193,6 +193,8 @@ private fun AgendaHeader() { modifier = GlanceModifier.fillMaxWidth().padding(horizontal = 4.dp), verticalAlignment = Alignment.CenterVertically, ) { + // Tap the "Upcoming" title to open the app on the user's default view + // (issue #20) — a null target resolves to the default home in the host. Text( text = context.getString(R.string.widget_agenda_title), style = TextStyle( @@ -200,7 +202,11 @@ private fun AgendaHeader() { fontSize = 16.sp, fontWeight = FontWeight.Medium, ), - modifier = GlanceModifier.defaultWeight(), + modifier = GlanceModifier + .defaultWeight() + .clickable( + actionStartActivity(MainActivity.openViewIntent(context, view = null)), + ), ) IconButton( resId = R.drawable.ic_widget_refresh, diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/widget/month/MonthWidget.kt b/app/src/main/java/de/jeanlucmakiola/calendula/widget/month/MonthWidget.kt index 1799bd5..d08e65b 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/widget/month/MonthWidget.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/widget/month/MonthWidget.kt @@ -194,6 +194,8 @@ private fun MonthHeader(label: String) { ), ), ) + // Tap the month/year title to open the app on the month view (issue #18), + // rooted over the default home so backing out returns there. Text( text = label, style = TextStyle( @@ -204,16 +206,17 @@ private fun MonthHeader(label: String) { ), modifier = GlanceModifier .defaultWeight() - .clickable(actionRunCallback()), + .clickable( + actionStartActivity( + MainActivity.openViewIntent(context, CalendarView.Month), + ), + ), ) + // The "today" button snaps the grid back to the current month in place. HeaderIcon( resId = R.drawable.ic_widget_today, contentDescription = context.getString(R.string.widget_today), - onClick = GlanceModifier.clickable( - actionStartActivity( - MainActivity.openDateIntent(context, today(systemZone()), CalendarView.Month), - ), - ), + onClick = GlanceModifier.clickable(actionRunCallback()), ) HeaderIcon( resId = R.drawable.ic_widget_chevron_right, -- 2.49.1 From 580dc5a669bf0cb44dae4429bd23b1102805a874 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Wed, 1 Jul 2026 12:23:33 +0200 Subject: [PATCH 3/4] feat(widget): tap anywhere on a month-widget day to open it (#18) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously only the small day-number was a tap target, so tapping the blank area of a day cell (or its "+N" overflow) did nothing. Make every non-event part of a day column open that day — the day number, the empty lane cells, and the overflow row — via a shared openDayAction, so a tap anywhere on a day opens it, matching the in-app month grid. Event bars keep opting out to open their own detail. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../calendula/widget/month/MonthWidget.kt | 33 +++++++++++++++---- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/widget/month/MonthWidget.kt b/app/src/main/java/de/jeanlucmakiola/calendula/widget/month/MonthWidget.kt index d08e65b..89b401b 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/widget/month/MonthWidget.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/widget/month/MonthWidget.kt @@ -304,6 +304,15 @@ private fun WeekRow( } } +/** + * Open [date]'s day view rooted in the month view (so back returns to the month + * grid) — the same target the in-app month grid uses when a day cell is tapped. + * Shared by every tappable part of a day column so, as in the app, a tap anywhere + * on a day opens it; only an event bar on top opts out to open its own detail. + */ +private fun openDayAction(context: Context, date: LocalDate) = + actionStartActivity(MainActivity.openDateIntent(context, date, CalendarView.Month)) + @Composable private fun DayNumber(date: LocalDate, isToday: Boolean, inMonth: Boolean, colW: Dp) { val context = LocalContext.current @@ -311,11 +320,7 @@ private fun DayNumber(date: LocalDate, isToday: Boolean, inMonth: Boolean, colW: modifier = GlanceModifier .width(colW) .height(DAY_NUMBER_HEIGHT) - // Tap a day number to open that day, rooted in the month view so back - // returns to the month grid. - .clickable( - actionStartActivity(MainActivity.openDateIntent(context, date, CalendarView.Month)), - ), + .clickable(openDayAction(context, date)), contentAlignment = Alignment.Center, ) { Box( @@ -342,6 +347,7 @@ private fun DayNumber(date: LocalDate, isToday: Boolean, inMonth: Boolean, colW: @Composable private fun LaneRow(week: MonthWeek, lane: Int, dark: Boolean, colW: Dp) { + val context = LocalContext.current Row(modifier = GlanceModifier.fillMaxWidth()) { var col = 0 while (col < 7) { @@ -355,7 +361,14 @@ private fun LaneRow(week: MonthWeek, lane: Int, dark: Boolean, colW: Dp) { if (timed != null) { SpanBar(event = timed, dark = dark, width = colW) } else { - Box(GlanceModifier.width(colW).height(LANE_HEIGHT)) {} + // Empty lane cell: a tap opens that day, so blank space in a + // day column is a day-open target just like the number is. + Box( + GlanceModifier + .width(colW) + .height(LANE_HEIGHT) + .clickable(openDayAction(context, week.days[col])), + ) {} } col += 1 } @@ -404,14 +417,20 @@ private fun SpanBar(event: EventInstance, dark: Boolean, width: Dp) { @Composable private fun OverflowRow(week: MonthWeek, colW: Dp) { + val context = LocalContext.current Row(modifier = GlanceModifier.fillMaxWidth()) { week.days.forEachIndexed { col, date -> val shownSpans = week.spans.count { col in it.startCol..it.endCol && it.lane < MAX_LANES } val freeSlots = (MAX_LANES - shownSpans).coerceAtLeast(0) val timedShown = minOf(freeSlots, week.timedByDay[date].orEmpty().size) val hidden = (week.countByDay[date] ?: 0) - shownSpans - timedShown + // The overflow row is part of the day column too: tapping it (whether + // it shows "+N" or is blank) opens that day, same as the app. Box( - modifier = GlanceModifier.width(colW).height(LANE_HEIGHT), + modifier = GlanceModifier + .width(colW) + .height(LANE_HEIGHT) + .clickable(openDayAction(context, date)), contentAlignment = Alignment.CenterStart, ) { if (hidden > 0) { -- 2.49.1 From e5aa90bdd6f315fbd94eb5fb348ecbed083cf264 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Wed, 1 Jul 2026 12:37:42 +0200 Subject: [PATCH 4/4] docs(changelog): note month-widget day tap opens that day (#18) Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e20ae8..ab6a6ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,9 +18,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 widget opens the app on the month view, and tapping the "Upcoming" title on the agenda widget opens it on your default view — so there's a one-tap way back into the app that lands where you'd expect, instead of only through a day - or event. The month widget's "today" button now snaps the grid back to the - current month in place. Thanks to @rgz46vic and @ptab for the suggestions - ([#18], [#20]). + or event. On the month widget, tapping anywhere on a day — not just the small + date number — now opens that day, and the "today" button snaps the grid back + to the current month in place. Thanks to @rgz46vic and @ptab for the + suggestions ([#18], [#20]). ### Fixed - Month widget arrows and "today" button work again. On release builds the -- 2.49.1