fix(intents): accept item-typed INSERT / INSERT_OR_EDIT for "Add to calendar" (#74)

Other apps' "Add to calendar" / "Save to calendar" actions commonly fire the
canonical insert intent — ACTION_INSERT with setType("vnd.android.cursor.item/
event") — the singular *item* MIME type (Android's own docs example; used by
e.g. DB Navigator). Calendula advertised INSERT only on the *dir* MIME type, so
it never matched: absent from the chooser, and a silent no-op when it was the
only calendar app installed.

Add the item-typed INSERT to the events filter, plus ACTION_INSERT_OR_EDIT (the
third "add to calendar" action AOSP and Google Calendar register). The runtime
parser already treats any ACTION_INSERT as create; teach insertFormOrNull /
editEventKeyOrNull about INSERT_OR_EDIT so an id-less one is a create and an
id-carrying one opens the edit form.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-24 17:37:59 +02:00
parent 7549c8fe04
commit 9e28fa4981
3 changed files with 43 additions and 14 deletions

View File

@@ -32,6 +32,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
Now a bigger widget gets bigger, more readable type and roomier rows, while the
default size looks exactly as before — no new setting; it follows the size you
already chose ([#51]).
- Calendula now appears under other apps' "Add to calendar" / "Save to calendar"
actions. Some apps (e.g. DB Navigator) fire the widely-used "insert event"
intent with the singular `vnd.android.cursor.item/event` type, which Calendula
didn't advertise — so it was left out of the chooser, and if it was your only
calendar app the save silently did nothing. It now accepts that form, plus the
`INSERT_OR_EDIT` action, and opens the new event prefilled for review ([#74]).
## [2.16.0] — 2026-07-17
@@ -1079,3 +1085,4 @@ automatically, with zero telemetry and no internet permission.
[#53]: https://codeberg.org/jlmakiola/calendula/issues/53
[#60]: https://codeberg.org/jlmakiola/calendula/issues/60
[#65]: https://codeberg.org/jlmakiola/calendula/issues/65
[#74]: https://codeberg.org/jlmakiola/calendula/issues/74

View File

@@ -123,11 +123,13 @@
<!-- Let another app or widget (e.g. the Todo Agenda widget) launch us
to create a new event, the way the AOSP calendar accepts it:
ACTION_INSERT on the events dir mime type, carrying the new
ACTION_INSERT on the events *dir* mime type, carrying the new
event's fields as CalendarContract extras
(MainActivity.insertFormOrNull, issue #30). ACTION_EDIT on the
dir mime is AOSP's "edit a new event" — i.e. create — so it maps
to the same prefilled create form. -->
to the same prefilled create form. (The far more common *item*-
typed INSERT — the form the Android docs' example and apps like
DB Navigator use — is the item filter below, issue #74.) -->
<intent-filter>
<action android:name="android.intent.action.INSERT" />
<action android:name="android.intent.action.EDIT" />
@@ -135,14 +137,31 @@
<data android:mimeType="vnd.android.cursor.dir/event" />
</intent-filter>
<!-- Edit an existing event another app/assistant/widget points at:
ACTION_EDIT on content://com.android.calendar/events/<id>, the way
AOSP fires it. Opens the occurrence in the edit form (not the
read-only detail — that's the VIEW filter above). Matched by the
provider's item MIME type, like the VIEW filter. The occurrence's
times ride as EXTRA_EVENT_BEGIN_TIME / EXTRA_EVENT_END_TIME when
supplied (MainActivity.editEventKeyOrNull). -->
<!-- Create or edit an event another app/assistant/widget points at,
addressed by the provider's *item* MIME type. Three actions share
this filter, told apart at runtime by the intent's data:
• ACTION_INSERT — create. This is the form the Android docs'
"insert an event" example and many apps use
(setType("vnd.android.cursor.item/event")), e.g. DB Navigator's
"Save to calendar". The dir-typed filter above alone missed it,
so Calendula never showed in the chooser — and, when it was the
only calendar app, the intent resolved to nothing (issue #74).
• ACTION_INSERT_OR_EDIT — the third "add to calendar" action AOSP
and Google Calendar register; a create, or an edit when it
carries an event id.
• ACTION_EDIT — edit the existing event at
content://com.android.calendar/events/<id> (an id-less EDIT is a
create). Opens the occurrence in the edit form, not the
read-only detail — that's the VIEW filter above.
Create fields ride as CalendarContract extras; an edit's
occurrence times ride as EXTRA_EVENT_BEGIN_TIME /
EXTRA_EVENT_END_TIME when supplied (MainActivity.insertFormOrNull
/ editEventKeyOrNull). -->
<intent-filter>
<action android:name="android.intent.action.INSERT" />
<action android:name="android.intent.action.INSERT_OR_EDIT" />
<action android:name="android.intent.action.EDIT" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="vnd.android.cursor.item/event" />

View File

@@ -222,10 +222,12 @@ class MainActivity : AppCompatActivity() {
* [buildInsertEventForm].
*/
private fun Intent.insertFormOrNull(): EventForm? {
// ACTION_EDIT on an existing event routes to the edit form instead
// ([editEventKeyOrNull]); only an id-less EDIT is a create.
// ACTION_EDIT / ACTION_INSERT_OR_EDIT on an existing event route to the
// edit form instead ([editEventKeyOrNull]); an id-less one is a create,
// as is any plain ACTION_INSERT.
val isCreate = action == Intent.ACTION_INSERT ||
(action == Intent.ACTION_EDIT && editEventKeyOrNull() == null)
((action == Intent.ACTION_EDIT || action == Intent.ACTION_INSERT_OR_EDIT) &&
editEventKeyOrNull() == null)
if (!isCreate) return null
return buildInsertEventForm(
beginMillis = longExtraOrNull(CalendarContract.EXTRA_EVENT_BEGIN_TIME),
@@ -344,10 +346,11 @@ class MainActivity : AppCompatActivity() {
* occurrence's times as `EXTRA_EVENT_BEGIN_TIME` / `EXTRA_EVENT_END_TIME`
* when it has them, otherwise we carry [NO_OCCURRENCE_TIME] and
* [EventEditViewModel.openForEdit] falls back to the event row's own
* DTSTART/DTEND. An id-less `ACTION_EDIT` is a create instead ([insertFormOrNull]).
* DTSTART/DTEND. An id-less `ACTION_EDIT` — or `ACTION_INSERT_OR_EDIT`, which
* some apps fire — is a create instead ([insertFormOrNull]).
*/
private fun Intent.editEventKeyOrNull(): LongArray? {
if (action != Intent.ACTION_EDIT) return null
if (action != Intent.ACTION_EDIT && action != Intent.ACTION_INSERT_OR_EDIT) return null
val uri = data ?: return null
if (uri.host != CALENDAR_PROVIDER_HOST) return null
val segments = uri.pathSegments