Files
ds-galo/android/README.md
T
iu 3e8a6b87b7 feat(android): сортировать весь список и добавить обратный порядок
Выбор порядка раньше действовал только на серверные альбомы: карточки устройства
были закреплены сверху и не сортировались, а направления не было вовсе.

- порядок применяется ко всему списку: карточки устройства и серверные
  упорядочиваются вместе единым компаратором
- альбом устройства получает дату последнего изменения (самый свежий элемент,
  как при выборе обложки), поэтому «по времени изменения» осмыслен и для него
- обратный порядок инвертирует выбранный ключ; карточка без даты остаётся
  последней в любом направлении
- пара (ключ, направление) хранится в DataStore за узким портом
  AlbumSortPreference; меню сортировки получило переключатель «Обратный порядок»
- README: раздел Album sorting обновлён
2026-10-16 13:27:20 +03:00

218 lines
11 KiB
Markdown

# galo Android client
Native Android client for a self-hosted [galo](../README.md) instance: browse
albums and media, watch photos/videos/360 panoramas, follow live album updates,
and upload from the device with a persistent background queue and offline cache.
Kotlin + Jetpack Compose, `minSdk 28`, `targetSdk 35`. The module is standalone:
it does not touch `backend/` or `frontend/`, and consumes the existing HTTP API
without server changes.
## Requirements
- JDK 17
- Android SDK (`platforms;android-35`, `build-tools;35.0.0`, `platform-tools`)
- `ANDROID_HOME` pointing at the SDK, or `android/local.properties` with
`sdk.dir=<path>`
## Build
```sh
cd android
./gradlew :app:assembleDebug # debug APK
./gradlew test # JVM unit + Robolectric UI tests
./gradlew lintDebug # Android lint
./gradlew :app:assembleRelease # unsigned release APK without a keystore
```
From the repository root:
```sh
make android # runs :app:assembleDebug (ANDROID_SDK=... to override)
make install-android-app # build and install the debug APK on a device
```
`install-android-app` builds the debug APK and installs it onto the sole connected
device. To choose a device explicitly, pass its serial number:
```sh
make install-android-app SERIAL=emulator-5554
```
`SERIAL` also falls back to `$ANDROID_SERIAL`. When several devices are connected and
`SERIAL` is not set, the target prints the list of devices (only those in the `device`
state) and stops without installing. `ADB=...` overrides the `adb` binary (defaults to
`$(ANDROID_SDK)/platform-tools/adb`).
Debug output: `android/app/build/outputs/apk/debug/app-debug.apk`.
## Signing (release)
The release keystore and credentials live **outside** the repository. Point the
build at a properties file with `GALO_KEYSTORE_PROPERTIES=/path/keystore.properties`,
or place an ignored `android/keystore.properties`:
```properties
storeFile=/secure/galo-release.jks
storePassword=...
keyAlias=galo
keyPassword=...
```
Without it, `assembleRelease` still resolves and produces an unsigned APK; no
secrets are stored in the repository.
## First run
1. Enter the instance base URL (e.g. `https://photos.example.com`) and confirm —
the app checks `GET /api/health`.
2. Plain `http://` is blocked by default. For a trusted LAN instance, enable
**Небезопасное соединение (только LAN)**; the UI then marks the connection as
insecure. The manifest permits cleartext, but the client itself refuses HTTP
until you opt in.
3. Log in or register. Tokens are stored in the platform Keystore
(EncryptedSharedPreferences) and refreshed transparently on `401`.
## Upload notes
- Uses the system Photo Picker; pick files, then choose the destination album.
- One file per multipart POST (the server's batch upload is fail-fast and caps
the total body size), so each file gets its own status and retry.
- Queue is persisted in Room and resumes after an app/device restart. Large and
video uploads show a foreground progress notification; transient failures
retry with exponential backoff (up to 3 attempts).
- Optional **Только Wi-Fi** limits uploads to unmetered networks; jobs wait in
the queue until Wi-Fi is available.
- **HEIC/HEIF** originals are converted to JPEG on-device (the server decodes
only JPEG/PNG/GIF/BMP/TIFF). Capture date and geolocation are carried over via
EXIF and the base filename is preserved. Note: for HEIC sources the stored
"original" is the converted JPEG.
## Offline cache
Album metadata and photo pages are cached in Room; media is cached by Coil keyed
by the object path without the signed query, so the cache is reused when the
signature rotates. Albums and photo grids open from cache when offline.
## Live updates
While the app is signed in the client holds the personal stream
`GET /api/sse/me` (authorized by the bearer header, not a query token) and applies
album lifecycle events (`album.created`, `album.updated`, `album.deleted`) straight
to the local cache, so the album list and an open album screen follow changes made
elsewhere without a manual refresh. After a reconnect the list is re-synced over
REST, which also drops albums deleted while the stream was down. When an album is
deleted or the user is removed from it, the album leaves the list and the open
album screen returns to it.
While an album screen is active the client also holds `GET /api/sse/albums/:id` and
backfills missed photo changes via `GET /api/albums/:id/photos?since=` after a
reconnect.
The streaming client does not refresh tokens by itself: it is built without the
authenticator that ordinary API calls use, and the authorization interceptor only
reads the current access token. A reconnect after expiry therefore restarts with
whatever token is stored, and recovers once any other request has refreshed it; a
client that idles with only the stream open keeps retrying with the stale token
until the next API call renews it.
## Attributes and actions
The mobile client mirrors the web workspace's attribute/action management.
- **Album panel.** Swiping right-to-left on any album screen (server or device)
opens the «Атрибуты и действия» panel with the sections «Обложка», «Информация»,
«Навигация» and «Действия» (rename, delete). Cover and navigation are limited to
owner/admin. For a device album linked to a server album, the panel shows the
linked server album's real attributes; an unlinked device album shows local
values (cover, name, item count) without the navigation section.
- **Photo sheet.** Swiping up in full-screen viewing (server or device) opens the
«Атрибуты и действия» sheet with «Информация», «Метки» and «Действия» (download,
move, copy, set as cover, delete) plus rename by tapping the file name. The sheet
does not open while an image is zoomed, a panorama rotates, or video controls are
in use. For device media linked to a server photo, the sheet shows that photo's
server attributes and actions; otherwise it shows local properties.
- **Attribute settings.** The «Информация» section reaches a screen listing the EXIF
attributes with visibility checkboxes and label fields («отметить все»/«снять
все»); the choice is stored in the profile's `photo_info` preferences, using the
same shape as web, so the two clients stay in sync.
API operations backing these panels (all already served by the backend):
- `PATCH /api/albums/:id` (`title`, `description`, `cover_photo_id` / `clear_cover`),
`DELETE /api/albums/:id`, `GET /api/albums/:id/members`,
`GET|POST|DELETE /api/albums/:id/share`
- `POST /api/photo-batch/copy`, `POST|DELETE /api/photos/:id/tags`, `GET /api/tags?q=`
- `PATCH /api/auth/me/preferences`; account payload `GET /api/auth/me` and photo
payloads expose `preferences`, `exif`, `tags`, `uploader` and `download_url`.
## 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.
## Album sorting
The album list can order every card by most recent change (the default), by title,
or by the number of items, and the chosen key can be reversed from the same menu.
Cards of device albums and server albums are ordered together, so one ordering rule
covers the whole list; a device album's change date is the newest item it holds,
which makes the default key meaningful for it as well. The choice — key and
direction — is made from the sort control in the top bar and is remembered on the
device in DataStore, alongside the other display settings; it is not synced, so each
device keeps its own ordering.
## Testing
```sh
cd android && ./gradlew test
```
Unit tests cover URL resolution, error mapping, token refresh, the offline
cache/repository fallback, page merging, the object cache key, SSE event
parsing/reconnect, HEIC detection, and the panorama camera math. Robolectric
Compose tests cover the albums grid/offline banner, the viewer swipe navigation,
and upload statuses. Request-level tests cover the album/tag/preferences
operations behind the attribute/action panels, and DTO tests cover the rich photo
and account payloads (`exif`, `tags`, `uploader`, `preferences`).
Device-only checks (real server E2E, Wi-Fi-only gating, background continuation,
HEIC conversion of a real asset) require a device or emulator.