# Calendula - Plan 06: ICS Import (v2.7, Branch 2 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 Lese-Hälfte des `.ics`-Themas, aufgesetzt auf den Writer aus Branch 1 (`feat/ics-export`, gemerged in `release/v2.7.0`). Calendula parst RFC-5545-Dateien und führt sie über zwei Wege ein: eine einzelne `VEVENT` öffnet das vorausgefüllte Erstellen-Formular (Review vor dem Speichern), eine Datei mit vielen Events geht in einen Bulk-Import mit Ziel-Kalender-Auswahl und Ergebnis-Report. Damit schließt sich der Backup→Restore-Kreis für lokale Kalender. Beide Branches landen in **einem** Release v2.7.0. `./gradlew lint test assembleDebug` bleibt grün; Release erst nach On-Device-Review. **Architecture:** `IcsParser` lebt rein in `domain/ics/` neben `IcsWriter` — kein Android, JVM-testbar, symmetrisch zum Writer. Ausgabe ist ein `IcsParseResult` (`events: List` + `warnings: List`). `ParsedIcsEvent` ist die Parse-Variante von `IcsEvent` (gleiche Felder, aber `uid: String?` — eine eingelesene `VEVENT` kann ohne UID kommen). Zwei Adapter: `ParsedIcsEvent.toEventForm(zone)` (Einzel-Öffnen → `EventForm`) und eine Repository-Methode `importEvents(targetCalendarId, events)` → `ImportSummary` (Bulk). Datei-IO (`Uri` lesen, Intent) liegt in `data/ics/` bzw. der Activity/Compose-Schicht; Routing 1-vs-viele entscheidet anhand der geparsten Event-Anzahl. **Liberal-in/strict-out (Leitprinzip):** unbekannte Properties, fremde `VTIMEZONE`-Blöcke und `RECURRENCE-ID`-Ausnahmen werden **übersprungen und im Report vermerkt**, nie still verschluckt; ein einzelnes kaputtes `VEVENT` lässt den Rest der Datei durch. **Leitentscheidungen:** 1. **Parse-Mechanik (Umkehr von `IcsText`):** zuerst **Unfolding** (CRLF + Space/Tab → wegfalten), dann pro Zeile `NAME[;params]:value` zerlegen, TEXT-Werte **unescapen** (`\\` `\;` `\,` `\n`). Eine reine, einzeln getestete Schicht (`IcsLineParser`), nicht ad hoc im Walker. 2. **Datum/Zeit-Parsing (Umkehr der Writer-Zeitzonenregel):** - `VALUE=DATE` (`YYYYMMDD`) → all-day, Instant auf UTC-Mitternacht (wie der Provider all-day speichert), exklusives `DTEND` bleibt exklusiv. - `…T…Z` → UTC-Instant. - `…T…` mit `TZID=` → lokale Wandzeit in der Zone, aufgelöst gegen die **OS-tz-Datenbank** (`TimeZone.of`); unbekannte/fehlende `TZID` → Gerätezone als Fallback (+ Warnung). - Kein `VTIMEZONE`-Parsing — `TZID` wird gegen die OS-DB aufgelöst (s. Branch-1 Entscheidung); ein `VTIMEZONE`-Block wird übersprungen (Warnung nur, wenn seine `TZID` nicht in der OS-DB ist). 3. **Routing 1-vs-viele:** genau **eine** `VEVENT` → vorausgefülltes Erstellen-Formular (`ParsedIcsEvent.toEventForm`, `calendarId=null` → Formular wählt wie gehabt den zuletzt genutzten Kalender vor). **Mehr als eine** → Bulk-Import-Screen (Ziel-Kalender-Picker, nur beschreibbare). Leere Datei → freundlicher „nichts gefunden"-Hinweis. 4. **UID-Dedup beim Bulk-Import:** vor dem Insert die vorhandenen `Events.UID_2445` des Ziel-Kalenders lesen; eine eingelesene UID, die schon existiert, wird **übersprungen** (gezählt als „bereits vorhanden"). v1: skip-not-update — kein Überschreiben, das hält den Restore idempotent und verlustfrei. Events ohne UID bekommen beim Insert eine frische (`UUID@calendula`, wie `insertEvent`). 5. **Empfang via Intent:** Manifest-`ACTION_VIEW`/`SEND` mit MIME `text/calendar` (+ `.ics`-Pfadmuster für `file`/`content`-Schemes). `MainActivity` (`singleTop`, wie beim Reminder-Tap) liest den `Uri`, parst, und reicht das Ergebnis als Compose-State an `CalendarHost` (gleiches Key-Muster wie der Notification-Deep-Link). 6. **Reminder/Status/Transp zurück:** `VALARM` `TRIGGER` (negatives `-PT…`, `PT0…`) → Lead-Minuten; `STATUS`/`TRANSP` → `EventStatus`/`Availability`. `DURATION`-statt-`DTEND` über `parseRfc2445DurationMillis` (existiert aus Branch 1). Attendees werden **nicht** importiert (kein Modell; Warnung wenn vorhanden). **Recherche-Befunde (Codebase, 2026-06-18 — aus Branch 1):** - `IcsText.escapeText`/`foldLine` + `IcsWriter` existieren; Parser spiegelt sie. - `parseRfc2445DurationMillis` (in `IcsExportMapper.kt`) parst die Provider-`DURATION`-Formen inkl. des nicht-standardkonformen `PS`. - `EventForm` (Domain): Zeiten als `LocalDateTime` in Gerätezone, `calendarId` nullable; das Formular wählt bei `null` den zuletzt genutzten Kalender vor. - `insertEvent` schreibt bereits `Events.UID_2445`; für den Import muss eine **vorgegebene** UID durchgereicht werden (Insert-Variante / Parameter). --- ## Tasks **Parser-Engine (`domain/ics/`, reine Kotlin, JVM-Tests):** - [x] `IcsText`: `unescapeText` + `unfold(lines)` ergänzen (+ Test) - [x] `IcsLineParser`: `NAME;PARAM=v;PARAM=v:VALUE` → (name, params-map, value); Param-Werte ggf. in Quotes; Test (Kantenfälle: Doppelpunkt im Wert, gequotete Params) - [x] `ParsedIcsEvent` (wie `IcsEvent`, aber `uid: String?`) + Datum/Zeit-Parser (`VALUE=DATE` / `…Z` / `TZID` → Instant + isAllDay + zoneId) - [x] `IcsParser.parse(text)` → `IcsParseResult(events, warnings)`: VCALENDAR/ VEVENT-Walk, skip-and-report für `RECURRENCE-ID`/unbekannte `VTIMEZONE`/ Attendees; ein defektes VEVENT killt nicht den Rest. **Round-trip-Test** gegen `IcsWriter`-Ausgabe (all-day, getimt, wiederkehrend+TZID, Reminder) + Fremd-Quirks (gefaltete Zeilen, fehlende UID, `PT…`-Trigger) **Datenschicht (`data/calendar/` + `data/ics/`):** - [x] `ParsedIcsEvent.toEventForm(zone)` (Einzel-Öffnen → `EventForm`); Test - [x] Datasource: `existingUids(calendarId)` (Query `Events.UID_2445`) + `insertImported(event, calendarId)` (Insert mit vorgegebener/erzeugter UID) - [x] Repository `importEvents(targetCalendarId, events)` → `ImportSummary` (imported / skippedDuplicate / skippedUnsupported); UID-Dedup; Test mit Fake-Datasource - [x] `IcsImporter` (`data/ics/`): `Uri` → Text lesen (UTF-8, `contentResolver`) **Intent + Routing:** - [x] Manifest: `ACTION_VIEW`/`ACTION_SEND`, MIME `text/calendar`, `.ics`- Pfadmuster (`file`/`content`); `MainActivity` parst eingehenden `Uri` - [x] Routing in `CalendarHost`: 1 Event → Erstellen-Formular vorausgefüllt; >1 → Bulk-Import-Screen; 0 → Hinweis **UI:** - [x] Bulk-Import-Screen: Ziel-Kalender-Picker (OptionCard, nur beschreibbare), Anzahl/Vorschau, Import-Button → `ImportSummary` als Ergebnis - [x] Einzel-Öffnen: `EventEditScreen` mit vorausgefülltem Formular (neuer Prefill-Pfad im `EventEditViewModel`, ohne `eventId`) - [x] Strings DE+EN: Import-Titel, Ziel-Auswahl, Ergebnis-Plurals (importiert / übersprungen-vorhanden / übersprungen-nicht-unterstützt), leere-Datei-Hinweis **Abschluss:** - [x] `./gradlew lint test assembleDebug` grün - [ ] CHANGELOG done; ROADMAP/STATE pending; v2.7 cut **erst** wenn beide Branches gemerged sind und On-Device-Review durch ist