# D-Bus API: `org.liberux.Contacts1`

## Bus, service and path

| Item              | Value                          |
| ----------------- | ------------------------------ |
| Bus               | Session bus (per-user)         |
| Well-known name   | `org.liberux.Contacts1`        |
| Object path       | `/org/liberux/Contacts1`       |
| Interface         | `org.liberux.Contacts1`        |
| Activation        | D-Bus user service unit (`data/org.liberux.Contacts1.service`) and a systemd `Type=dbus` user unit. |

The service is single-instance per user. ATL talks to the same instance the native Liberux apps use; there is no per-app sandboxed bus.

## Design philosophy

The interface is **typed by entity**, not a generic SQL pass-through. Three reasons:

1. ATL needs to translate Android `Uri` + `ContentValues` calls into something semantically meaningful. A typed API maps cleanly: `content://com.android.contacts/data/N` ↔ `GetData( N )`.
2. Native Rust clients (eydos-calls) want compile-time-checked structs, not `a{sv}` dictionaries everywhere.
3. We control the D-Bus surface, so we can version it independently of the SQLite schema.

The `data` table's variable shape per mimetype is genuinely heterogeneous, so `Data` methods accept and return `a{sv}` dicts whose keys are documented per mimetype in `vcard-mapping.md` and `schema.md`. Top-level entities (`RawContact`, `Contact`) have fixed structs.

## Type vocabulary

The following composite types appear repeatedly; defined once here, referenced below.

### `RawContact` struct

D-Bus signature: `(uussssuxx)`

| Field              | D-Bus type | Notes                                       |
| ------------------ | ---------- | ------------------------------------------- |
| `id`               | `u`        | `raw_contacts._id`                          |
| `contact_id`       | `u`        | `raw_contacts.contact_id`                   |
| `account_name`     | `s`        | Empty string if NULL.                       |
| `account_type`     | `s`        | Empty string if NULL.                       |
| `display_name`     | `s`        | Empty string if NULL.                       |
| `sourceid`         | `s`        | Empty string if NULL.                       |
| `version`          | `u`        |                                             |
| `created_at`       | `x` (i64)  | ms since epoch.                             |
| `updated_at`       | `x` (i64)  | ms since epoch.                             |

Boolean flags `deleted` and `dirty` are intentionally not exposed in v1; they are sync-engine internals.

### `Contact` struct

D-Bus signature: `(usubuxx)`

| Field                 | Type   | Notes                                   |
| --------------------- | ------ | --------------------------------------- |
| `id`                  | `u`    | `contacts._id`                          |
| `display_name`        | `s`    |                                         |
| `photo_data_id`       | `u`    | 0 if no photo.                          |
| `starred`             | `b`    |                                         |
| `times_contacted`     | `u`    |                                         |
| `last_time_contacted` | `x`    | 0 if never.                             |
| `created_at`          | `x`    | ms since epoch.                         |

### `Data` row

D-Bus signature: `(usua{sv}bbu)`

| Field                | Type     | Notes                                                     |
| -------------------- | -------- | --------------------------------------------------------- |
| `id`                 | `u`      | `data._id`                                                |
| `mimetype`           | `s`      | Full `vnd.android.cursor.item/...` string.                |
| `raw_contact_id`     | `u`      |                                                           |
| `fields`             | `a{sv}`  | Decoded fields. Keys per mimetype (see schema.md).        |
| `is_primary`         | `b`      |                                                           |
| `is_super_primary`   | `b`      |                                                           |
| `data_version`       | `u`      |                                                           |

The `fields` dict uses **named keys**, not `data1..data15`. The encoding lives in one place (`db::data::row_to_fields` / `fields_to_row`) and is mimetype-aware. Photos use `photo` → `ay` (byte array).

Example for `…/phone_v2`:

```
{
    "number"            -> "+34 600 123 456" : s
    "normalized_number" -> "+34600123456"    : s
    "type"              -> 2                 : i  (Phone.TYPE_MOBILE)
    "label"             -> ""                : s  (only set when type=0)
}
```

### Error model

Errors are returned as D-Bus errors with the namespace `org.liberux.Contacts1.Error.*`:

