feat(android): sync device albums with a server album

Add phone album sync to the Android client: MediaStore source (device
albums), a Room index with upload provenance, on-device previews, and a
mixed server + device album list where a linked pair collapses into a
single device card with a link indicator and mode label.

Three modes over one pipeline: A (off), B (upload copies, originals stay
local), C (upload originals and offload them locally to previews). The
sync engine tracks device changes (add/remove/rename/move/content), matches
assets to server photos by sha256 content hash, deletes only this client's
uploads, and reflects server changes without touching local originals.
Offload is automatic; the zoomed viewer pulls the server original.

Backend: expose the original content checksum in photo data (Postgres and
SQLite), additive with no migration.

Also anchor the `.gitignore` `data/` rule to `/data/` so the Android
`com.galo.android.data` package is tracked, and add the halo launcher icon.
This commit is contained in:
iu
2026-10-13 17:10:47 +03:00
parent b7316f9e00
commit 38b5b03f59
102 changed files with 7199 additions and 153 deletions
+42
View File
@@ -100,6 +100,48 @@ While an album screen is active the client holds
`GET /api/sse/albums/:id?token=` and backfills missed changes via
`GET /api/albums/:id/photos?since=` after a reconnect.
## Device albums and synchronization
The album list mixes the server's albums with the device's own albums (MediaStore
buckets). Each card is tagged with its source (`Сервер` / `Телефон`), and a linked
pair shows the connection both ways: the device album shows `→ <server album>`, the
server album shows `источник: <device albums>`.
- **Access.** Enumerating device albums needs media read access: `READ_MEDIA_IMAGES`
and `READ_MEDIA_VIDEO` on Android 13+, or `READ_EXTERNAL_STORAGE` on Android 12
and lower. The app requests it at runtime; without (or with partial) access the
device albums are simply not shown, and server albums keep working.
- **Synchronization modes** are chosen per device album from the album screen
(swipe right, or the settings icon):
- **Выключена (A)** — local only. Previews are generated on the device and cached
in the app's private cache; the album makes **no server requests**.
- **Копии оригиналов на сервере (B)** — originals stay on the device, copies are
uploaded to a chosen server album.
- **Перенос оригиналов на сервер (C)** — originals move to the server; the device
keeps previews. Freeing space replaces the original in MediaStore only after the
user confirms the OS write request, in batches.
- Previews are capped at a 1600px long side (JPEG q80) and generated on the device,
including a poster frame for videos.
- **Deduplication.** A device asset is matched to a server photo by the sha256 of the
bytes that leave the device (the server exposes each original's checksum). Content
already present is linked instead of re-uploaded; an edited asset is re-uploaded and
its previous upload removed; a pure rename is propagated via `PATCH /api/photos/:id`.
- **Deletions.** The server photo is deleted only when this client uploaded it, its
content is gone from the device and no other linked album still holds it. Photos the
client did not upload are never touched, and a photo already gone from the server is
only reflected in the local index.
- **Background.** Synchronization runs on app start and while the app is open (a
MediaStore content observer), and periodically in the background via WorkManager.
The periodic pass honors the **Только Wi-Fi** setting. Freeing space in mode C is
never automatic — it always waits for the user's confirmation.
- **Provenance.** Mode C writes a marker into the EXIF of the preview it leaves
behind (server album/photo, original hash, mode). After a reinstall the app reads
these markers to restore the album link and which uploads are its own; unmarked
media is left alone.
- **Reversibility.** Leaving mode C warns first and offers to fetch the originals
back from the server (space permitting); declining keeps previews only. Unlinking
an album drops the local link and never deletes photos on the server.
## Testing
```sh