Skip to content

Built in tasksprovider - #2750

Open
timofurrer wants to merge 6 commits into
bitfireAT:mainfrom
timofurrer:built-in-tasksprovider
Open

Built in tasksprovider#2750
timofurrer wants to merge 6 commits into
bitfireAT:mainfrom
timofurrer:built-in-tasksprovider

Conversation

@timofurrer

@timofurrer timofurrer commented Aug 5, 2026

Copy link
Copy Markdown

Related: #2740

Checklist

  • The PR has a proper title, description and label.
  • I have [self-reviewed the
    Here's the filled-out template as raw markdown, ready to copy into the PR description:

Note: This is a prototype / proof-of-concept, meant to demonstrate the approach from
discussion #2740 and gather feedback — not to land as-is. Please confirm the discussion has
been acknowledged by the core team before treating this as more than a draft.

Purpose

Implements a first version of the DAVx⁵-hosted VTODO tasks provider proposed in discussion #2740
(design doc: doc/tasks-provider.md).

Today, DAVx⁵'s task sync only works through third-party providers (OpenTasks, tasks.org, jtx
Board), each built on a lossy DMFS-based Android contract — dropped ORGANIZER parameters,
collapsed VALARM (just minutes_before + a reference enum), opaque ATTENDEE/ATTACH/CONTACT/
RESOURCES/REQUEST-STATUS, no RFC 9253 task-dependency support, and dropped RECURRENCE-ID
overrides (#2357). This PR adds a DAVx⁵-hosted ContentProvider with a full-fidelity contract
that any third-party frontend can build against, with no direct DAVx⁵ dependency (see §1 of the
design doc for the full traceability matrix).

Short description

  • Added tasks-provider-contract: a dependency-free public contract (TaskLists, Tasks,
    TaskProperties, TaskAlarms, TaskInstances) for third-party frontends, discovered at
    runtime via an intent action (D1) so the OSE/Managed/Selectable DAVx⁵ variants can coexist.
  • Added tasks-provider: a Room-backed ContentProvider (DavTasksProvider) implementing the
    contract — strict-mode query builder, column allow-listing (SQL injection defense), dirty-flag
    propagation from sub-rows to their parent task, tombstones vs. physical deletes, transactional
    batch/bulk operations.
  • Added synctools/storage/davtasks and synctools/mapping/davtasks: storage/mapping layers
    mirroring the existing DMFS backend's structure, with full-fidelity VTODO↔ContentValues mapping
    (ORGANIZER CN/SENT-BY, full ATTENDEE parameters, RFC 9253 RELTYPE values, full VALARM fields
    instead of the DMFS minutes_before collapse).
  • Wired the new provider into the existing sync engine (LocalDavTaskList, DavTasksSyncManager,
    DavTaskSyncer, TasksAppManager, AutomaticSyncManager) alongside the existing
    OpenTasks/tasks.org/jtx paths — no changes to the shared sync logic itself.
  • Added a Settings UI option to select the built-in provider, next to the existing third-party
    choices.
  • Implemented RRULE/RDATE/EXDATE expansion into TaskInstances, including RECURRENCE-ID override
    folding (RecurrenceExpander/InstanceMaintainer) — bounded and deterministic (the RRULE's own
    COUNT/UNTIL, 500 occurrences, or 10 years past DTSTART, whichever comes first) rather than a
    rolling time window, so it's cheap to fully re-derive on every write.
  • Instrumented tests against a real provider instance (no mocking, project convention), plus
    Robolectric/JVM unit tests for the mapping and recurrence-expansion layers.

Not yet implemented: attachment blob storage (openFile), a client library/sample frontend, and
migration tooling from the existing third-party providers — see the Phasing table in
doc/tasks-provider.md for full scope and current status per phase.

Checklist

Phase 1 (provider core) of the DAVx5 VTODO tasks provider design (doc/tasks-provider.md):
- tasks-provider-contract: dependency-free public contract (URIs, columns, mimetypes,
  permissions, intent-discovery action) that third-party frontends depend on.
- tasks-provider: Room schema (tasks.db, no destructive fallback) + DavTasksProvider
  ContentProvider (URI matching, strict query builder, column allow-listing, dirty
  propagation, tombstones, batch/bulk in a transaction, notifyChange). Insert defaults
  fill in NOT NULL columns the caller didn't supply, the way other Android providers
  (e.g. CalendarProvider2) handle optional columns.

RRULE/Instances expansion (Phase 3) and attachment blob storage (Phase 4) are not yet
implemented; the Instances table schema exists so frontends can already target it.

Includes instrumented security/semantics tests against a real (test-registered)
DavTasksProvider instance: column allow-listing (SQL injection defense), dirty-flag
semantics for sync-adapter vs. non-sync-adapter callers, tombstones vs. real deletes,
cascade-on-delete, and the Instances table's read-only enforcement.
@CLAassistant

CLAassistant commented Aug 5, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

Includes android:description for the READ/WRITE permissions' grant dialog,
so a third-party frontend's permission request doesn't read as vague/
suspicious (no supporting text) to the user granting it.
DavTaskList/DavTaskListProvider/DavRecurringTaskList mirror the DMFS backend's
storage/tasks classes, operating against the DAVx5-hosted provider via
ContentProviderClient. DavTaskBuilder/DavTaskHandler map VTODO <-> Entity with
full fidelity where the DMFS backend is lossy: ORGANIZER CN/SENT-BY, full
ATTENDEE parameters, RFC 9253 RELTYPE values, CONTACT/RESOURCES/REQUEST-STATUS,
and VALARM stored as TRIGGER/DURATION/REPEAT/ACTION instead of collapsed to
minutes_before.

Recurrence exceptions are dropped (logged), matching the DMFS backend's
existing bitfireAT#2357 limitation - full RECURRENCE-ID override support is Phase 3.
Inline BINARY attachments are dropped (logged) - blob storage is Phase 4.

Includes instrumented CRUD tests for DavTaskList/DavTaskListProvider (covering
the two-table Properties/Alarms sub-row routing that's new relative to the
DMFS backend's single Properties table) and Robolectric tests for
DavTaskBuilder/DavTaskHandler.

PropertyList.add() in ical4j is immutable and returns a copy, so the
plusAssign helper for it was a no-op - alarm trigger/description/etc. never
made it into the built VALARM. Build the property list in one shot instead.
Also drop the broken plusAssign helper so it can't be reused.
LocalDavTask/LocalDavTaskList/LocalDavTaskListStore implement the existing
LocalResource/LocalCollection/LocalDataStore interfaces on top of
storage/davtasks, mirroring the DMFS-backed LocalTask/LocalTaskList/
LocalTaskListStore. DavTasksSyncManager/DavTaskSyncer mirror
TasksSyncManager/TaskSyncer for VTODO CalDAV sync.

TasksAppManager recognizes the built-in provider's (runtime-discovered, D1)
authority and routes getDataStore()/BaseSyncWorker's dispatch to it.
SyncDataType.currentAuthority()/fromAuthority() do the same.
AutomaticSyncManager's enable/disable loops and provider-selection check
are extended the same way, since they don't fit the TaskProvider.ProviderName
model the three external providers use. A dedicated DavTasksSyncAdapterService
+ res/xml/sync_davtasks.xml register the actual SyncAdapterType Android's
content-triggered sync mechanism needs - without it, notifyChange(syncToNetwork
= true) has nothing to route to, no matter how AutomaticSyncManager is wired.

Verified end-to-end on a real device against a real CalDAV server: a
third-party frontend's local write triggers an automatic sync via the
registered adapter, uploads through DavTasksSyncManager, and clears dirty.

Known follow-up: no Settings UI card yet to let a user pick the built-in
provider (only the 3 external ones are listed today).
Adds a fourth option ("DAVx5 (built-in)") to the existing Tasks settings
screen, alongside jtx Board/tasks.org/OpenTasks - closes the gap flagged
in the previous commit where the built-in provider had no way to actually
be selected from the app.

The switch on that row is disabled/always-on rather than interactive: it
has nothing to install (unlike the three external providers), so making it
look like a live toggle was misleading - confirmed by a real user tapping it
expecting it to do something. Re-selecting an already-selected built-in
option always re-applies (rather than no-op'ing) so it can be used to force
a re-run of the automatic-sync setup, e.g. after an app update.

Also fixes TasksAppWatcher, which runs on every app start: it only knew
about the three external providers, so it saw the newly-selectable built-in
provider as "nothing selected" and cleared it right back out - reproduced
via a manual sync request waking the app process, which runs the watcher
before the sync worker gets to read the (about to be cleared) setting.
TaskInstances was a stub - not even non-recurring tasks got a row, since nothing
ever called TaskInstanceDao.insert(). Add RecurrenceExpander (pure, ical4j Recur-based:
RRULE ∪ RDATE − EXDATE anchored on DTSTART, RECURRENCE-ID overrides folded in by
matching original_instance_time) and InstanceMaintainer, which re-derives a task's
whole family (main + exceptions) from scratch and is wired into
DavTasksProvider.insert/update/delete.

Deliberately bounded and deterministic instead of a rolling time window: expansion
stops at whichever of the RRULE's own COUNT/UNTIL, 500 occurrences, or 10 years past
DTSTART comes first - cheap to fully re-derive on every write, no background job
needed. Documented on TaskInstances/RDATE/EXDATE so it's not a silent surprise.

tasks-provider gets its own ical4j dependency rather than depending on synctools,
which wraps ical4j with sync-engine concerns this module has no business knowing
about; needed the same strict Apache Commons pins synctools uses to avoid a
transitive version conflict on synctools' androidTest classpath (it depends on
this module to test DavTasksProvider against a real instance).
@timofurrer
timofurrer force-pushed the built-in-tasksprovider branch from 1da5306 to 71761bb Compare August 6, 2026 09:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants