mirror of
https://github.com/bulwarkmail/native.git
synced 2026-11-02 08:07:11 +00:00
e67e2408407f946fb5d5bac675fc80890cc16fb3
7
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
e67e240840 |
feat: ask android to sync when contacts or calendars change #34
With device sync on, Android should hear about a change as soon as the
app does, not only at the next periodic sync. This adds what tells it:
- The app's own contact and calendar edits, StateChanges on the live
stream for `AddressBook`/`ContactCard` and `Calendar`/`CalendarEvent`,
and the app coming back to the foreground request a sync of that
account and authority. Requests within 5 s fold into one, and a
StateChange the engine caused itself (its own upload echoing back) is
skipped.
- While the app is closed, the account's push subscription carries
those types too. The FCM and UnifiedPush services hand every push to
the native router from
|
||
|
|
a948738506 |
feat: map contacts and groups to android's contacts provider #34
This is the planner for contacts (`src/device-sync/contacts`), the last piece the sync task was waiting for: it turns JSContact cards into raw contacts and data rows, and device edits back into JMAP writes. - Names, nicknames, organizations and titles, emails, phones, postal addresses, websites, birthdays and anniversaries (also without a year), relations, notes and photos map both ways, with their types and custom labels. Group cards become groups, and their members become group memberships. - Device edits upload as patches of the fields whose rows changed; a whole map goes up only where Stalwart needs one. Whatever a card holds that Android can't show (pronouns, online services, keywords, a second note, an address's coordinates) is never touched, so an edit on the phone leaves it intact on the server. - Rows are matched to their card's entries by the keys they carry and, where an editor re-inserted them without (Fossify saves that way), by their values. An entry the editor didn't show is never read as deleted, and the keys Stalwart assigns by position to cards from other clients are followed when they shift. - Contacts in read-only address books are put back when the device changes them. Text beyond what the provider stores is cut on the device and never uploaded. Checked: 151 contacts tests (mappings both ways, one test per edited field, Fossify and AOSP editing, re-keyed cards, merges, groups, read-only books, a full lifecycle), the engine's tests with both real planners, and `npm run typecheck`, `npm test` and `npm run i18n:check` on this commit. |
||
|
|
3ed5fa53af |
feat: map calendars and events to android's calendar provider #34
The engine hands every event it syncs to a planner that turns it into CalendarContract rows, and device edits back into JMAP writes. This is that planner for calendars (`src/device-sync/calendar`), now used by the sync task: - Calendars become calendar rows with what the apps need (SYNC_EVENTS=1, an owner, colours, the reminders allowed); calendars of shared accounts carry the account's name. - Events keep their time zone, floating and all-day times, recurrence rules, excluded and changed instances, reminders, attendees and the user's own reply. A rule Android can't hold (RSCALE, SKIP, leap months) stays on the server untouched, and local times Stalwart can't resolve (a start in a repeated hour, an UNTIL in a skipped one) are written in a form it keeps. - Device edits upload as patches of what differs from the last synced state, never a whole event, and only what the device can show is ever sent. A single-instance edit or deletion becomes an override of its series; "this and following" caps the series and creates a new one. How Etar and Fossify save (a cancelled instance instead of an exclusion, attendee rows deleted and re-inserted) is read as the edit it stands for, not as a deletion. Invitations go out on the changes a calendar app would notify about. - Events in read-only calendars and subscribed feeds are put back when the device changes them; only the user's own reply goes up, where the server allows it. Tasks, task-only calendars and the birthday calendar never reach the device. - Reminders are written as Reminders rows when the calendar app is to remind the user, and left out when Bulwark does. Checked: 179 calendar tests (download, upload, exceptions, the edits Etar, Fossify and CONTENT_EXCEPTION_URI make, echoes that write nothing, time zone and DST edges), and `npm run typecheck`, `npm test` and `npm run i18n:check` on this commit. |
||
|
|
96a6b38321 |
feat: sync the device's contacts and calendars with the server in the background #34
The sync adapters from
|
||
|
|
f743ab0f7d |
feat: give device sync scoped access to the contacts and calendar providers #34
The engine plans its provider reads and writes in JS, but only native code can reach ContactsProvider and CalendarProvider. This adds that bridge to the BulwarkDeviceSync module, with the rest of what the engine and the app need from it: - `query`, `applyBatch`, `readSyncState` and `readPhoto` take the `ProviderQuery`/`ProviderOp[]` JSON of `src/device-sync/types.ts`, check it (`sync/ProviderOps.kt`) and run it for one Android account of our type (`sync/ProviderScope.kt`, `sync/ProviderIo.kt`). Every selection gets the account ANDed in, every row an op names by id or points to as a parent must be the account's, and every URI carries `caller_is_syncadapter`, so nothing the sync writes marks rows dirty or schedules an upload. The calls run one at a time on the module's own thread through an unstable provider client, so a long batch never stalls other module calls and a provider that dies fails the call instead of the app. - `showSyncProblem` and `clearSyncProblem` post and clear the one notification per account that needs the user, e.g. "Sign in again". - `setPushRoutes` stores which Android accounts each JMAP account feeds, for the push router in `sync/PushRouting.kt`; the push services start using it in a later commit. - A run now ends exactly once and refuses a report that comes after that. A sync requested while a run is open makes the run ask for another sync when it ends, because SyncManager drops such requests as duplicates. `onPerformSync` lets nothing escape any more: an exception there killed the whole process, UI included. - `ensureAccount` no longer hands an existing Android account to another app account (`conflict`): its rows and SyncState belong to the first. Payloads cross as JSON strings, which the bridge moves much faster than nested maps. `src/device-sync/native.ts` is the typed JS side. The fake provider follows the rules the emulator runs turned up: contacts `data1`..`data14` read back as text, events and calendars the sync adapter inserts have DIRTY NULL, attendee rows are addressed by id, an op by id expects one row, and a vanished parent fails with `assert` while another account's fails with `scope`. Checked: 54 JVM tests (`gradlew :app:testDebugUnitTest`), debug and release (R8) builds, `npm run typecheck` and `npm test` on this commit, and on the API 32 emulator through the debug receiver: inserts with back-references and group memberships, SyncState, photo writes and scaled reads, asserts and expected counts, both scope refusals, the 499-op yield boundary, `tooLarge` for a 1.3 MB batch, and the notification. |
||
|
|
59fcf386b9 |
test: add a fake device provider and jmap server for the sync tests #34
The device sync engine reaches Android's providers through a native bridge and the server through a detached JMAP client, and neither runs in vitest. This adds the stand-ins the engine and the mappers are tested against, and the contracts they implement: - `src/device-sync/types.ts`, `planner.ts`, `wire.ts`, `android-columns.ts`: the bridge, the provider op model, the JMAP port, and the interface between the engine and the contacts and calendar planners - a fake ContactsProvider and CalendarProvider with the behaviour the design relies on: the DIRTY and VERSION rules, soft and hard deletes, exception linking, normalisation, asserts, the yield-point limit, and a "user" face that edits the way AOSP Contacts, Fossify and Etar do - a fake Stalwart with `/changes` paging and compaction, `cannotCalculateChanges`, `ifInState`, uid checks behind a lagging search index, positional re-keying and fault injection (lost responses, 429, auth, SetErrors) - shared pure helpers (canonical JSON, PatchObjects with Stalwart's missing-parent rule, key and uid minting) and a test that walks the import graph, so no mapper or engine module pulls in React Native - `src/stores/device-sync-store.ts`: the per-account preferences and last run status the engine and the settings will share Checked: `npx vitest run src/device-sync` (91 tests), the full suite (2209 tests) and `npm run typecheck`. |
||
|
|
ed2525e5cb |
feat: register a bulwark account with android's contacts and calendar sync #34
Android syncs the contacts and calendars of other services through
accounts and sync adapters. This adds the Android side of device sync:
a "Bulwark" account type (`${applicationId}.account`, set in
build.gradle), an authenticator that sends Settings → Add account to
the app instead of asking for credentials, and one sync adapter each
for the contacts and the calendar provider.
An adapter does no syncing itself. It starts the BulwarkDeviceSync
headless JS task from the main thread, through the helper the push job
uses, waits for the task's report and hands it to SyncManager as the
SyncResult. The engine that will write the report runs in JS, so the
existing client stays the only owner of token refresh, mTLS and
session discovery; for now the task only reports that device sync is
off. Nothing in the app creates an account yet, so the feature stays
unreachable until its settings land. Debug builds get an adb hook for
the device tests (DeviceSyncDebugReceiver, shell only).
Hard errors are reported without progress: SyncManager retries a
failed sync at once when it made progress, which would loop an auth
failure.
Checked on the API 34 and API 32 emulators with the debug build, from
logcat and `dumpsys content`:
- app in the foreground: report after 319-334 ms
- app in the background: 320-327 ms
- process killed: SyncManager starts it for the sync service, React
Native starts headless, and the report arrives after 16.2 s (API 34)
and 8.5 s (API 32), most of it the debug bundle download from Metro
- an io report is retried with back-off and an auth report is not
retried; too many deletions shows the system's "Attempted to delete
too many Contacts." notification
- with Metro unreachable the adapter gives up after 2 s instead of
waiting for its deadline
- `gradlew :app:testDebugUnitTest`: 10 JVM tests for the report mapping
|