fix(intents): register calendar intent filters so Calendula can be the default calendar (#9)
All checks were successful
CI / ci (pull_request) Successful in 7m51s

Calendula didn't declare the intent filters launchers and the system use for
calendar actions, so it never appeared in the "default calendar app" chooser —
on every platform, not just GrapheneOS (issue #9). Android exposes no API for an
app to set itself default, so registering these filters is the only way users can
pick it from the system picker.

Adds to MainActivity:
- MAIN + APP_CALENDAR — the "open the calendar app" action the OS/launchers use.
- VIEW on content://com.android.calendar/time/<epochMillis> and the time/epoch
  mime type — a launcher/clock date tap. The provider's time Uri is parsed into a
  LocalDate and opened on the day view, rooted over the default home view (a new
  sourceless WidgetNavRequest.OpenDate). The .ics import path now ignores the
  calendar provider host so a date tap isn't mistaken for a file to import.

Bumps to 2.11.1.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-28 12:15:56 +02:00
parent c7ef52e426
commit b6a45b7264
7 changed files with 87 additions and 5 deletions

View File

@@ -35,6 +35,9 @@ import de.jeanlucmakiola.calendula.ui.crash.submitCrashReport
import de.jeanlucmakiola.calendula.ui.settings.SettingsViewModel
import de.jeanlucmakiola.calendula.ui.theme.CalendulaTheme
import kotlinx.datetime.LocalDate
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toLocalDateTime
import kotlin.time.Instant
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@@ -156,10 +159,33 @@ class MainActivity : AppCompatActivity() {
Intent.ACTION_SEND -> IntentCompat.getParcelableExtra(this, Intent.EXTRA_STREAM, Uri::class.java)
else -> null
} ?: return null
// The calendar "view time" Uri (a date tap) is also ACTION_VIEW/content;
// it's a navigation, not a file to import, so [navRequestOrNull] owns it.
if (uri.host == CALENDAR_PROVIDER_HOST) return null
return uri.takeIf { it.scheme == "content" || it.scheme == "file" }
}
/**
* The date a launcher/clock date tap points at, parsed from the AOSP calendar
* "view time" intent: ACTION_VIEW on `content://com.android.calendar/time/
* <epochMillis>`. Null for any other intent. The matching manifest filter is
* what lets users pick Calendula from the system calendar chooser (issue #9).
*/
private fun Intent.calendarTimeDateOrNull(): LocalDate? {
if (action != Intent.ACTION_VIEW) return null
val uri = data ?: return null
if (uri.host != CALENDAR_PROVIDER_HOST) return null
val segments = uri.pathSegments
if (segments.firstOrNull() != "time") return null
val millis = segments.getOrNull(1)?.toLongOrNull() ?: return null
return Instant.fromEpochMilliseconds(millis)
.toLocalDateTime(TimeZone.currentSystemDefault()).date
}
private fun Intent.navRequestOrNull(): WidgetNavRequest? {
// 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) }
val source = sourceViewOrNull()
val eventId = getLongExtra(EXTRA_EVENT_ID, -1L)
return when {
@@ -201,6 +227,10 @@ class MainActivity : AppCompatActivity() {
}
companion object {
// The calendar provider's authority/host. A date tap arrives as
// ACTION_VIEW on content://com.android.calendar/time/<epochMillis>.
private const val CALENDAR_PROVIDER_HOST = "com.android.calendar"
private const val EXTRA_EVENT_ID = "de.jeanlucmakiola.calendula.extra.EVENT_ID"
private const val EXTRA_BEGIN_MILLIS = "de.jeanlucmakiola.calendula.extra.BEGIN"
private const val EXTRA_END_MILLIS = "de.jeanlucmakiola.calendula.extra.END"

View File

@@ -196,7 +196,9 @@ fun CalendarHost(
dismissCoveringOverlays()
createDateIso = null
pendingDayIso = req.dateIso
viewStack = viewBaseStack(defaultView, req.source).drillToDay()
// No widget source (an external date tap) roots over the default
// home view, so backing out of the day returns home then exits.
viewStack = viewBaseStack(defaultView, req.source ?: defaultView).drillToDay()
onWidgetNavConsumed()
}
is WidgetNavRequest.OpenEvent -> {

View File

@@ -13,8 +13,13 @@ import de.jeanlucmakiola.calendula.ui.common.CalendarView
* detail-key channel and leave the base view untouched.)
*/
sealed interface WidgetNavRequest {
/** Open the day view anchored on [dateIso] (an ISO `yyyy-MM-dd` date), over [source]. */
data class OpenDate(val dateIso: String, val source: CalendarView) : WidgetNavRequest
/**
* Open the day view anchored on [dateIso] (an ISO `yyyy-MM-dd` date), over
* [source]. A null [source] means the request came from outside the app (a
* launcher/clock date tap, issue #9) rather than a widget, so it roots over
* the default home view instead of a widget's view.
*/
data class OpenDate(val dateIso: String, val source: CalendarView?) : WidgetNavRequest
/** Open one occurrence's detail (an agenda-widget event tap), over [source]. */
data class OpenEvent(