Steps 2 and 3 of docs/STORAGE-AND-SYNC.md. Agendula stops depending on a tasks provider app being installed: it now carries one. The module New :provider — the dmfs task provider 1.4.2 (Apache-2.0, DB 23), vendored in-tree, renamed to authority de.jeanlucmakiola.agendula.tasks and permissions de.jeanlucmakiola.agendula.permission.*. It coexists with OpenTasks and tasks.org rather than replacing them; nothing collides with org.dmfs.*, so both can be installed at once. The contract shape is untouched — same tables, same columns — because that is what our data layer and every CalDAV engine already speak. We own the namespace it lives in, not the schema. Vendored rather than depended on because the permission names are hardcoded in the upstream AAR's manifest and cannot be renamed in a prebuilt artifact; in-tree also satisfies F-Droid's from-source rule. provider/PROVENANCE.md records the upstream commit and every deviation, each marked with an AGENDULA CHANGE comment at the site so the list and the code cannot drift apart. The change that matters most is the account cleanup. Upstream holds GET_ACCOUNTS and deletes any task list whose account it cannot see. We dropped that permission — we only ever need our own accounts, which are visible without it — but an account we cannot see is indistinguishable from one that was removed, so left alone the provider would quietly delete synced lists. Cleanup is now restricted to account types this package authenticates itself, which is currently none. ProviderAccountCleanupTest pins that, and answers open question 3: the local path works with no account present at all. Also required by targetSdk 36, none of which upstream faced at 29: FLAG_IMMUTABLE on the notification PendingIntent, an inexact-alarm fallback so a revoked SCHEDULE_EXACT_ALARM cannot kill the app on a timezone change, and an explicit android:exported on the receiver. Storage modes ProviderResolver gains a StorageMode: LOCAL (our provider) or EXTERNAL (an installed one). Not a third SYNCED value — synced is LOCAL with an account attached, which is derived state, and modelling it as a separate store would imply switching sync on is a migration. It isn't. When the user has not chosen, the tell is whether we already hold an external provider's runtime permission. That permission is dangerous-level, so it can only be there because an earlier version asked and they agreed — the signature of an existing Posture A user, who must not be dropped onto an empty store. Fresh installs get local-first. hasPermission now short-circuits for our own provider: same-uid access bypasses the check outright, so ProviderStatus.NEEDS_PERMISSION can no longer fire in Local mode. That was the work item the storage-and-sync doc called for. The resolver's platform calls moved behind ProviderEnvironment so the decision — the part that loses people their data if wrong — is unit-tested on the JVM. Verified: 51 vendored provider tests pass, app tests pass, lintDebug and assembleDebug clean. ProviderAccountCleanupTest skips on ARM64, where Robolectric has no SQLite backend, and runs on x86_64 CI. Not yet exercised on a device. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
193 lines
11 KiB
Markdown
193 lines
11 KiB
Markdown
# `:provider` — provenance
|
||
|
||
This module is **not our code**. It is the dmfs task provider, vendored, with
|
||
its namespace renamed to ours and a short list of changes recorded below.
|
||
|
||
| | |
|
||
|---|---|
|
||
| Upstream | [dmfs/opentasks](https://github.com/dmfs/opentasks) |
|
||
| Module taken | `opentasks-provider`, plus `opentasks-contract` (see [Why the contract came along](#why-the-contract-came-along)) |
|
||
| Version | `1.4.2` |
|
||
| Commit | `49ebf80b1eeee52a611e5a22f24f849852a6255f` (2021-03-21) |
|
||
| License | Apache-2.0 — see [`LICENSE`](LICENSE) and [`NOTICE`](NOTICE), both upstream's, unmodified |
|
||
| Database version | **23** |
|
||
|
||
Agendula itself is MIT. Apache-2.0 into MIT is fine in that direction, but this
|
||
module keeps its own `LICENSE`, `NOTICE`, and per-file Apache headers, and those
|
||
must survive any future edit here.
|
||
|
||
**1.4.2 specifically, for the database version.** DB 23 is the first to carry
|
||
`is_recurring`. tasks.org's fork is DB 22 and lacks it — which is why
|
||
`TaskMapper.task` on the app side derives recurrence from `rrule`/`rdate`
|
||
instead of trusting that column, and why it must keep doing so as long as
|
||
External mode supports tasks.org.
|
||
|
||
## Why vendored at all
|
||
|
||
Recorded properly in [`docs/STORAGE-AND-SYNC.md`](../docs/STORAGE-AND-SYNC.md);
|
||
in one line: **the permission names are hardcoded in the upstream AAR's
|
||
manifest.** No prebuilt artifact — Maven Central, JitPack, anything — can have
|
||
them renamed without `tools:` node surgery, and shipping under dmfs's own
|
||
permission names would make Agendula and OpenTasks mutually uninstallable
|
||
(`INSTALL_FAILED_DUPLICATE_PERMISSION`). In-tree also satisfies F-Droid's
|
||
from-source requirement, which a JitPack artifact would not.
|
||
|
||
In-tree rather than a git submodule, unlike floret-kit: we co-develop the kit,
|
||
whereas this is a fork we expect to resync from upstream approximately never.
|
||
|
||
## The namespace rename
|
||
|
||
Everything in this table is a rename and nothing more. The **contract shape is
|
||
untouched** — same tables, same column names, same URI paths — because that
|
||
shape is what our data layer, and every CalDAV engine, already speaks. We own
|
||
the namespace it lives in, not the schema.
|
||
|
||
| | Upstream | Ours |
|
||
|---|---|---|
|
||
| Authority | `org.dmfs.tasks` | `de.jeanlucmakiola.agendula.tasks` |
|
||
| Read permission | `org.dmfs.permission.READ_TASKS` | `de.jeanlucmakiola.agendula.permission.READ_TASKS` |
|
||
| Write permission | `org.dmfs.permission.WRITE_TASKS` | `de.jeanlucmakiola.agendula.permission.WRITE_TASKS` |
|
||
| Permission group | `org.dmfs.tasks.permissiongroup.Tasks` | `de.jeanlucmakiola.agendula.permissiongroup.Tasks` |
|
||
| Notification-alarm action | `org.dmfs.tasks.provider.NOTIFICATION_ALARM` | `de.jeanlucmakiola.agendula.provider.NOTIFICATION_ALARM` |
|
||
| Resource prefix | `opentasks_*` | `agendula_*` |
|
||
| R class | `org.dmfs.tasks.provider.R` | `de.jeanlucmakiola.agendula.provider.R` |
|
||
|
||
### What was deliberately *not* renamed
|
||
|
||
- **Java package names** stay `org.dmfs.provider.tasks` / `org.dmfs.tasks.contract`.
|
||
They are not a registered namespace — two apps may share them freely — and
|
||
keeping them means the diff against upstream stays legible. Only the AGP
|
||
`namespace` (which decides where `R` lands) is ours.
|
||
- **`TaskContract.LOCAL_ACCOUNT_TYPE`** stays `"org.dmfs.account.LOCAL"`. It is a
|
||
value stored in the database and recognised by dmfs-contract providers
|
||
generally, so the *same* app code has to write it whether it is talking to our
|
||
provider or, in External mode, to OpenTasks. Renaming it would fork the write
|
||
path in two for no gain.
|
||
- **`ACTION_BROADCAST_TASK_DUE` / `…_TASK_STARTING` / `ACTION_DATABASE_INITIALIZED`.**
|
||
Every send site calls `setPackage()` on its own package first, so these never
|
||
cross app boundaries and cannot collide with an installed OpenTasks.
|
||
|
||
## Changes to upstream source
|
||
|
||
Four files under `src/main/java`, one under `src/test/java`. Each edit is marked
|
||
with an `AGENDULA CHANGE` comment at the site, so this list and the code cannot
|
||
drift apart. Keep that convention.
|
||
|
||
### Behavioural
|
||
|
||
1. **`Utils.cleanUpLists` — only prune account types we authenticate ourselves.**
|
||
*The one change here that is about correctness rather than mechanics.*
|
||
|
||
Upstream holds `GET_ACCOUNTS` and enumerates every account on the device. We
|
||
dropped that permission (below), so `AccountManager` only ever reports
|
||
accounts of our own type. Upstream's cleanup deletes any task list whose
|
||
account is absent from that array — and an account we *cannot see* is
|
||
indistinguishable from one that has been *removed*. Left alone, the provider
|
||
would quietly delete synced lists. Not "sync stops": data disappears, with no
|
||
error anywhere.
|
||
|
||
Now a list is only prunable when its account type belongs to an authenticator
|
||
in **this package**. Agendula ships no authenticator yet, so the set is empty
|
||
and nothing is ever pruned; our sync adapter's type will join it on its own
|
||
when it lands, no edit needed here. Local lists were already exempt upstream.
|
||
|
||
2. **`TaskProvider.insert` — the same restriction for the stale-list signal.**
|
||
Upstream flags "list with unknown account" and broadcasts about it. With the
|
||
account cache holding only our own accounts, that fired on every insert into
|
||
any externally-synced list. Nothing listens today; the point is that whatever
|
||
listens tomorrow gets a signal that means something.
|
||
|
||
3. **`TaskProviderBroadcastReceiver.onReceive` — fall-through written out.**
|
||
Upstream's `switch` has no `break` in any branch, so `TIMEZONE_CHANGED` runs
|
||
all three content operations and `NOTIFICATION_ALARM` runs the last two. The
|
||
comment on the first branch ("don't trigger the notifications update yet")
|
||
describes breaks that were never written, so the code and the stated intent
|
||
contradict each other.
|
||
|
||
**Observed behaviour is preserved exactly**, just spelled out with `if`s
|
||
rather than reached by accident. A vendored fork is the wrong place to guess
|
||
at intent. ⚠️ **Open:** which of the two is the bug wants a device with a task
|
||
due across a timezone change to settle.
|
||
|
||
4. **`TaskProviderBroadcastReceiver.planNotificationUpdate` — inexact-alarm fallback.**
|
||
`setExact` throws `SecurityException` on API 31–32 when the user revokes
|
||
`SCHEDULE_EXACT_ALARM` — inside a receiver handling a system broadcast, so the
|
||
app would die on every timezone change. Falls back to `set` when exact alarms
|
||
aren't permitted. This alarm drives only the provider's own bookkeeping;
|
||
Agendula's user-visible reminders come from `ReminderScheduler`, which asks
|
||
for the permission properly.
|
||
|
||
### Required by modern Android (would not build or would crash otherwise)
|
||
|
||
5. **`PendingIntent.FLAG_IMMUTABLE`** added in `planNotificationUpdate`. Mandatory
|
||
since Android 12; throws `IllegalArgumentException` without it at targetSdk ≥ 31.
|
||
Upstream targets 29. Nothing mutates the intent later, so immutable is also
|
||
correct on the merits.
|
||
6. **`android:exported`** stated explicitly on the receiver. AGP hard-errors on an
|
||
intent-filtered component without it at targetSdk ≥ 31.
|
||
7. **`package=` attribute** removed from the manifest; AGP 8+ takes it from the
|
||
`namespace` in the build file.
|
||
8. **`<uses-sdk tools:overrideLibrary="org.dmfs.android.contenttestpal"/>`** removed
|
||
along with the androidTest sources it existed for (below).
|
||
|
||
### Permissions
|
||
|
||
9. **`android.permission.GET_ACCOUNTS` dropped.** We only ever need to see
|
||
accounts of our own type, and since API 26 an authenticator makes those
|
||
visible to its own package with no grant at all. Safe **only** in combination
|
||
with change 1 — the two must be read together.
|
||
|
||
### Build and test
|
||
|
||
10. **`build.gradle` → `build.gradle.kts`**, on the root version catalog. minSdk
|
||
21 → 29 (matching `:app`; the merger rejects a lower floor), Java 8 → 17.
|
||
11. **Test stack modernised**, sources otherwise untouched: JUnit 4.12 → 4.13.2,
|
||
Robolectric 3.5.1 → 4.16, Mockito 2.27 → 5.20, Hamcrest 1.3 → 3.0.
|
||
`org.dmfs:jems`, `rfc5545-datetime` and `lib-recur` stay on the versions
|
||
upstream pinned — all three resolve from Maven Central, so no new repository
|
||
was added (`settings.gradle.kts` is still `google()` + `mavenCentral()` under
|
||
`FAIL_ON_PROJECT_REPOS`).
|
||
12. **`ZippedTest.testAbsent`** — diamond `new Zipped<>` given an explicit type
|
||
argument. `absent()` pins no type, and javac 17 will not infer what javac 8
|
||
did. The assertion is unchanged.
|
||
13. **`src/test/resources/robolectric.properties` added** (`sdk=34`,
|
||
`conscryptMode=OFF`). A library module has no `targetSdk` for Robolectric to
|
||
read, and Robolectric installs Conscrypt unconditionally, whose uber jar has
|
||
no `linux-aarch_64` native — so without this the suite fails at setup on ARM64
|
||
machines while passing on x86_64 CI. See the file for the reasoning.
|
||
14. **`src/androidTest` dropped entirely.** It depends on `contentpal` /
|
||
`contenttestpal`, which are JitPack-only; adding JitPack would widen the
|
||
dependency trust surface for test-only code. ⚠️ This is the one place
|
||
vendoring lost coverage — those were the provider's *integration* tests
|
||
(recurrence, reparenting, instances, observers). The 51 JVM tests in
|
||
`src/test` all pass and are retained.
|
||
15. **`agendula_provider_changed_receivers` emptied.** Upstream notifies
|
||
`org.andstatus.todoagenda`, which listens for changes to the *dmfs* authority
|
||
and has never heard of ours. Anything re-added here also needs a `<queries>`
|
||
entry in the app manifest or package-visibility rules drop the broadcast.
|
||
16. **Translated `agendula_provider_label` overrides removed** (the other
|
||
translated strings are kept as upstream shipped them). The base label became
|
||
"Agendula tasks" so it is distinguishable from OpenTasks' own "Tasks" entry in
|
||
the system permission dialog; the inherited translations still said plain
|
||
"Tasks" in their language, which would have contradicted it.
|
||
|
||
## Resyncing from upstream
|
||
|
||
Unlikely to ever be worth it — upstream 1.4.2 is from 2021 — but if it is: the
|
||
`AGENDULA CHANGE` markers are the complete list of what to reapply, `git log` on
|
||
this directory is the audit trail, and the 51 JVM tests are the safety net.
|
||
Re-read change 1 before touching anything account-related.
|
||
|
||
## Known-unverified
|
||
|
||
Everything here is verified by the JVM test suite and a clean build. What is
|
||
**not** yet verified on a device with real data:
|
||
|
||
- The local-list path with **no account present at all** — the entirety of Local
|
||
mode. `cleanUpLists` exempts local lists explicitly and change 1 makes the
|
||
prunable set empty, so it should hold by construction; it is covered by
|
||
`ProviderAccountCleanupTest`, but that is Robolectric, not a device.
|
||
- The timezone-change behaviour in change 3.
|
||
- Any interaction with an external sync engine writing into our authority
|
||
(nothing does yet — that is the DAVx5 ask, step 4 of the sequencing).
|