| Error name                                | Meaning                                                  |
| ----------------------------------------- | -------------------------------------------------------- |
| `org.liberux.Contacts1.Error.NotFound`    | Referenced id does not exist.                            |
| `org.liberux.Contacts1.Error.InvalidArgument` | Caller-supplied data fails validation (e.g. unknown mimetype, missing required field). |
| `org.liberux.Contacts1.Error.Conflict`    | Optimistic concurrency: caller-supplied `version`/`data_version` is stale. |
| `org.liberux.Contacts1.Error.PermissionDenied` | Reserved for the future polkit hook (see "Permissions"). |
| `org.liberux.Contacts1.Error.Internal`    | DB or I/O error. The message is human-readable, not machine-parseable. |

## Methods

All methods listed below live on the single object `/org/liberux/Contacts1`, interface `org.liberux.Contacts1`.

### RawContacts

#### `GetRawContact( u id ) → (uussssuxx) raw_contact`

Returns the full `RawContact` struct. Errors `NotFound` if `id` does not exist or is soft-deleted.

#### `InsertRawContact( a{sv} fields ) → u id`

Creates a new raw_contact and the matching `contacts` row (v1 aggregation: 1:1).

`fields` keys (all optional):

| Key            | Type | Default                         |
| -------------- | ---- | ------------------------------- |
| `account_name` | `s`  | `""`                            |
| `account_type` | `s`  | `"org.liberux.local"`           |
| `sourceid`     | `s`  | `""`                            |
| `display_name` | `s`  | recomputed from later data rows |

Returns the new `raw_contacts._id`. Emits `Changed( contact_id, "added" )`.

#### `UpdateRawContact( u id, a{sv} patch )`

Partial update. Only keys present in `patch` are touched. Same key set as `InsertRawContact`. Bumps `version` and `updated_at`. Emits `Changed( contact_id, "updated" )`.

#### `DeleteRawContact( u id )`

Soft-deletes the raw_contact and (in v1, since 1:1) the corresponding `contacts` row, plus all attached `data` rows via FK cascade. Emits `Changed( contact_id, "deleted" )`.

### Data

#### `GetData( u id ) → (usua{sv}bbu) data`

Returns the `Data` row. Errors `NotFound` if absent.

#### `AddData( u raw_contact_id, s mimetype, a{sv} fields ) → u id`

Inserts a new `data` row. `fields` keys depend on `mimetype` (see schema.md). Unknown mimetypes are accepted but `fields` is restricted to keys `data1`..`data15` raw access in that case (forward compatibility). Emits `Changed( contact_id, "updated" )`.

#### `UpdateData( u id, a{sv} patch )`

Partial update of `fields`. To clear a field, set it to an empty variant of the appropriate type. Bumps `data_version`. Emits `Changed( contact_id, "updated" )`.

#### `DeleteData( u id )`

Removes the `data` row. Emits `Changed( contact_id, "updated" )`. If the row was a photo and was referenced by `contacts.photo_id`, that pointer is cleared.

#### `QueryData( u raw_contact_id, s mimetype ) → a(usua{sv}bbu)`

Returns all `data` rows of the given mimetype attached to the given raw_contact. `mimetype` may be `""` to match any.

### Contacts (aggregated)

#### `GetContact( u id ) → (usubuxx) contact`

Returns the aggregated `Contact` struct. Errors `NotFound` if absent.

#### `QueryContacts( s filter ) → a(uss)`

Returns `(id, display_name, photo_uri)` triples. `filter` is matched case-insensitively against `contacts.display_name` using `LIKE '%filter%'`. Empty `filter` returns every non-deleted contact, sorted by `display_name`.

`photo_uri` is `""` if there is no photo, otherwise a stable URI of the form `liberux-contacts:///data/<photo_data_id>` that resolves via a future `GetPhoto` call (placeholder in v1: any consumer may also fetch directly via `GetData`).

#### `QueryContactsFull( s filter ) → a(ussbsis)`

Like `QueryContacts`, but each entry also carries the contact's primary phone, so list views (e.g. a dialer's contact list) avoid a `QueryData` round-trip per contact. Same `filter` semantics: case-insensitive `LIKE '%filter%'` on `display_name`, empty matches all, sorted by `display_name`.

Each tuple is `(id, display_name, photo_uri, starred, number, phone_type, phone_label)`:

| Field         | Type | Notes                                                              |
| ------------- | ---- | ---------------------------------------------------------------- |
| `id`          | `u`  | `contacts._id`.                                                  |
| `display_name`| `s`  | Empty if NULL.                                                   |
| `photo_uri`   | `s`  | Same scheme as `QueryContacts`; empty if no photo.              |
| `starred`     | `b`  |                                                                  |
| `number`      | `s`  | Primary phone `number` (`data1`); empty if the contact has none. |
| `phone_type`  | `i`  | Android `Phone.TYPE_*` (`data2`); `-1` if no phone.             |
| `phone_label` | `s`  | Custom label (`data3`), only meaningful when `phone_type = 0`.   |

