docs: bring the docs in line with what shipped
STORAGE-AND-SYNC.md asked for a follow-up pass on ARCHITECTURE.md §7 and the
ProviderResolver KDoc, which still defined Posture B as "bundle OpenTasks and
find org.dmfs.tasks first" — the plan that was withdrawn as a dead end. That pass,
plus the status the doc left open.
ARCHITECTURE.md now describes the app as built: two modules, the storage-mode
table with the permission each needs, the autoMode rule and why it keys on
holding an external provider's permission, the two-not-three mode vocabulary, and
a manifest section that says what :provider contributes and what is deliberately
absent (GET_ACCOUNTS, INTERNET). §7 records squatting the dmfs authority as a
dead end rather than a road not yet taken, so it doesn't get re-proposed.
ROADMAP.md turns "Posture B, later" into what actually landed and lists what
didn't: the frontend surfaces, the DAVx5 issue, the sync adapter, and device
verification. Two open decisions resolved and struck through — the authority
choice, and recurrence-aware editing, which fix/provider-interaction-review made
stale.
STORAGE-AND-SYNC.md gets per-step status. Open question 3 ("does it work with no
account?") is answered, with the caveat that the test proving it is Robolectric
and skips on ARM64 — answered by construction, not yet on a device.
PLAN.md gets a banner. It's the original design document and still holds the
reasoning behind the layering, but two of its premises are overturned and it
should not be read as current.
README.md was telling users they need a tasks provider installed. They don't, and
that's the headline feature: a table of where tasks can live, that our provider
coexists with OpenTasks rather than replacing it, and that everything exports as
standard .ics.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
50
README.md
50
README.md
@@ -3,8 +3,8 @@
|
|||||||
<h1>Agendula</h1>
|
<h1>Agendula</h1>
|
||||||
|
|
||||||
<p><strong>A modern Material 3 Expressive task app for Android.</strong><br>
|
<p><strong>A modern Material 3 Expressive task app for Android.</strong><br>
|
||||||
Reads, writes, and reminds — on top of an existing tasks provider, with no own
|
Keeps your tasks on your device, or on top of a tasks provider you already use.
|
||||||
sync stack.</p>
|
Open standards, no account required.</p>
|
||||||
|
|
||||||
<a href="https://codeberg.org/jlmakiola/agendula/actions"><img src="https://codeberg.org/jlmakiola/agendula/actions/workflows/ci.yaml/badge.svg?branch=main" alt="CI"></a>
|
<a href="https://codeberg.org/jlmakiola/agendula/actions"><img src="https://codeberg.org/jlmakiola/agendula/actions/workflows/ci.yaml/badge.svg?branch=main" alt="CI"></a>
|
||||||
<img src="https://img.shields.io/badge/Android-10%2B-3DDC84?logo=android&logoColor=white" alt="Android 10+">
|
<img src="https://img.shields.io/badge/Android-10%2B-3DDC84?logo=android&logoColor=white" alt="Android 10+">
|
||||||
@@ -15,31 +15,47 @@ sync stack.</p>
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
Agendula is the task-list sibling to [Calendula](https://codeberg.org/jlmakiola/calendula).
|
Agendula is the task-list sibling to [Calendula](https://codeberg.org/jlmakiola/calendula).
|
||||||
Where Calendula is a pure front-end over Android's `CalendarContract`, Agendula is
|
Where Calendula is a pure front-end over Android's `CalendarContract`, Agendula
|
||||||
a pure front-end over the **OpenTasks `TaskContract` provider** — the store that
|
speaks the **dmfs `TaskContract`** — the same shape DAVx5 (and SmoothSync,
|
||||||
DAVx5 (and SmoothSync, DecSync, …) syncs your CalDAV `VTODO` tasks into. No own
|
DecSync, …) syncs your CalDAV `VTODO` tasks into.
|
||||||
database, no reinvented sync.
|
|
||||||
|
|
||||||
The name rhymes with its sibling on purpose: **Agendula** is *agenda* — Latin for
|
The name rhymes with its sibling on purpose: **Agendula** is *agenda* — Latin for
|
||||||
“things to be done” — given Calendula's `-ula` ending. Calendula keeps your days;
|
“things to be done” — given Calendula's `-ula` ending. Calendula keeps your days;
|
||||||
Agendula keeps your to-dos. (A Calendula flower head is botanically a cluster of
|
Agendula keeps your to-dos. (A Calendula flower head is botanically a cluster of
|
||||||
many small *florets* — so the two apps are florets of one bloom.)
|
many small *florets* — so the two apps are florets of one bloom.)
|
||||||
|
|
||||||
> **Status: data layer done, UI in progress.** The full non-visual stack over
|
## Where your tasks live — your choice
|
||||||
> the `TaskContract` provider — provider resolution, live-updating reads,
|
|
||||||
> writes, smart-list filtering, and a self-scheduled reminder engine — is built
|
| | Where | Sync | Needs |
|
||||||
> and unit-tested. The Material 3 Expressive screens are now being built on top,
|
|---|---|---|---|
|
||||||
> one at a time. See [`docs/ROADMAP.md`](docs/ROADMAP.md) for status,
|
| **On your device** *(default)* | Agendula's own task store, bundled in the app | none yet — sync of our own is planned | nothing. No account, no permissions, no other app |
|
||||||
|
| **In a provider you already use** | OpenTasks or tasks.org | whatever syncs it for you — DAVx5 and friends | that app installed, and its read/write permission |
|
||||||
|
|
||||||
|
Agendula carries its own copy of the Apache-2.0 dmfs task provider, under its own
|
||||||
|
name — so it **coexists with OpenTasks rather than replacing it**, and installing
|
||||||
|
one never breaks the other. It is a fork of a proven schema, not a database
|
||||||
|
written from scratch, which is why every CalDAV engine already understands it.
|
||||||
|
|
||||||
|
Your tasks are exportable as standard iCalendar `.ics` files at any time, because
|
||||||
|
data you can't take with you isn't really yours.
|
||||||
|
|
||||||
|
> **Status: backend complete, UI catching up.** Storage, provider, reads and
|
||||||
|
> writes, smart-list filtering, a self-scheduled reminder engine, and export are
|
||||||
|
> built and unit-tested. The Material 3 Expressive screens are being built on
|
||||||
|
> top, one at a time — the storage-mode picker and export screen are not there
|
||||||
|
> yet. See [`docs/ROADMAP.md`](docs/ROADMAP.md) for status,
|
||||||
> [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) for how it's built, and
|
> [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) for how it's built, and
|
||||||
> [`docs/PLAN.md`](docs/PLAN.md) for the A-now-B-later design rationale.
|
> [`docs/STORAGE-AND-SYNC.md`](docs/STORAGE-AND-SYNC.md) for why storage works
|
||||||
|
> the way it does.
|
||||||
|
|
||||||
## Sync sources (by design)
|
## Sync sources (by design)
|
||||||
|
|
||||||
Agendula works with anything that writes to the tasks provider — **DAVx5**
|
In provider mode Agendula works with anything that writes to that provider —
|
||||||
(CalDAV), **SmoothSync**, **CalDAV-Sync**, **DecSync CC**, or any Android sync
|
**DAVx5** (CalDAV), **SmoothSync**, **CalDAV-Sync**, **DecSync CC**, or any
|
||||||
adapter — because it builds on the provider, not on any one sync app. Google
|
Android sync adapter — because it builds on the provider, not on any one sync
|
||||||
Tasks / Microsoft To Do are out of scope by design (proprietary; they would mean
|
app. Google Tasks / Microsoft To Do are out of scope by design (proprietary; they
|
||||||
owning a sync stack). Open standards — CalDAV / iCalendar / DecSync — are the lane.
|
would mean owning a sync stack). Open standards — CalDAV / iCalendar / DecSync —
|
||||||
|
are the lane.
|
||||||
|
|
||||||
## Translations
|
## Translations
|
||||||
|
|
||||||
|
|||||||
@@ -8,12 +8,21 @@ This document describes how Agendula is built **as it stands today**. For the
|
|||||||
|
|
||||||
## 1. The thesis in one sentence
|
## 1. The thesis in one sentence
|
||||||
|
|
||||||
Agendula is a Material 3 Expressive **front-end** over the OpenTasks
|
Agendula is a Material 3 Expressive task app over the dmfs `TaskContract` — it
|
||||||
`TaskContract` provider — it reads, writes, and reminds on top of a tasks store
|
reads, writes, and reminds against a task store the user chooses: **its own
|
||||||
that some other app (DAVx5, SmoothSync, DecSync CC, tasks.org, …) syncs over
|
bundled provider** (the default) or an external provider app already on the
|
||||||
CalDAV. **Agendula owns no database and no sync stack.** It is the task-list
|
device (OpenTasks, tasks.org) synced by DAVx5, SmoothSync, DecSync CC and the
|
||||||
sibling to [Calendula](https://codeberg.org/jlmakiola/calendula),
|
like. It is the task-list sibling to
|
||||||
which does the same thing for `CalendarContract`.
|
[Calendula](https://codeberg.org/jlmakiola/calendula), which does the same thing
|
||||||
|
for `CalendarContract`.
|
||||||
|
|
||||||
|
**Agendula owns storage but not, yet, sync.** That is a deliberate change from
|
||||||
|
the original "owns no database" thesis, settled in
|
||||||
|
[`STORAGE-AND-SYNC.md`](STORAGE-AND-SYNC.md): depending on a provider app being
|
||||||
|
installed made someone else's roadmap a gate on the app working at all. The
|
||||||
|
database is the vendored dmfs provider under *our* authority — our namespace, not
|
||||||
|
a schema written from scratch — so every CalDAV engine still understands it. A
|
||||||
|
sync adapter of our own is the 1.x arc.
|
||||||
|
|
||||||
The whole design hangs off one rule:
|
The whole design hangs off one rule:
|
||||||
|
|
||||||
@@ -22,9 +31,8 @@ The whole design hangs off one rule:
|
|||||||
> **Provider column names and the authority string never leak above the data
|
> **Provider column names and the authority string never leak above the data
|
||||||
> layer.**
|
> layer.**
|
||||||
|
|
||||||
This is what lets "Posture A" (front-end over an installed provider) become
|
That rule is what let Posture B land as an addition rather than a rewrite: the
|
||||||
"Posture B" (bundle the Apache-2.0 provider, be self-contained) without touching
|
UI, the ViewModels and the domain were untouched by it. See §7.
|
||||||
the UI, the ViewModels, or the domain. See §7.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -49,9 +57,14 @@ the UI, the ViewModels, or the domain. See §7.
|
|||||||
│ ProviderResolver / ContentObserver│
|
│ ProviderResolver / ContentObserver│
|
||||||
│ reminders/ prefs/ di/ demo/ │
|
│ reminders/ prefs/ di/ demo/ │
|
||||||
└───────────────┬──────────────────────────────┘
|
└───────────────┬──────────────────────────────┘
|
||||||
│ content:// + dangerous perms
|
│ content://
|
||||||
┌───────────────▼──────────────────────────────┐
|
┌───────────────▼──────────────────────────────┐
|
||||||
External │ OpenTasks provider ←sync← DAVx5 / DecSync… │
|
Storage │ Local mode (default): │
|
||||||
|
│ :provider — our own bundled task provider │
|
||||||
|
│ same uid, no permission grant needed │
|
||||||
|
│ External mode: │
|
||||||
|
│ OpenTasks / tasks.org ←sync← DAVx5 / … │
|
||||||
|
│ dangerous perms, requested at point of use │
|
||||||
└──────────────────────────────────────────────┘
|
└──────────────────────────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -69,15 +82,21 @@ Both are bound in Hilt in `data/di/DataModule.kt`.
|
|||||||
|
|
||||||
## 3. Module & package layout
|
## 3. Module & package layout
|
||||||
|
|
||||||
Single `:app` module (Posture A). Package root `de.jeanlucmakiola.agendula`.
|
Two modules: `:app` and `:provider`. Package root `de.jeanlucmakiola.agendula`.
|
||||||
|
|
||||||
| Package | Contents |
|
| Module | Contents |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `domain/` | `Models` (TaskList, Task, TaskDetail, enums + pure iCal↔domain value mappers), `TaskForm` (validated create/edit), `TaskFilter` + `TaskFiltering` (smart lists), `TaskSorting`, `DayWindow` (local-midnight maths). No Android imports. |
|
| `:provider` | Agendula's own task store — the Apache-2.0 dmfs task provider 1.4.2, vendored in-tree under our authority and permission namespace. Not our code; see [`provider/PROVENANCE.md`](../provider/PROVENANCE.md) for the upstream commit and every deviation. No app code imports from it except `ProviderResolver`, which reads the authority out of its resources. |
|
||||||
| `data/tasks/` | `TasksContract` (vendored subset), `ProviderResolver` (the A/B seam), `TaskProjections`, `ColumnReader`, `TaskMapper` (cursor→domain), `TaskWriteMapper` (form→`ContentValues`), `TasksDataSource` + `AndroidTasksDataSource`, `TasksRepository` + `Impl`, `Failures`. |
|
|
||||||
|
| Package (`:app`) | Contents |
|
||||||
|
|---|---|
|
||||||
|
| `domain/` | `Models` (TaskList, Task, TaskDetail, enums + pure iCal↔domain value mappers), `TaskForm` (validated create/edit), `TaskFilter` + `TaskFiltering` (smart lists), `TaskSorting`, `AllDayTime` (the two date conventions), `DayWindow` (local-midnight maths). No Android imports. |
|
||||||
|
| `domain/export/` | `ExportModels` + `ICalendarWriter` — VTODO serialization. Pure Kotlin, so the format is JVM-testable. |
|
||||||
|
| `data/tasks/` | `TasksContract` (vendored subset), `ProviderResolver` + `ProviderEnvironment` + `StorageMode` + `StorageModeHolder` (the A/B seam), `TaskProjections`, `ColumnReader`, `TaskMapper` (cursor→domain), `TaskWriteMapper` (form→`ContentValues`), `TasksDataSource` + `AndroidTasksDataSource`, `TasksRepository` + `Impl`, `Failures`. |
|
||||||
|
| `data/export/` | `TaskExporter` (lists → `.ics` documents), `ExportWriter` (SAF plumbing; a floret-kit candidate). |
|
||||||
| `data/reminders/` | `ReminderScheduler` (the self-scheduled engine), `DueReminderReceiver`, `BootReceiver`, `ProviderChangeReceiver`, `ScheduledReminderStore`, `TaskNotifier`. |
|
| `data/reminders/` | `ReminderScheduler` (the self-scheduled engine), `DueReminderReceiver`, `BootReceiver`, `ProviderChangeReceiver`, `ScheduledReminderStore`, `TaskNotifier`. |
|
||||||
| `data/prefs/` | `SettingsPrefs` (DataStore). |
|
| `data/prefs/` | `SettingsPrefs` (DataStore). |
|
||||||
| `data/di/` | `DataModule` (binds + provides), `Qualifiers` (`@IoDispatcher`). |
|
| `data/di/` | `DataModule` (binds + provides), `Qualifiers` (`@IoDispatcher`, `@ApplicationScope`). |
|
||||||
| `data/demo/` | `DemoSeeder` (debug-only sample data). |
|
| `data/demo/` | `DemoSeeder` (debug-only sample data). |
|
||||||
| `ui/` | `theme/`, `common/` (GroupedList, ListChip), `lists/`, `tasklist/`, `detail/`, `edit/`, `settings/`, `permission/` (each a ViewModel + UiState; `lists` also has its screen), `RootScreen`. |
|
| `ui/` | `theme/`, `common/` (GroupedList, ListChip), `lists/`, `tasklist/`, `detail/`, `edit/`, `settings/`, `permission/` (each a ViewModel + UiState; `lists` also has its screen), `RootScreen`. |
|
||||||
| root | `AgendulaApp` (Hilt app), `MainActivity`. |
|
| root | `AgendulaApp` (Hilt app), `MainActivity`. |
|
||||||
@@ -88,20 +107,45 @@ Single `:app` module (Posture A). Package root `de.jeanlucmakiola.agendula`.
|
|||||||
|
|
||||||
### 4.1 Provider targeting — `ProviderResolver`
|
### 4.1 Provider targeting — `ProviderResolver`
|
||||||
|
|
||||||
`ProviderResolver.resolve()` walks a preference-ordered candidate list and
|
`ProviderResolver.resolve()` returns the active `TaskProvider(authority,
|
||||||
returns the first provider actually installed (via
|
readPermission, writePermission, packageName, isOwn)` for the selected
|
||||||
`PackageManager.resolveContentProvider`), or `null` if none is. Each candidate
|
`StorageMode`.
|
||||||
is a `TaskProvider(authority, readPermission, writePermission, packageName)`.
|
|
||||||
|
|
||||||
| Provider | Authority | Permissions |
|
| Mode | Provider | Authority | Permissions |
|
||||||
|---|---|---|
|
|---|---|---|---|
|
||||||
| OpenTasks | `org.dmfs.tasks` | `org.dmfs.permission.READ_TASKS` / `WRITE_TASKS` |
|
| **Local** (default) | ours, bundled | `de.jeanlucmakiola.agendula.tasks` | **none** — same uid |
|
||||||
| tasks.org | `org.tasks.opentasks` | `org.tasks.permission.READ_TASKS` / `WRITE_TASKS` |
|
| External | OpenTasks | `org.dmfs.tasks` | `org.dmfs.permission.READ_TASKS` / `WRITE_TASKS` |
|
||||||
|
| External | tasks.org | `org.tasks.opentasks` | `org.tasks.permission.READ_TASKS` / `WRITE_TASKS` |
|
||||||
|
|
||||||
Both are backed by the same dmfs `TaskProvider`, so the **same `TaskContract`
|
All three are backed by the same dmfs `TaskProvider` — ours *is* that provider,
|
||||||
columns apply** regardless of which is present. `null` from `resolve()` drives
|
vendored — so the **same `TaskContract` columns apply** throughout.
|
||||||
the "install a tasks provider" onboarding gate. `hasPermission()` checks both
|
|
||||||
runtime perms for the active provider.
|
`hasPermission()` short-circuits to `true` for our own provider: a same-uid
|
||||||
|
caller bypasses a provider's permission checks outright, so
|
||||||
|
`ProviderStatus.NEEDS_PERMISSION` can never fire in Local mode. In External mode
|
||||||
|
it checks both runtime perms, and `null` from `resolve()` drives the "install a
|
||||||
|
tasks provider" gate.
|
||||||
|
|
||||||
|
**Choosing the mode.** An explicit choice is stored in `SettingsPrefs` and
|
||||||
|
mirrored into the resolver by `StorageModeHolder` — the resolver is consulted
|
||||||
|
synchronously on every query and cannot read DataStore itself. When there is no
|
||||||
|
explicit choice (the normal case), `autoMode()` decides:
|
||||||
|
|
||||||
|
> **External if we already hold an external provider's runtime permission,
|
||||||
|
> otherwise Local.**
|
||||||
|
|
||||||
|
That permission is dangerous-level, so it can only be there because an earlier
|
||||||
|
version asked and the user agreed — the signature of an existing Posture A user,
|
||||||
|
who must not be dropped onto an empty store and left to conclude their tasks were
|
||||||
|
deleted. A fresh install holds nothing and gets local-first storage.
|
||||||
|
|
||||||
|
The platform calls sit behind `ProviderEnvironment` so this decision is unit
|
||||||
|
tested on the JVM (`ProviderResolverTest`) rather than only on a device.
|
||||||
|
|
||||||
|
**Storage modes** are `LOCAL` and `EXTERNAL` only. `STORAGE-AND-SYNC.md`
|
||||||
|
describes three, but *Synced* is not a third store — it is Local with an account
|
||||||
|
attached, so it is derived state, and modelling it as a separate mode would imply
|
||||||
|
that turning sync on is a migration. It isn't.
|
||||||
|
|
||||||
### 4.2 `TasksContract`
|
### 4.2 `TasksContract`
|
||||||
|
|
||||||
@@ -192,17 +236,32 @@ This is the single largest piece of genuinely-new code in Agendula.
|
|||||||
|
|
||||||
## 7. The A / B seam (why the layering is shaped this way)
|
## 7. The A / B seam (why the layering is shaped this way)
|
||||||
|
|
||||||
- **Posture A (today):** front-end over whatever provider is installed. Ships
|
Both terms were **redefined** by [`STORAGE-AND-SYNC.md`](STORAGE-AND-SYNC.md).
|
||||||
fast; requires a provider app present (the "needs DAVx5/OpenTasks" onboarding
|
They no longer mean what earlier drafts of this document said.
|
||||||
moment).
|
|
||||||
- **Posture B (later):** add a `:provider` module bundling the Apache-2.0
|
|
||||||
`opentasks-provider`. `ProviderResolver` then finds **our own** `org.dmfs.tasks`
|
|
||||||
first; external CalDAV engines sync directly into it. **The UI, ViewModels,
|
|
||||||
domain, and `TasksRepository` do not change** — only the resolver's default and
|
|
||||||
some manifest perms.
|
|
||||||
|
|
||||||
Bundling the provider bundles **storage, not sync** — Agendula stays a pure
|
- **Posture A** — front-end over an *external* provider (OpenTasks, tasks.org).
|
||||||
front-end over open backends either way.
|
Still fully supported; it stopped being the only option and became a user
|
||||||
|
choice, `StorageMode.EXTERNAL`.
|
||||||
|
- **Posture B (shipped)** — the `:provider` module: the Apache-2.0 dmfs provider
|
||||||
|
vendored under **our own** authority `de.jeanlucmakiola.agendula.tasks` and our
|
||||||
|
own permission namespace. It **coexists with everything and replaces nothing**.
|
||||||
|
|
||||||
|
> **Dead end, do not revisit:** bundling the provider under dmfs's *own*
|
||||||
|
> authority so DAVx5 would sync into it unwittingly. Two apps cannot declare the
|
||||||
|
> same authority (`INSTALL_FAILED_CONFLICTING_PROVIDER`) or the same
|
||||||
|
> `<permission>` name (`INSTALL_FAILED_DUPLICATE_PERMISSION`), so anyone with
|
||||||
|
> OpenTasks installed simply could not have installed Agendula. Account
|
||||||
|
> visibility is also keyed by *package*, not authority, which would have left the
|
||||||
|
> bundled provider seeing zero accounts and pruning synced lists as orphaned.
|
||||||
|
> Full reasoning in `STORAGE-AND-SYNC.md`.
|
||||||
|
|
||||||
|
The seam earned its keep: `ProviderResolver` is still the only thing that knows
|
||||||
|
an authority exists and `AndroidTasksDataSource` the only thing that touches a
|
||||||
|
resolver, so vendoring an entire content provider **changed no UI, no ViewModel,
|
||||||
|
no domain type, and not one line of `TasksRepository`.**
|
||||||
|
|
||||||
|
Bundling the provider bundles **storage, not sync**. Our own sync adapter is a
|
||||||
|
separate, later piece of work — see `STORAGE-AND-SYNC.md`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -241,8 +300,9 @@ the `@IoDispatcher`). `AgendulaApp` is the `@HiltAndroidApp` entry point;
|
|||||||
| Build | AGP 9.2.1, Kotlin 2.3.21, KSP, Hilt 2.59.2, Java 17 |
|
| Build | AGP 9.2.1, Kotlin 2.3.21, KSP, Hilt 2.59.2, Java 17 |
|
||||||
| SDK | compileSdk 37, minSdk 29 (Android 10), targetSdk 36 |
|
| SDK | compileSdk 37, minSdk 29 (Android 10), targetSdk 36 |
|
||||||
| UI | Compose BOM 2026.05.01, Material3 `1.5.0-alpha21` (Expressive APIs), Glance 1.1.1 (widget, later) |
|
| UI | Compose BOM 2026.05.01, Material3 `1.5.0-alpha21` (Expressive APIs), Glance 1.1.1 (widget, later) |
|
||||||
| Other | DataStore, kotlinx-datetime, kotlinx-coroutines |
|
| Other | DataStore, DocumentFile (SAF export), kotlinx-datetime, kotlinx-coroutines |
|
||||||
| Tests | JUnit5 (Jupiter) + Truth + Turbine + coroutines-test; the data source is the JVM-testable seam |
|
| `:provider` | Java 17, `org.dmfs` jems / rfc5545-datetime / lib-recur (all Maven Central — no new repository; `settings.gradle.kts` stays `google()` + `mavenCentral()` under `FAIL_ON_PROJECT_REPOS`) |
|
||||||
|
| Tests | `:app` is JUnit5 (Jupiter) + Truth + Turbine + coroutines-test, with the data source and `ProviderEnvironment` as the JVM-testable seams. **`:provider` is JUnit4 + Robolectric** — upstream's own suite, kept as written rather than rewritten, since that coverage is what makes vendoring safe. Don't add `useJUnitPlatform()` there. |
|
||||||
| Versioning | committed `versionName` is the source of truth; a bump reaching `main` triggers the release and the pipeline mints the `vX.Y.Z` tag. `versionCode = MAJOR*10000 + MINOR*100 + PATCH`. See [`RELEASING.md`](RELEASING.md). |
|
| Versioning | committed `versionName` is the source of truth; a bump reaching `main` triggers the release and the pipeline mints the `vX.Y.Z` tag. `versionCode = MAJOR*10000 + MINOR*100 + PATCH`. See [`RELEASING.md`](RELEASING.md). |
|
||||||
| CI | Split by forge: `.forgejo/workflows/ci.yaml` on Codeberg (canonical, no secrets), `.gitea/workflows/release.yaml` on Gitea (all secrets). See [`RELEASING.md`](RELEASING.md). |
|
| CI | Split by forge: `.forgejo/workflows/ci.yaml` on Codeberg (canonical, no secrets), `.gitea/workflows/release.yaml` on Gitea (all secrets). See [`RELEASING.md`](RELEASING.md). |
|
||||||
| Distribution | F-Droid (`fdroid-metadata/`) + Codeberg release APKs |
|
| Distribution | F-Droid (`fdroid-metadata/`) + Codeberg release APKs |
|
||||||
@@ -251,13 +311,25 @@ the `@IoDispatcher`). `AgendulaApp` is the `@HiltAndroidApp` entry point;
|
|||||||
|
|
||||||
## 11. Manifest surface
|
## 11. Manifest surface
|
||||||
|
|
||||||
- **Permissions:** both `org.dmfs.*` and `org.tasks.*` read/write tasks perms
|
- **Declared by `:app`:** both `org.dmfs.*` and `org.tasks.*` read/write tasks
|
||||||
declared statically (the active set is requested at runtime);
|
perms (static manifest, so they are always declared; requested at runtime only
|
||||||
`POST_NOTIFICATIONS`, `RECEIVE_BOOT_COMPLETED`, exact-alarm
|
in External mode); `POST_NOTIFICATIONS`, `RECEIVE_BOOT_COMPLETED`, exact-alarm
|
||||||
(`USE_EXACT_ALARM` on 33+, `SCHEDULE_EXACT_ALARM` ≤32).
|
(`USE_EXACT_ALARM` on 33+, `SCHEDULE_EXACT_ALARM` ≤32).
|
||||||
- **`<queries>`** for package visibility: both provider authorities + a LAUNCHER
|
- **Declared by `:provider`:** the `<provider>` itself plus our own
|
||||||
intent (so `resolveContentProvider` works and onboarding can open the
|
`de.jeanlucmakiola.agendula.permission.READ_TASKS` / `WRITE_TASKS` and their
|
||||||
provider / a store listing).
|
permission group. These exist for **other** apps — Agendula reaches its own
|
||||||
|
provider same-uid and neither declares a `uses-permission` for them nor asks.
|
||||||
|
The provider is `exported="true"` on purpose: that is what would let DAVx5
|
||||||
|
write into it once it knows our authority.
|
||||||
|
- **Deliberately absent:** `GET_ACCOUNTS` (stripped from the vendored provider —
|
||||||
|
see change 1 in `PROVENANCE.md`) and `INTERNET`, which stays undeclared until
|
||||||
|
sync actually ships. Export needs no storage permission at all; SAF hands us a
|
||||||
|
`Uri` the user picked.
|
||||||
|
- **`<queries>`** for package visibility: both *external* provider authorities +
|
||||||
|
a LAUNCHER intent (so `resolveContentProvider` works and onboarding can open
|
||||||
|
the provider / a store listing). Our own provider needs no entry.
|
||||||
- **Receivers:** `DueReminderReceiver` (not exported), `BootReceiver`,
|
- **Receivers:** `DueReminderReceiver` (not exported), `BootReceiver`,
|
||||||
`ProviderChangeReceiver` (both authorities). No `EVENT_REMINDER` receiver —
|
`ProviderChangeReceiver` (all three authorities, ours first — an intent-filter
|
||||||
that's a Calendula thing that doesn't apply here.
|
host must be a literal), and the vendored provider's own
|
||||||
|
`TaskProviderBroadcastReceiver`. No `EVENT_REMINDER` receiver — that's a
|
||||||
|
Calendula thing that doesn't apply here.
|
||||||
|
|||||||
19
docs/PLAN.md
19
docs/PLAN.md
@@ -1,5 +1,24 @@
|
|||||||
# Agendula — implementation plan
|
# Agendula — implementation plan
|
||||||
|
|
||||||
|
> ⚠️ **Historical document.** This is the original design plan, kept for the
|
||||||
|
> reasoning behind decisions that are still in force — the layering, the data
|
||||||
|
> model, the reminder engine, what transfers from Calendula. It is **not** a
|
||||||
|
> description of the app as it stands.
|
||||||
|
>
|
||||||
|
> Two things here have since been overturned, both by
|
||||||
|
> [`STORAGE-AND-SYNC.md`](STORAGE-AND-SYNC.md), which supersedes this document
|
||||||
|
> wherever they disagree:
|
||||||
|
>
|
||||||
|
> 1. **"No own storage."** Agendula now ships its own bundled task provider, and
|
||||||
|
> depending on an external provider app is a user choice rather than a
|
||||||
|
> requirement.
|
||||||
|
> 2. **"Posture B = bundle OpenTasks under `org.dmfs.tasks`."** That is a dead
|
||||||
|
> end, not a later step — two apps cannot declare the same authority or
|
||||||
|
> permission name. Posture B shipped under *our own* authority instead.
|
||||||
|
>
|
||||||
|
> For the current picture see [`ARCHITECTURE.md`](ARCHITECTURE.md); for status,
|
||||||
|
> [`ROADMAP.md`](ROADMAP.md).
|
||||||
|
|
||||||
> A modern Material 3 Expressive **task** app for Android. Reads, writes, and
|
> A modern Material 3 Expressive **task** app for Android. Reads, writes, and
|
||||||
> reminds — on top of an existing tasks provider (synced by DAVx5 / SmoothSync /
|
> reminds — on top of an existing tasks provider (synced by DAVx5 / SmoothSync /
|
||||||
> DecSync over CalDAV), with no own sync stack.
|
> DecSync over CalDAV), with no own sync stack.
|
||||||
|
|||||||
@@ -10,16 +10,18 @@ Status legend: ✅ done · 🚧 in progress · ⬜ not started
|
|||||||
|
|
||||||
## Current state (one line)
|
## Current state (one line)
|
||||||
|
|
||||||
The full non-visual stack ("backoffice") over the OpenTasks `TaskContract`
|
Agendula now **carries its own task store**: the `:provider` module ships the
|
||||||
provider is **done and unit-tested**, and the Material 3 Expressive UI is built
|
vendored dmfs provider under our authority, so the app is complete and local-first
|
||||||
through **M5**: lists → task list (swipe gestures, inline add, smart-list section
|
with nothing else installed, and an external provider (OpenTasks / tasks.org) is a
|
||||||
headers) → detail / edit with full CRUD, date-time pickers, priority,
|
user choice rather than a requirement. The non-visual stack over `TaskContract` is
|
||||||
percent-complete, conflict-safe saves, per-task reminders, and subtask
|
done and unit-tested, export to iCalendar has landed, and the Material 3
|
||||||
create + reparent — plus a one-time reminder onboarding step and a Settings
|
Expressive UI is built through **M5**: lists → task list (swipe gestures, inline
|
||||||
screen (theme, dynamic colour, due-reminder master toggle + default offset +
|
add, smart-list section headers) → detail / edit with full CRUD, date-time
|
||||||
exact-alarm status, default list, and the add-a-subtask-row opt-out). Remaining
|
pickers, priority, percent-complete, conflict-safe saves, per-task reminders, and
|
||||||
work is M6 (Glance widget, translations, F-Droid release; the Settings screen
|
subtask create + reparent — plus a one-time reminder onboarding step and a
|
||||||
landed early with M5 and still needs a language entry).
|
Settings screen. Remaining work is the **frontend surfaces for what just landed**
|
||||||
|
(a storage-mode picker, an export screen), then M6 (Glance widget, translations,
|
||||||
|
F-Droid release) and the sync adapter.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -128,11 +130,33 @@ The engine exists (M1: `ReminderScheduler` + boot / provider-change re-sync,
|
|||||||
- ⬜ Translations — only `res/values/` (English); no `values-XX`.
|
- ⬜ Translations — only `res/values/` (English); no `values-XX`.
|
||||||
- ⬜ Finalize F-Droid metadata, confirm CI release flow.
|
- ⬜ Finalize F-Droid metadata, confirm CI release flow.
|
||||||
|
|
||||||
### ⬜ Posture B (separate track, later)
|
### ✅ Posture B — our own task store
|
||||||
Add a `:provider` module bundling the Apache-2.0 `opentasks-provider`;
|
Agendula stopped depending on a provider app being installed. Direction and
|
||||||
`ProviderResolver` defaults to our own `org.dmfs.tasks`; add sync-adapter
|
reasoning in [`STORAGE-AND-SYNC.md`](STORAGE-AND-SYNC.md); note it **redefined**
|
||||||
permissions; ship self-contained. UI / repository / domain untouched — see
|
what Posture B means (our own authority, coexisting with everything — *not*
|
||||||
[`ARCHITECTURE.md`](ARCHITECTURE.md) §7.
|
squatting `org.dmfs.tasks`, which is a dead end).
|
||||||
|
- ✅ `fix/provider-interaction-review` merged (step 1).
|
||||||
|
- ✅ `:provider` — the Apache-2.0 dmfs provider 1.4.2 (DB 23) vendored in-tree
|
||||||
|
under `de.jeanlucmakiola.agendula.tasks` and our own permission namespace.
|
||||||
|
`GET_ACCOUNTS` dropped, with the account-cleanup path reworked so it can only
|
||||||
|
prune account types we authenticate ourselves — the deletion is unsafe without
|
||||||
|
that rework. Modernized to minSdk 29 / targetSdk 36 / Java 17.
|
||||||
|
[`provider/PROVENANCE.md`](../provider/PROVENANCE.md) records every deviation,
|
||||||
|
each also marked `AGENDULA CHANGE` at the site. Upstream's 51 JVM tests pass.
|
||||||
|
- ✅ Storage modes + the permission-gate bypass — `ProviderStatus.NEEDS_PERMISSION`
|
||||||
|
can no longer fire in Local mode, and an upgrading Posture A user stays on the
|
||||||
|
provider that holds their data (`ProviderResolver.autoMode`).
|
||||||
|
- ✅ Export to iCalendar (step 3) — a v1 feature now that Local-mode data lives
|
||||||
|
only in our app's private storage. One `.ics` per list, to a folder or a zip,
|
||||||
|
via SAF. Backend only.
|
||||||
|
- ⬜ **Frontend surfaces for the above** — a storage-mode picker in Settings and
|
||||||
|
an export screen. The backend is done and unused until these exist.
|
||||||
|
- ⬜ File the DAVx5 issue (step 4) — non-blocking, cheap, serves F-Droid users.
|
||||||
|
- ⬜ Sync adapter (step 5) — the 1.x arc. Design discussion still open: protocol
|
||||||
|
coverage, account model, conflict resolution, and `ical4android`'s licence
|
||||||
|
against our MIT.
|
||||||
|
- ⬜ Verify on a device: the local path with no account, and the vendored
|
||||||
|
provider's timezone-change behaviour (change 3 in `PROVENANCE.md`).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -145,11 +169,20 @@ These carry over from [`PLAN.md`](PLAN.md) §9; resolved ones are struck through
|
|||||||
`org.tasks.opentasks` + `org.tasks.permission.*`.
|
`org.tasks.opentasks` + `org.tasks.permission.*`.
|
||||||
3. **jtx Board** — support its richer contract later, or stay OpenTasks-only?
|
3. **jtx Board** — support its richer contract later, or stay OpenTasks-only?
|
||||||
(Not in the candidate list today.)
|
(Not in the candidate list today.)
|
||||||
4. **Posture B authority choice** — bundling `org.dmfs.tasks` makes Agendula a
|
4. ~~**Posture B authority choice**~~ resolved: **our own**
|
||||||
*replacement* for OpenTasks (one authority owner per device). Intended, but a
|
`de.jeanlucmakiola.agendula.tasks`. Squatting `org.dmfs.tasks` is a dead end,
|
||||||
conscious choice.
|
not merely a trade-off — two apps cannot declare the same authority or
|
||||||
5. **Recurring tasks** — read as occurrences today (`isRecurring` flag exists);
|
permission name, so anyone with OpenTasks installed could not have installed
|
||||||
recurrence-aware editing is out of scope for v1.
|
Agendula at all.
|
||||||
|
5. ~~**Recurring tasks** — recurrence-aware editing out of scope for v1~~ stale:
|
||||||
|
`fix/provider-interaction-review` routes edits to a recurring task through the
|
||||||
|
instances URI, so the provider forks an override instead of re-anchoring the
|
||||||
|
series.
|
||||||
|
6. **Resolver ordering / mode-selection UX** — `autoMode()` picks a sane default
|
||||||
|
today (see [`ARCHITECTURE.md`](ARCHITECTURE.md) §4.1); the Settings override it
|
||||||
|
assumes is not built yet.
|
||||||
|
7. **Sync protocol coverage**, account model, conflict resolution — the next
|
||||||
|
design discussion.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -4,9 +4,13 @@
|
|||||||
> bundle OpenTasks" working notes, which are withdrawn (see
|
> bundle OpenTasks" working notes, which are withdrawn (see
|
||||||
> [Dead ends](#dead-ends--do-not-revisit)). This is the detailed companion to
|
> [Dead ends](#dead-ends--do-not-revisit)). This is the detailed companion to
|
||||||
> `ARCHITECTURE.md` §7 and the `ProviderResolver` comments, **and it redefines
|
> `ARCHITECTURE.md` §7 and the `ProviderResolver` comments, **and it redefines
|
||||||
> what Posture B means** — those two need a follow-up edit.
|
> what Posture B means.**
|
||||||
> `ROADMAP.md` / `PLAN.md` remain known-stale and are due a deliberate pass;
|
>
|
||||||
> this document does not attempt it.
|
> **Status, 2026-08-02: steps 1–3 are done** — see
|
||||||
|
> [Sequencing](#sequencing). `ARCHITECTURE.md` and `ROADMAP.md` have had their
|
||||||
|
> follow-up pass and now match what shipped; `PLAN.md` is the original design
|
||||||
|
> document and is left as the historical record. What is built, and what is still
|
||||||
|
> only described here, is marked step by step below.
|
||||||
|
|
||||||
## The plan, in short
|
## The plan, in short
|
||||||
|
|
||||||
@@ -24,13 +28,13 @@
|
|||||||
|
|
||||||
**In what order**
|
**In what order**
|
||||||
|
|
||||||
| # | Step | Why now |
|
| # | Step | Why now | Status |
|
||||||
|---|---|---|
|
|---|---|---|---|
|
||||||
| 1 | Merge `fix/provider-interaction-review` | unmerged and rotting; touches the same permission flow as step 2 |
|
| 1 | Merge `fix/provider-interaction-review` | unmerged and rotting; touches the same permission flow as step 2 | ✅ done |
|
||||||
| 2 | Vendor `:provider` under our own authority | the identity, done once — and it ships a complete local-first app |
|
| 2 | Vendor `:provider` under our own authority | the identity, done once — and it ships a complete local-first app | ✅ done |
|
||||||
| 3 | Export / backup | our data now lives only in our app's private storage |
|
| 3 | Export / backup | our data now lives only in our app's private storage | ✅ backend done; no UI yet |
|
||||||
| 4 | File the DAVx5 issue | cheap, non-blocking, serves F-Droid users |
|
| 4 | File the DAVx5 issue | cheap, non-blocking, serves F-Droid users | ⬜ |
|
||||||
| 5 | Sync adapter | the 1.x arc; design discussion pending |
|
| 5 | Sync adapter | the 1.x arc; design discussion pending | ⬜ |
|
||||||
|
|
||||||
Everything below is the reasoning behind those choices, the alternatives that
|
Everything below is the reasoning behind those choices, the alternatives that
|
||||||
were rejected, and the constraints they have to survive.
|
were rejected, and the constraints they have to survive.
|
||||||
@@ -64,8 +68,8 @@ Plus a standing rule: **anything that isn't task-domain goes to floret-kit.**
|
|||||||
|
|
||||||
### The vocabulary, redefined
|
### The vocabulary, redefined
|
||||||
|
|
||||||
`ARCHITECTURE.md` §7 and `ProviderResolver`'s KDoc still describe Posture B as
|
`ARCHITECTURE.md` §7 and `ProviderResolver`'s KDoc used to describe Posture B as
|
||||||
"bundle OpenTasks and find `org.dmfs.tasks` first." Replace with:
|
"bundle OpenTasks and find `org.dmfs.tasks` first." Both now read as below:
|
||||||
|
|
||||||
- **Posture A** — front-end over an *external* provider (OpenTasks, tasks.org).
|
- **Posture A** — front-end over an *external* provider (OpenTasks, tasks.org).
|
||||||
Still fully supported; it stops being the default and becomes a **user
|
Still fully supported; it stops being the default and becomes a **user
|
||||||
@@ -173,19 +177,36 @@ feature — see [Storage modes](#storage-modes--the-users-choice).
|
|||||||
Local and Synced are the same store — Synced is Local with an account attached,
|
Local and Synced are the same store — Synced is Local with an account attached,
|
||||||
so switching on sync is not a migration.
|
so switching on sync is not a migration.
|
||||||
|
|
||||||
**Resolver ordering needs deciding.** Today `ProviderResolver.CANDIDATES` is a
|
**Resolver ordering — decided, and it went both ways as expected.**
|
||||||
fixed priority list and the first hit wins. Once we bundle our own provider,
|
`ProviderResolver` now takes an explicit `StorageMode` from Settings when there
|
||||||
"first hit" is the wrong rule: someone who used Agendula locally and *later*
|
is one, and otherwise calls `autoMode()`. The auto rule turned out to be sharper
|
||||||
installs DAVx5 + OpenTasks would see an external candidate outrank the provider
|
than "rank ours first whenever it's non-empty", and needs no database probe:
|
||||||
that actually holds their data. Options: rank ours first whenever it's
|
|
||||||
non-empty, or make the mode an explicit Settings choice (it's user-visible
|
|
||||||
either way, so probably both — auto-pick a sane default, let Settings override).
|
|
||||||
|
|
||||||
**Export/backup is a v1 feature.** Not, as previously framed, a migration safety
|
> **External if we already hold an external provider's runtime permission,
|
||||||
net for "uninstall OpenTasks" — that scenario no longer exists. It's data
|
> otherwise Local.**
|
||||||
portability for Local-mode users, whose tasks otherwise exist in exactly one
|
|
||||||
place with no second copy. On Play, where most users won't have a sync engine,
|
That permission is dangerous-level, so it can only be there because an earlier
|
||||||
that's the majority.
|
version asked and the user agreed — which is exactly what "existing Posture A
|
||||||
|
user" means. A fresh install holds nothing and gets local-first. ⬜ The Settings
|
||||||
|
override the rule assumes is not built yet.
|
||||||
|
|
||||||
|
**Note on the mode vocabulary.** The code has two modes, not three:
|
||||||
|
`StorageMode.LOCAL` and `StorageMode.EXTERNAL`. As this document says two
|
||||||
|
paragraphs up, Synced *is* Local with an account attached — so it is derived
|
||||||
|
state, and giving it its own constant would imply switching sync on is a
|
||||||
|
migration when the whole point is that it isn't.
|
||||||
|
|
||||||
|
**Export/backup is a v1 feature.** ✅ Backend built (no UI yet). Not, as
|
||||||
|
previously framed, a migration safety net for "uninstall OpenTasks" — that
|
||||||
|
scenario no longer exists. It's data portability for Local-mode users, whose
|
||||||
|
tasks otherwise exist in exactly one place with no second copy. On Play, where
|
||||||
|
most users won't have a sync engine, that's the majority.
|
||||||
|
|
||||||
|
One `.ics` per list — a list is a CalDAV collection, and that's the unit other
|
||||||
|
clients understand — written through SAF to either a folder or a single zip.
|
||||||
|
Local tasks have no `_uid` (only a sync adapter may assign one), so the writer
|
||||||
|
synthesises a stable UID per task; without it a re-imported backup would
|
||||||
|
duplicate every task instead of matching it.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -241,10 +262,11 @@ mechanisms, and conflating them is how apps end up over-permissioned:
|
|||||||
| `INTERNET` | **don't declare it until sync ships** |
|
| `INTERNET` | **don't declare it until sync ships** |
|
||||||
| `GET_ACCOUNTS` | never — stripped from the vendored provider; our own account type doesn't need it to see its own accounts |
|
| `GET_ACCOUNTS` | never — stripped from the vendored provider; our own account type doesn't need it to see its own accounts |
|
||||||
|
|
||||||
**Work item:** the permission gate in `RootScreen` / `PermissionViewModel` /
|
~~**Work item:** the permission gate … needs a bypass for our own provider.~~
|
||||||
`ProviderResolver.hasPermission` currently assumes an external provider always
|
✅ Done. `ProviderResolver.hasPermission` short-circuits to `true` when
|
||||||
needs a grant. It needs a bypass for our own provider. Modest, but it's the
|
`TaskProvider.isOwn`, so `ProviderStatus.NEEDS_PERMISSION` cannot fire in
|
||||||
exact flow `fix/provider-interaction-review` just touched — merge that first.
|
Local/Synced mode, and `PermissionViewModel` never offers our own permissions to
|
||||||
|
the request launcher. Covered by `ProviderResolverTest`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -259,7 +281,7 @@ task-domain, it goes to the kit.
|
|||||||
| ContentProvider seam — `ColumnReader`, failures, observer→Flow | `core-provider` | **already on the kit's deferred list**, blocked on migrating Calendula to the name-based reader. Bundling our own provider is the forcing function that makes this worth doing. |
|
| ContentProvider seam — `ColumnReader`, failures, observer→Flow | `core-provider` | **already on the kit's deferred list**, blocked on migrating Calendula to the name-based reader. Bundling our own provider is the forcing function that makes this worth doing. |
|
||||||
| Runtime-permission staging — request/state machine, rationale plumbing, "ask at point of use" | new, e.g. `core-permissions` | pure mechanics, and Calendula has the identical problem |
|
| Runtime-permission staging — request/state machine, rationale plumbing, "ask at point of use" | new, e.g. `core-permissions` | pure mechanics, and Calendula has the identical problem |
|
||||||
| DAV client + iCalendar parse/serialize | new, e.g. `core-dav` | **the big one.** Calendula is a calendar app; it needs the same primitives. Worth designing for two consumers from the start rather than extracting later |
|
| DAV client + iCalendar parse/serialize | new, e.g. `core-dav` | **the big one.** Calendula is a calendar app; it needs the same primitives. Worth designing for two consumers from the start rather than extracting later |
|
||||||
| Export/backup plumbing — SAF, file writing, share-out | kit | the *serialization* of tasks is domain; the plumbing isn't |
|
| Export/backup plumbing — SAF, file writing, share-out | kit | the *serialization* of tasks is domain; the plumbing isn't. **Built app-local for now** (`data/export/ExportWriter`), on the kit's own principle of not extracting before a second consumer exists — the seam is in place, so moving it is a file move. `ICalendarWriter` stays app-local permanently: it's domain. |
|
||||||
| Sync-adapter/account scaffolding | kit, probably | the `AbstractThreadedSyncAdapter` + authenticator boilerplate is identical everywhere; the delta logic is domain |
|
| Sync-adapter/account scaffolding | kit, probably | the `AbstractThreadedSyncAdapter` + authenticator boilerplate is identical everywhere; the delta logic is domain |
|
||||||
|
|
||||||
**Stays app-local:** the vendored `:provider` module (task-specific, and
|
**Stays app-local:** the vendored `:provider` module (task-specific, and
|
||||||
@@ -337,14 +359,33 @@ the roadmap should say so rather than inheriting the old estimate.
|
|||||||
## Open questions
|
## Open questions
|
||||||
|
|
||||||
1. **Sync protocol coverage**, account model, conflict resolution — the next
|
1. **Sync protocol coverage**, account model, conflict resolution — the next
|
||||||
discussion.
|
discussion. Still open.
|
||||||
2. **Resolver ordering / mode selection UX** once our provider coexists with
|
2. **Resolver ordering** — ✅ decided, see
|
||||||
external ones (see [Storage modes](#storage-modes--the-users-choice)).
|
[Storage modes](#storage-modes--the-users-choice). The **mode-selection UX**
|
||||||
3. **Does the vendored provider work with no account at all?** Local-only mode
|
is still open: `autoMode()` picks a default, but the Settings override it
|
||||||
depends on it entirely. First thing the vendoring work should prove.
|
assumes does not exist yet.
|
||||||
4. **`ical4android` licensing** vs our MIT.
|
3. **Does the vendored provider work with no account at all?** ✅ Answered, with
|
||||||
|
a caveat about *how* it was answered.
|
||||||
|
|
||||||
|
By construction: `cleanUpLists` exempts local lists explicitly (upstream's own
|
||||||
|
rule), and our rework restricts pruning to account types this package
|
||||||
|
authenticates — currently none — so nothing can be pruned at all.
|
||||||
|
`ProviderAccountCleanupTest` creates a local list and a task in it with zero
|
||||||
|
accounts present and reads both back.
|
||||||
|
|
||||||
|
⚠️ **But that test is Robolectric, and it skips on ARM64**, where Robolectric
|
||||||
|
has no SQLite backend in either mode. It runs on x86_64 CI. It is not a
|
||||||
|
substitute for a device, and this remains on the device-verification list.
|
||||||
|
4. **`ical4android` licensing** vs our MIT. Still open — and note the export path
|
||||||
|
does *not* depend on it: `ICalendarWriter` is our own ~200 lines, no library.
|
||||||
|
The question is really about the sync adapter's iCalendar *parsing*.
|
||||||
5. **jtx Board** as an additional External-mode candidate — richer contract,
|
5. **jtx Board** as an additional External-mode candidate — richer contract,
|
||||||
later. (`PLAN.md` decision #3, still open.)
|
later. (`PLAN.md` decision #3, still open.)
|
||||||
|
6. **The vendored provider's timezone-change behaviour** — upstream's receiver
|
||||||
|
has a comment describing `break`s that were never written. We preserved the
|
||||||
|
observed behaviour and wrote it out explicitly; which of code or comment is the
|
||||||
|
bug wants a device to settle. Change 3 in
|
||||||
|
[`provider/PROVENANCE.md`](../provider/PROVENANCE.md).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user