# 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. The Tasks screen *and* the Tasks widget both render the real parent/child tree (`util/TaskTree.kt` is shared by both). - **Recurrence** — a Repeats picker (Daily/Weekly/Monthly/Yearly, interval, and an end condition of never/after N times/on a date) for both events and tasks, backed by a real iCal `RRULE`. - **Conflict handling** — a stale save (someone else changed or deleted the item since you opened it) surfaces both versions in a dialog and lets you keep your edit or take theirs, instead of just failing with an error. - **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, and independently of the app being open at all: a `WorkManager` periodic job (`sync/SyncScheduler.kt`, `sync/SyncWorker.kt`) calls `repository.fullSync()` roughly every 30 minutes whenever the device has network - matching the widgets' own OS-level redraw floor, since syncing more often than a widget can even show an update buys nothing. ## 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". Run the (small, growing) JVM unit test suite with `./gradlew test` — no emulator needed. ## Known limitations - **No recurrence editing for a recurring item's start/end time.** You can set/change *whether* something repeats and how (Daily/Weekly/Monthly/Yearly, interval, end condition) via the Repeats picker, and edit all of a recurring item's other fields freely - but the Starts/Ends/Due date fields become read-only the moment an item is recurring, because [CalendarItem.start]/`end`/`due` hold the *resolved next occurrence* for a recurring item, not the master date, and writing that back would silently shift the whole series forward on every edit. Change the actual start date of a recurring series in Nextcloud's web UI. A rule the picker can't represent (`BYDAY`/`BYMONTH`/etc, or anything outside Daily/Weekly/Monthly/Yearly) is left completely untouched rather than flattened into something simpler. Both guarantees are regression-tested in `IcsMapperTest`. - **Conflict resolution is a two-choice dialog, not a field-by-field merge.** A stale write (HTTP 412 - the item changed or was deleted elsewhere since you opened it) shows both versions and lets you keep your edit or take theirs; there's no way to merge individual fields from each. ## 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. - `sync/SyncWorker.kt` / `sync/SyncScheduler.kt` — the periodic background sync job. - `util/TaskTree.kt` — the parent/child ordering shared by the Tasks screen and widget. - `util/RecurrenceUtils.kt` — re-dates a recurring item to its next occurrence for display. - `data/repository/SaveConflictException.kt` — the 412-conflict data carried to the edit screen's dialog. - `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).