# NCal A native Android app that talks **directly to Nextcloud's CalDAV endpoint** — no DAVx⁵, no separate tasks app, no Android Calendar/Tasks Provider in the middle. One app, full CRUD, for both events and tasks, plus subtasks, reminders, and home screen widgets — built to run on phones with **no Google Play Services at all**. The Android project lives in [`ncal/`](ncal/); open that folder in Android Studio. ## Why this exists No mainstream Android app natively renders both `VEVENT` (events) and `VTODO` (tasks) from CalDAV in one UI. DAVx⁵ + Etar + Tasks.org gets you three apps for one job. This is the fourth option: one app, one account, one sync engine. ## Privacy & security - **No Google Play Services, anywhere.** Sync, reminders, and the home screen widgets all use plain AOSP/AndroidX APIs (`AlarmManager`, `NotificationManager`, `androidx.glance`, `androidx.work`) — every one of these is a core platform capability, not a Google service, so the app runs unmodified on GrapheneOS, LineageOS, or any other de-googled ROM. - **No analytics, crash reporting, or ad SDKs.** Check `ncal/app/build.gradle.kts` — every dependency is either AndroidX/Jetpack, OkHttp, or `biweekly` (iCalendar parsing). Nothing phones home to a vendor. - **No third-party server.** The only network calls in the app go to whatever Nextcloud server URL *you* enter at login (`data/network/CalDavClient.kt`). There is no hardcoded third-party endpoint anywhere in the source. - **Everything is encrypted at rest.** Server URL, username, and app password live in `EncryptedSharedPreferences` (`data/prefs/SecurePrefs.kt`, AES-256-GCM/SIV via an Android Keystore-backed `MasterKey`). The local Room cache itself is an SQLCipher-encrypted database (`ncal_encrypted.db`), not plain SQLite — nothing the app writes to disk, including the widgets' own tiny bit of navigation state, is ever unencrypted. `android:allowBackup="false"` also keeps Android's cloud backup from ever exporting any of it off-device. - **HTTPS only.** `usesCleartextTraffic="false"` blocks plain HTTP at the OS network layer, and the login screen always normalizes whatever you type to `https://`. - **Local-first.** The calendar/task cache is a local, encrypted database. It exists purely so the UI renders instantly and works offline; the server is always the source of truth on next sync. ## Features - **Agenda** — unified, day-grouped list of events *and* tasks. Syncs automatically every time the app is opened, plus pull to refresh any time. - **Month view** — a full calendar grid with events rendered as spanning colored pills and tasks as dot + text, exactly mirroring what's on the home screen widget (they share the same layout algorithm). Tap a day to see its agenda below the grid. - **Tasks** — dedicated checklist view. Tasks render as an indented tree under their parent, not a flat list. - **Subtasks** — assign any task a parent task (stored as the standard iCalendar `RELATED-TO` property, so it round-trips with Nextcloud's own Tasks app and other CalDAV clients). The parent picker excludes the task itself and its own descendants to prevent cycles. - **Reminders** — add one or more reminders to any event or task (at the scheduled time, or a preset offset before it: 5 min up to 1 week). Reminders fire as real system notifications via `AlarmManager` exact alarms — not a best-effort background job — and survive reboots and app updates. Tapping a reminder notification opens the app directly to that item. - **Task status, priority, % complete, location, and URL** — full VTODO fields, not just a completed checkbox: a proper status (Not started / In progress / Completed / Cancelled), an iCal `PRIORITY` 1-9 picker color-coded 1-4 red/5 yellow/6-9 blue (surfaced as the task's leading dot everywhere it's shown — Tasks list, Month view, both widgets), a 0-100% completion slider, and location/URL fields shared with events. - **Home screen widgets** (see below). - **Calendars** — pick which calendars/task-lists sync, mirroring what you'd see in Nextcloud's own sidebar. - **Recurring events and tasks** — `RRULE` (`FREQ=DAILY/WEEKLY/MONTHLY/YEARLY` with `INTERVAL`/`COUNT`/`UNTIL`) is evaluated client-side to re-date each item to its next upcoming occurrence. See "Known limitations" below for what this doesn't cover. ## Home screen widgets Two widgets, built with Jetpack Glance (`androidx.glance:glance-appwidget`), which compiles down to plain `RemoteViews`/`AppWidgetProvider` — no Google dependency: - **Month grid widget** ("Calendar Month" in the widget picker) — the exact same month view as the in-app Month screen (shared layout code in `util/MonthGridLayout.kt`), with ‹ › month navigation and tap-to-open on any event or task. Resizable, and fonts/day-circle size/event-lane count all scale up when you make the widget bigger instead of leaving blank space. - **Next Tasks widget** ("Tasks" in the widget picker) — a scrollable list of incomplete tasks grouped and ordered by due date, with a tap-to-complete circle on each row (color-coded by priority) and tap-to-open on the row itself. Both widgets refresh automatically whenever you sync, save, or delete something in the app — no manual refresh needed. Note this means a widget's content is only as fresh as the last time the app itself synced (on open, on manual pull-to-refresh, or after an edit) — the widgets read from the local cache on their own ~30-minute OS redraw timer, they don't independently hit the network. ## Architecture - **Kotlin + Jetpack Compose (Material 3)**, MVVM, single-activity, no DI framework (`ui/ViewModelFactory.kt` is a small hand-rolled factory instead). - **`CalDavClient`** (`data/network/`) — hand-rolled CalDAV client over OkHttp. Speaks `PROPFIND` (discover calendars/task-lists), `REPORT` (`calendar-query` to fetch `VEVENT`/`VTODO`), and `PUT`/`DELETE` with `If-Match`/`If-None-Match` for safe, conflict-aware writes. Every successful `PUT` captures the server's returned `ETag` so the next edit doesn't race against a stale local copy. - **`biweekly`** — parses/generates the actual iCalendar (RFC 5545) payloads, including `VALARM` (reminders) and `RELATED-TO` (subtasks). - **Room, on an SQLCipher-encrypted database** (`data/db/`) — local cache so every screen (and both widgets) render instantly and survive being offline, without ever touching disk unencrypted. - **`NextcloudRepository`** — single source of truth gluing the network client, the DB, reminder scheduling, and widget refresh together. Every screen reads from Room `Flow`s; every write goes through `save()`/`delete()`, which do the network call first, then update the cache, reminders, and widgets. - **`ReminderScheduler`** (`notifications/`) — schedules one exact `AlarmManager` alarm per item, always for its *next* upcoming reminder offset; the receiver re-arms the following offset each time it fires, so there's never more than one pending alarm per item to track. - **`MonthGridWidget` / `NextTasksWidget`** (`widget/`) — Glance widgets that read straight from the same repository the app uses. ## Setup 1. **Create a Nextcloud app password** — not your real login password. Settings → Security → Devices & sessions → Create new app password. 2. **Open `ncal/` in Android Studio** (Koala/2024.1 or newer). Let it sync Gradle. 3. Run on a device or emulator (min SDK 26 / Android 8+). 4. On first launch, enter your server URL, username, and the app password. 5. Go to the **Calendars** screen (gear icon) and confirm which calendars/task-lists are enabled. 6. Optional: long-press your home screen → Widgets → NCal, then add "Calendar Month" and/or "Tasks". ## Known limitations - **Editing an existing recurring item drops its `RRULE`.** Saving rebuilds the `.ics` from the edit form's fields, so an edited recurring item becomes a one-off. Viewing/completing/deleting a recurring item is safe; editing its fields is not, if you want the series to survive the save. `CalendarItem.rawIcs` retains the original document for exactly this reason — wiring `toIcs()` to mutate the parsed original in place is the natural fix if this turns out to matter in practice. - **No recurrence-editing UI** — no picker to *set* a recurrence rule on a new item. - **Subtask tree is app-side only.** The Month view and both widgets show tasks flat/chronological, not nested — only the in-app Tasks screen renders the parent/ child tree. - **No background sync while the app is closed.** Sync happens on app open, manual pull-to-refresh, or after an edit — there's no periodic `WorkManager` job calling `repository.fullSync()` yet, so a widget can go stale if you never open the app. - **Last-write-wins conflict handling.** A genuinely stale write fails loudly (a `CalDavException` with the HTTP code) rather than silently clobbering a change made elsewhere — but there's no merge UI, just an error surfaced to the edit screen. ## Where to look first if you want to extend it - `data/network/CalDavClient.kt` — all server communication. - `util/IcsMapper.kt` — the iCalendar ⇄ app-model translation layer (events, tasks, subtasks, reminders). - `util/MonthGridLayout.kt` — the month-grid layout algorithm shared by the in-app Month screen and the month widget. - `data/repository/NextcloudRepository.kt` — the sync/cache/reminder/widget orchestration. - `notifications/ReminderScheduler.kt` — reminder alarm scheduling. - `widget/` — the two Glance home screen widgets. ## Building a signed release `ncal/app/build.gradle.kts` reads signing credentials from a git-ignored `ncal/keystore.properties` (`storePassword`, `keyPassword`, `keyAlias`, `storeFile`) — generate your own keystore and fill that file in; nothing under `ncal/keystore/` or `ncal/keystore.properties` is tracked by git, and no signing key ships in this repo. Without that file present, `:app:assembleRelease` still produces an **unsigned** APK. ## License GPL-3.0 — see [`LICENSE`](LICENSE).