The "primary" phone is the contact's `is_super_primary` row, else `is_primary`, else the lowest-`_id` `…/phone_v2` row across its non-deleted raw_contacts.

#### `SearchByPhoneNumber( s number ) → au`

Returns the `raw_contacts._id` list of every raw_contact owning a `…/phone_v2` data row whose normalized number matches `number`. `number` is normalized client-side or server-side using the same rules; if the input is already in E.164 it is matched against `data4` directly. The match is "ends-with" on the last 7 digits, mirroring Android's `PhoneLookup` semantics.

### Batch

#### `ApplyBatch( a(s sa{sv}) ops ) → a(s a{sv}) results`

Each operation is a `(operation_kind, target, args)` triple:

| Operation kind   | `target` semantics                  | `args` keys                            |
| ---------------- | ----------------------------------- | -------------------------------------- |
| `"insert_raw"`   | Ignored (`""`).                     | Same as `InsertRawContact`.            |
| `"update_raw"`   | Decimal `raw_contact_id`.           | Same as `UpdateRawContact` patch.      |
| `"delete_raw"`   | Decimal `raw_contact_id`.           | None.                                  |
| `"add_data"`     | Decimal `raw_contact_id`.           | `mimetype` + per-mimetype fields.      |
| `"update_data"`  | Decimal `data_id`.                  | Patch of `fields`.                     |
| `"delete_data"`  | Decimal `data_id`.                  | None.                                  |

Back-references (Android's `withValueBackReference`) are encoded by passing the literal string `"@<index>"` (e.g. `"@0"`) as the target, where `<index>` is the zero-based index of a previous `insert_raw` in the same batch. The server resolves the placeholder to the id returned by that earlier insert.

The batch executes inside a single SQL transaction. On any error the transaction is rolled back and the call returns `Error.InvalidArgument` (or another error) referring to the failing operation index in the message body.

`results` is parallel to `ops`. Each entry is `(result_kind, payload)`:

| `result_kind`  | `payload`                                   |
| -------------- | ------------------------------------------- |
| `"id"`         | `{ "id": u }` — for inserts and adds.       |
| `"ok"`         | `{}` — for updates and deletes.             |

`ApplyBatch` increments `sync_state.change_counter` exactly once and emits exactly one `BatchChanged` signal (no per-operation `Changed` signals).

## Signals

### `Changed( u contact_id, s change_kind )`

Fired after a single-operation mutation. `change_kind` is one of `"added"`, `"updated"`, `"deleted"`.

`contact_id` refers to `contacts._id`. For data-level mutations it is the contact_id of the parent raw_contact.

Consumers translate this signal into:
- Android `ContentObserver.onChange()` (via ATL).
- GLib signals on the EDS-side address book (after EDS observes the CardDAV `ctag` change).

### `BatchChanged()`

Fired after `ApplyBatch` completes. Carries no payload — observers are expected to refresh whatever queries they are watching.

## Permissions

v1 ships **without** any permission check. Every connection on the session bus may call any method.

The dispatcher must contain a single hook point — a `permission_check( method_name, sender, args ) -> Result<()>` function called before every method body. In v1 it returns `Ok( () )` unconditionally. The hook is documented with a `TODO( permissions ): wire polkit here once ATL surfaces a per-app sender identity` comment.

This deliberately keeps the surface tight so wiring polkit later does not require touching call sites.

## Stability and versioning

The interface name `org.liberux.Contacts1` is the v1 surface. Breaking changes will use a new interface name (`Contacts2`) on the same object path; both can coexist during a transition window. Additive changes (new methods, new optional `a{sv}` keys) do not bump the version.

## Introspection example

A condensed `org.freedesktop.DBus.Introspectable` reply looks like:

```xml
<interface name="org.liberux.Contacts1">
    <method name="GetRawContact">
        <arg type="u"        name="id"          direction="in"/>
        <arg type="(uussssuxx)" name="raw_contact" direction="out"/>
    </method>
    <method name="InsertRawContact">
        <arg type="a{sv}"    name="fields"      direction="in"/>
        <arg type="u"        name="id"          direction="out"/>
    </method>
    <!-- ... others elided ... -->
    <signal name="Changed">
        <arg type="u" name="contact_id"/>
        <arg type="s" name="change_kind"/>
    </signal>
    <signal name="BatchChanged"/>
</interface>
```
