Go to file
RomanNum3ral cc4303fadb
Consolidate Month/Week/List/Tasks into one Calendar home screen; add Files/Photos upload, and configurable Wi-Fi auto-upload for photos and files
- Calendar home screen (ui/calendar/CalendarHomeScreen.kt): Month/Week/List/Tasks
  are now tabs of one screen instead of four separate destinations, with a
  trimmed top bar (Search/Talk/Files/Photos/Calendars).
- Files: multi-file selection and whole-folder upload (subfolders recreated
  server-side via MKCOL), alongside the existing browse/download/delete/rename.
- Photos: upload from device and download back to the device's gallery.
- Auto-upload (data/prefs/AutoUploadPrefs.kt, sync/PhotoAutoUploadWorker.kt,
  sync/PhotoAutoUploadScheduler.kt, sync/LocalFolderScanner.kt,
  sync/CameraBuckets.kt): opt-in, Wi-Fi-only background upload. Fully
  user-configured - any number of independent source -> destination folder
  pairs, each source either a device photo folder or an arbitrary local
  folder (files and subfolders, recursed and recreated server-side). Nothing
  is watched until at least one pair is explicitly added.
- README updated to document all of the above.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-20 05:48:38 -04:00
.gitea/workflows Add offline resilience, undo-delete, search, week view, BYDAY recurrence, import/export, and single-occurrence recurring edits 2026-07-17 18:53:39 -04:00
ncal Consolidate Month/Week/List/Tasks into one Calendar home screen; add Files/Photos upload, and configurable Wi-Fi auto-upload for photos and files 2026-07-20 05:48:38 -04:00
.gitignore Fix remaining known limitations: recurrence editing, subtask tree parity, conflict resolution 2026-07-16 21:02:48 -04:00
LICENSE Initial commit: NCal, a native Nextcloud CalDAV client for Android 2026-07-16 18:19:03 -04:00
README.md Consolidate Month/Week/List/Tasks into one Calendar home screen; add Files/Photos upload, and configurable Wi-Fi auto-upload for photos and files 2026-07-20 05:48:38 -04:00

