# Calendula - Plan 05: ICS Export (v2.7, Branch 1 von 2) > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Die Schreib-Hälfte des `.ics`-Themas. Calendula serialisiert eigene Events nach RFC 5545 — als Einzel-Event (Share-Sheet) und als Ganz-Kalender-Backup (SAF-Datei). Damit existiert für gerätelokale Kalender (`ACCOUNT_TYPE_LOCAL`) zum ersten Mal ein Backup; ohne Sync ist ein verlorenes Gerät sonst Totalverlust. Diese Branch baut **nur Export**; Import (Parser, Restore, Open-into-form) folgt in Branch 2 (`feat/ics-import`), beide landen zusammen in **einem** Release v2.7.0 — es gibt also keine Zwischenversion, die UIDs schreibt, ohne sie je zu lesen. `./gradlew lint test assembleDebug` bleibt grün; Release erst nach On-Device-Review (gemeinsam mit Branch 2). **Architecture:** Neue reine-Kotlin-Engine `domain/ics/` — kein `CalendarContract`, keine Android-Deps, voll JVM-testbar. Kern ist `IcsWriter` (nimmt eine Liste eigener Event-Modelle + Kalender-Metadaten, gibt einen `VCALENDAR`-String zurück). Keine ICS-Library: wir bleiben auf `kotlinx-datetime` (kein `java.time`-Desugaring) und hand-rollen wie schon bei RRULE in `Recurrence.kt`. Das Schreiben in eine SAF-Datei / das Share-Intent liegt in einer dünnen Android-Schicht (`data/ics/IcsExporter` o. ä.), die Provider-Reads → Domain-Modelle → `IcsWriter` → `OutputStream` verdrahtet. **Recherche-Befunde (Codebase, 2026-06-18):** 1. **Keine ICS-Library, kein `java.time`-Desugaring** — Stack ist `kotlinx-datetime` + `kotlin.time.Instant`. RRULE wird bereits in `domain/Recurrence.kt` von Hand geparst/gerendert (inkl. der sorgfältigen `UNTIL`/DST-Korrektur). `IcsWriter` reiht sich in genau diese Kultur ein und nutzt `SimpleRecurrence.toRRule()` direkt. 2. **Kein UID-Handling.** `Events.UID_2445` wird heute nirgends gelesen oder geschrieben. Für idempotenten Restore in Branch 2 muss Import auf UID matchen — ohne UID verdoppelt ein erneuter Import alles. Darum schreibt **diese** Branch bereits UID bei jedem Insert (Vorarbeit; ohne Import noch unsichtbar, zahlt sich erst in Branch 2 aus — beide im selben Release). 3. **Zeitzonen werden gespeichert, aber nie vom Nutzer gewählt.** Lesen/Anzeige: `EVENT_TIMEZONE` landet in `EventDetail.eventTimezone`, das Detail zeigt ein Fremdzonen-Label nur bei Abweichung vom Gerät (`foreignTimeZoneLabel`). Schreiben (`EventWriteMapper.toWriteTimes`): all-day → UTC-Mitternachten, `EVENT_TIMEZONE="UTC"`; getimt → `EVENT_TIMEZONE = zone.id`, und alle Caller übergeben `currentSystemDefault()` (kein Zonen-Feld im Formular). Jedes selbst erstellte Event trägt also die Gerätezone zum Erstellzeitpunkt; eingesynkte Events behalten ihre Originalzone. **Leitentscheidungen:** 1. **Zeitzonen-Regel beim Schreiben (fallbasiert):** - **All-day** → `DTSTART;VALUE=DATE:YYYYMMDD`, `DTEND` exklusiv (Tag-danach). Keine Zone — trivial korrekt. - **Getimt, nicht wiederkehrend** → UTC-Instant `…T…Z`. Ein Instant ist ein Instant; die Anzeige rechnet beim Import wieder in die Gerätezone. Verlustfrei. - **Getimt, wiederkehrend** → `DTSTART;TZID=:`. Eine Serie muss an der **Wandzeit** verankert sein, sonst driftet ein „wöchentlich 9 Uhr" über die nächste DST-Grenze um eine Stunde. Die Zone liegt bereits in `EVENT_TIMEZONE` vor; die lokale Wandzeit ist eine `kotlinx-datetime`-Konversion (Instant → LocalDateTime in der Zone). - **`VTIMEZONE`-Blöcke werden bewusst NICHT emittiert.** Beim eigenen Round-Trip löst Branch-2-Import `TZID` gegen die OS-tz-Datenbank auf (`kotlinx-datetime`/`java.time` kennen jede IANA-Id). Technisch nicht RFC-konform; einziger Preis sind strenge Fremd-Parser ohne tz-DB — als bekannte Lücke dokumentiert (Skip-and-report-Territorium des Imports), kein Blocker. Voll-`VTIMEZONE` ist „später, falls nötig". 2. **UID bei jedem Insert.** `insertEvent` schreibt fortan `Events.UID_2445` (z. B. `@calendula`). Bestehende Events ohne UID exportieren wir mit einer **deterministischen, stabilen** Fallback-UID, abgeleitet aus `event-id + DTSTART` (`-@calendula`), damit derselbe Bestand über mehrere Backups dieselbe UID behält und Branch-2-Restore nicht verdoppelt. Bestehende Rows werden **nicht** rückwirkend gestempelt (kein Migrations-Sweep über fremde Kalender). 3. **Manueller Export, kein Background.** Backup via `ACTION_CREATE_DOCUMENT` (SAF, MIME `text/calendar`, Default-Name `calendula-backup-.ics`); Einzel-Event-Share via `ACTION_SEND` mit einem `FileProvider`-Cache-File (`text/calendar`). Kein WorkManager, kein geplantes/automatisches Backup (passt zum „kein Hintergrunddienst"-Ethos; Auto-Backup bleibt explizit Roadmap-`later`). 4. **Backup-Layout: eine kombinierte `VCALENDAR`-Datei** über alle gerätelokalen (beschreibbaren) Kalender. Pro Event ein `VEVENT`; die Kalender-Zugehörigkeit reist als `X-WR-CALNAME` / `CATEGORIES` o. ä. mit, damit Branch-2-Restore wieder auffächern oder per Ziel-Picker einsortieren kann. Eine Datei ist einfacher zu teilen/abzulegen als n Dateien. *Offen, vor dem Backup-Task zu fixieren:* exaktes Property fürs Kalender-Mapping (`X-WR-CALNAME` pro `VCALENDAR` erlaubt nur einen Namen; für mehrere Kalender in einer Datei brauchen wir ein Pro-`VEVENT`-Property wie `X-CALENDULA-CALENDAR` oder `CATEGORIES`). 5. **Feldumfang = was Calendula modelliert.** `IcsWriter` serialisiert genau die gelesenen Felder: `SUMMARY`, `DTSTART`/`DTEND` (Regel #1), `LOCATION`, `DESCRIPTION`, `RRULE` (über `toRRule`), `VALARM` aus den Remindern (DISPLAY, `TRIGGER` = `-PTM`), `STATUS` (CONFIRMED/TENTATIVE/CANCELLED), `TRANSP` (Free→TRANSPARENT/Busy→OPAQUE), `UID`, `DTSTAMP`. Felder ohne sauberes Modell (Attendees, RECURRENCE-ID- Ausnahmen) bleiben **vorerst weg** — Export erzeugt nichts, was Import in Branch 2 nicht auch wieder lesen kann. 6. **Korrekte RFC-5545-Mechanik:** Zeilen-Folding bei >75 Oktett (CRLF + Space-Fortsetzung), Text-Escaping (`\` `;` `,` `\n`), CRLF-Zeilenenden, `PRODID`/`VERSION:2.0`-Header. Eine reine, einzeln getestete Hilfsschicht (`IcsLine`/`fold`/`escapeText`), nicht ad hoc im Writer verstreut. --- ## Tasks **Domain-Engine (`domain/ics/`, reine Kotlin, JVM-Tests):** - [x] `IcsText`: `escapeText`, `foldLine` (75-Oktett, CRLF+Space) + Test (`IcsTextTest`). Wert-Helfer (Instant→`…T…Z`, Wandzeit→`TZID`, LocalDate→`VALUE=DATE`) leben als private Helfer in `IcsWriter`. - [x] `IcsEvent`-Eingabemodell (reine Kotlin: summary, start/end als Instant + isAllDay + zoneId, recurrenceRule?, location, description, reminderMinutes, status, availability, uid, calendarName) — entkoppelt vom Provider-Modell - [x] `IcsWriter.writeCalendar(events, dtStamp)` → String: Header, pro Event `VEVENT` nach Entscheidung #5, Zeitzonen-Regel #1, `VALARM`; JVM-Test `IcsWriterTest` (all-day, getimt, wiederkehrend+TZID, unbekannte Zone, Reminder, Escaping) - [x] UID-Ableitung `deriveIcsUid` (`uid ?: "-@calendula"`) + Stabilitätstest **Provider → Domain (`data/calendar/IcsExportMapper.kt`):** - [x] Mapper Provider-Row → `IcsEvent` (`ColumnReader.toIcsEvent`) inkl. DURATION→DTEND-Rekonstruktion (`parseRfc2445DurationMillis`), `EventExportProjection`; Datasource-Methode `exportableEvents()` + Repository `exportEvents()`; Test `IcsExportMapperTest` - [x] `insertEvent` schreibt `Events.UID_2445` (`UUID@calendula`) bei jedem Create **Android-Export-Schicht:** - [x] `data/ics/IcsExporter`: `writeDocument(uri)` (SAF) + `stageShareFile` (FileProvider-Cache) als UTF-8 - [x] Einzel-Event-Share: Share-Action im Event-Detail → `IcsWriter` für ein Event (one-off) → Cache-File über `FileProvider` → `ACTION_SEND` - [x] Ganz-Kalender-Backup: „Export as .ics file" in Settings → Calendars → `ACTION_CREATE_DOCUMENT` → in den URI streamen; Ergebnis-Snackbar (Plural „Exported N events") - [x] `FileProvider` + `file_paths.xml` im Manifest (Cache-Dir für Shares) - [x] Strings DE+EN: Share-Label/Chooser/Fehler, Backup-Sektion/Aktion/ Fehler + Plural, dateierter Default-Name **Abschluss:** - [ ] `./gradlew lint test assembleDebug` grün ← **nächster Schritt (Test)** - [x] CHANGELOG (`[Unreleased]`) ergänzt - [ ] On-Device-Review; **kein** Tag/Release vor Review und vor Merge von Branch 2 (`feat/ics-import`) **Offene Detail-Calls (vor Review klären, nicht-blockierend):** - Kalender→Event-Mapping nutzt das per-`VEVENT`-Property `X-CALENDULA-CALENDAR` (statt `X-WR-CALNAME`), damit eine kombinierte Datei mehrere Kalender trägt. - Backup = **eine** kombinierte `VCALENDAR`-Datei über alle lokalen Kalender. - EXDATE / `RECURRENCE-ID`-Ausnahmen werden beim Export ausgelassen (`ORIGINAL_ID IS NULL`) — dokumentierter v1-Grenzfall, Import lässt sie auch aus.