release: v2.7.0 — ICS export & import #7

Merged
makiolaj merged 7 commits from release/v2.7.0 into main 2026-06-18 14:26:53 +00:00
Owner

Cuts v2.7.0 (versionCode 20700).

Added

  • Share a single event as an .ics file from the event detail screen.
  • Back up local (device-only) calendars: Settings → Calendars → Export as .ics.
  • Open / receive .ics files: single event → prefilled create form; multi-event file → dedup-aware bulk import.

Fixed

  • Single-day all-day events (e.g. birthdays) no longer appear on the following day in the day/week/month views or the event detail screen — the all-day date range was being read in the device time zone instead of UTC.
  • Release (R8) build no longer crashes at startup: added a keep rule for the Room DB class the home-screen widget framework needs.

Verified on-device (Pixel 10). Tag v2.7.0 will be pushed after merge to trigger the F-Droid/Gitea release.

🤖 Generated with Claude Code

Cuts **v2.7.0** (`versionCode 20700`). ## Added - Share a single event as an `.ics` file from the event detail screen. - Back up local (device-only) calendars: Settings → Calendars → Export as `.ics`. - Open / receive `.ics` files: single event → prefilled create form; multi-event file → dedup-aware bulk import. ## Fixed - Single-day all-day events (e.g. birthdays) no longer appear on the following day in the day/week/month views or the event detail screen — the all-day date range was being read in the device time zone instead of UTC. - Release (R8) build no longer crashes at startup: added a keep rule for the Room DB class the home-screen widget framework needs. Verified on-device (Pixel 10). Tag `v2.7.0` will be pushed after merge to trigger the F-Droid/Gitea release. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
makiolaj added 7 commits 2026-06-18 14:26:25 +00:00
Branch 1 of 2 for v2.7 (the .ics topic). Adds the write side of a
hand-rolled RFC 5545 engine (zero deps, stays on kotlinx-datetime):

- domain/ics: IcsText (escape + 75-octet folding), IcsEvent model,
  IcsWriter.writeCalendar. Timezone rule: all-day VALUE=DATE, one-off
  timed UTC Z, recurring timed TZID-labelled from EVENT_TIMEZONE (no
  VTIMEZONE — import resolves TZID against the OS tz db).
- Single-event share from the detail screen (FileProvider + ACTION_SEND).
- Whole-calendar backup of the writable local calendars to a SAF file
  (Settings -> Calendars -> Export as .ics), one combined VCALENDAR.
- insertEvent now writes Events.UID_2445; legacy rows fall back to a
  stable synthesised UID at export time so a later restore won't dupe.
- EXDATE / RECURRENCE-ID overrides are deliberately skipped this pass
  (documented v1 limit; import will skip them too).

Engine + mapper unit-tested. Import (Branch 2, feat/ics-import) ships in
the same v2.7 release; no tag until both land + on-device review.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
v2.7 Branch 1 of 2: .ics export — single-event share + whole-calendar backup of local calendars. Import (feat/ics-import) lands next in the same release.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
All-day events live at UTC midnights with an exclusive end, but coversDay
sliced each day in the device timezone. East of UTC the exclusive end
landed a few hours into the next local day, so a one-day all-day event
(e.g. a birthday) rendered on two days in the day/week/month views — while
the detail and edit screens, which work in UTC, showed it correctly.

Compare all-day coverage in UTC and step the exclusive end back to the
last covered day, mirroring the detail/edit views.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
v2.7 Branch 2 (core, no UI yet). The read side of the .ics engine:

- domain/ics: IcsParser (inverse of IcsWriter) — unfold/unescape/param
  parsing, VALUE=DATE / UTC-Z / TZID date handling resolved against the OS
  tz db, VEVENT walk → ParsedIcsEvent + typed warnings. Liberal-in/
  strict-out: a malformed VEVENT is skipped, RECURRENCE-ID overrides /
  attendees / unresolved TZIDs are reported, not silently dropped.
- Promoted parseRfc2445DurationMillis into domain/ics (shared by writer-
  side mapper and parser); IcsDuration + test.
- Datasource existingUids()/insertImportedEvent(); repository
  importEvents() with UID dedup (skip known UIDs → idempotent restore) →
  IcsImportSummary. IcsImporter reads a Uri's text.
- ParsedIcsEvent.toEventForm() for the single-event "open into the create
  form" path.

Parser round-trips against IcsWriter; dedup + form-adapter unit-tested.
Intent filter, routing and import UI land in the next commit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Completes v2.7 Branch 2. Wires the import core into the app:

- Manifest ACTION_VIEW/SEND for text/calendar; MainActivity parses the
  incoming Uri (content/file only, so calendula:// deep-links don't match)
  and routes it through RootScreen → CalendarHost like the other one-shot
  intents.
- ImportViewModel reads + parses the file and routes by count: one event →
  the prefilled create form for review (EventEditViewModel.openImported,
  which freezes the reminder default so the file's reminders win); many →
  ImportScreen with a writable-calendar picker, then a bulk import (UID
  dedup) and a result summary.
- ImportScreen also surfaces parser warnings (skipped recurrence overrides,
  ignored attendees, unknown-timezone fallback). Strings EN+DE.

Package is ui.imports (not ui.import — Java keyword). lint + test +
assembleDebug green. No v2.7 tag until on-device review.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The minified release build crashed on every launch before any UI:

  Unable to get provider androidx.startup.InitializationProvider:
    Failed to create an instance of androidx.work.impl.WorkDatabase

The home-screen widgets use Glance, which pulls in WorkManager and its
transitive Room database (room-runtime 2.2.5). Room 2.2.5's bundled keep
rule is `-keep class * extends androidx.room.RoomDatabase` — it keeps the
class but not its constructor. Under R8 full mode (AGP 9) the generated
WorkDatabase_Impl was reduced to a non-instantiable class, so Room's
reflective newInstance() threw InstantiationException at startup.

Add `-keep class * extends androidx.room.RoomDatabase { *; }` so the
generated *_Impl classes keep their constructors. Verified against the
rebuilt release APK: WorkDatabase_Impl is now PUBLIC FINAL with its
<init> present.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
release: cut v2.7.0 — ICS export & import (.ics share, backup, open/receive)
All checks were successful
CI / ci (push) Successful in 5m48s
Release — F-Droid repo + Gitea release / ci (push) Successful in 7m40s
Release — F-Droid repo + Gitea release / gitea-release (push) Successful in 5s
Release — F-Droid repo + Gitea release / build-and-deploy (push) Successful in 5m44s
d20d446cbe
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
makiolaj merged commit 290a905f8b into main 2026-06-18 14:26:53 +00:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: makiolaj/calendula#7