README.md

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/; 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 — this holds for Files, Photos, Talk, and Auto-upload too, not just calendar sync.
  • Auto-upload is opt-in and scoped to exactly what you configure. It uploads nothing until you explicitly add at least one source → destination folder pair, and only ever reads the specific photo folder(s)/local folder(s) you picked — a photo folder needs the OS photo-library permission (asked for the first time you add one), an arbitrary folder needs no broader permission at all, since the Storage Access Framework grant is scoped to just the folder you picked via the system picker.
  • 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

  • Calendar home screen — Month, Week, List, and Tasks are tabs of one screen (ui/calendar/CalendarHomeScreen.kt), not four separate destinations, so switching how you're looking at the same data is a tap, not a navigation.
    • List — unified, day-grouped list of events and tasks. Syncs automatically every time the app is opened, plus pull to refresh any time.
    • Month — 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.
    • Week — a scrollable week 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 every field that actually differs and lets you pick, per field, whether your edit or the server's current value wins ("All mine"/"All theirs" shortcuts too), 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 tasksRRULE (FREQ=DAILY/WEEKLY/MONTHLY/YEARLY with INTERVAL/COUNT/UNTIL, including ordinaled monthly rules like "the 3rd Thursday" or "the last Friday") is evaluated client-side to re-date each item to its next upcoming occurrence. See "Editing recurring items" below for what this doesn't cover.
  • Edit/delete a single occurrence of a recurring event or task without touching the rest of the series, using standard RFC 5545 mechanics (RECURRENCE-ID overrides and EXDATE exclusions) rather than app-specific state, so it round-trips with any other CalDAV client.
  • Search — find any event or task across every enabled calendar/task-list by title.
  • Undo-after-delete — deleting an event or task shows a snackbar to undo it; the item is recreated server-side with a fresh UID (Nextcloud's calendar trashbin keeps the original UID reserved for 30 days).
  • Import/export .ics — export any calendar/task-list to a real .ics file via the system share sheet, or import one into any calendar.
  • Files — full WebDAV browsing of your Nextcloud Files space: browse folders, download and open a file in whatever app handles its type, upload one or many files at once, upload an entire local folder (subfolders included, recreated on the server), delete, rename, and create folders.
  • Photos — a dedicated photo grid + full-screen swipeable viewer over the same Files space (filtered to images), with upload from the device and download back to the device's gallery.
  • Talk — read and send text messages in your Nextcloud Talk rooms (no voice/video calls — see "What's deliberately not built" below).
  • Auto-upload — a Wi-Fi-only background job that uploads new content to Nextcloud automatically, fully opt-in and user-configured: any number of independent source → destination folder pairs, each source either a device photo folder (Camera, Screenshots, etc., picked from what's actually on the device) or an arbitrary local folder of any file type (subfolders included, recreated on the server the same way Files' manual folder upload does). Nothing is watched or uploaded until you explicitly add at least one pair; see data/prefs/AutoUploadPrefs.kt and sync/PhotoAutoUploadWorker.kt.

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 Flows; 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.
  • FilesClient / FilesRepository (data/network/, data/repository/) — plain WebDAV browsing/CRUD (PROPFIND/GET/PUT/DELETE/MKCOL/MOVE) against /remote.php/dav/files/<user>/, reusing the same DavXml multistatus parser the CalDAV client uses (WebDAV XML parsing is generic, not calendar-specific). Backs both the Files screen and the Photos gallery (Photos is Files' listing filtered to image extensions and rendered as a grid).
  • TalkClient / TalkRepository (data/network/, data/repository/) — Nextcloud Talk chat over its OCS API (room list is api/v4, chat itself is api/v1 — Talk versions each feature area independently). No local cache; text-only, real-time-ish via the chat endpoint's own long-poll (lookIntoFuture=1), no WebSocket/push infra.
  • AutoUploadPrefs / PhotoAutoUploadWorker / PhotoAutoUploadScheduler (data/prefs/, sync/) — the Wi-Fi auto-upload background job. User-configured source → destination pairs (UploadMapping) are JSON-serialized into a single preference (plain, unencrypted — nothing in it is sensitive). Photo-bucket sources are queried via MediaStore and share one global watermark; arbitrary local-folder sources (sync/LocalFolderScanner.kt, via Storage Access Framework + takePersistableUriPermission) are recursively walked each run and each track their own independent watermark, since unlike MediaStore they have no shared timeline to merge against. Runs under a NetworkType.UNMETERED WorkManager constraint (the closest available proxy for "Wi-Fi only").

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".
  7. Optional: on the same Calendars screen, turn on Auto-upload and add one or more folder pairs to start uploading photos and/or files to Nextcloud on Wi-Fi. It asks for photo-library access the first time you add a photo-folder pair; an arbitrary-folder pair needs no extra permission (grant is scoped to the folder you pick, via the system folder picker).

Run the (small, growing) JVM unit test suite with ./gradlew test — no emulator needed.

Editing recurring items

Recurring items get two guarantees, both regression-tested in IcsMapperTest:

  • Editing the series' actual start/end/due time is safe. [CalendarItem.start]/end/ due hold the resolved next occurrence for a recurring item (what every list/grid displays), not the master date - so the edit screen shows/edits a separate recurrenceMasterStart/End/Due for a recurring item instead, with a note that the change applies to the whole series. Editing them writes the true master DTSTART/DTEND/DUE; leaving them untouched round-trips the original value exactly, so an unrelated edit (e.g. just the title) can never silently shift a series forward the way it used to.
  • A rule the Repeats picker can't represent is left completely alone. BYDAY/ BYMONTH/etc-based rules (or anything outside Daily/Weekly/Monthly/Yearly) show as "Does not repeat" in the picker but are never flattened or overwritten by it.

What's deliberately not built

  • Talk voice/video calls. Text chat only. This server's Talk signaling has no dedicated TURN relay configured (turnservers: []), so calls would only connect over the same LAN, not realistically across networks — building the calling UI is deferred until a TURN server (e.g. coturn) is set up.

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.
  • util/ConflictDiff.kt — computes which fields actually differ for the merge dialog.
  • widget/ — the two Glance home screen widgets.
  • data/network/FilesClient.kt / data/repository/FilesRepository.kt — WebDAV Files browsing/CRUD, shared by the Files screen and the Photos gallery.
  • data/network/TalkClient.kt / data/repository/TalkRepository.kt — Nextcloud Talk chat.
  • sync/PhotoAutoUploadWorker.kt / data/prefs/AutoUploadPrefs.kt — the Wi-Fi auto-upload background job and its user-configured source → destination mappings.
  • sync/LocalFolderScanner.kt — recursive local-folder walk (via SAF) for arbitrary-folder auto-upload mappings.

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.