feat(android): add native Android client
Kotlin + Jetpack Compose client for a self-hosted instance: manual server setup and health check, auth with Keystore-backed tokens, album and photo browsing with SSE live updates, upload queue in WorkManager with HEIC-to-JPEG conversion, and Room/Coil offline cache. Add an android Makefile target and revise project.md §17.
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
# 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)
|
||||
```
|
||||
|
||||
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 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.
|
||||
|
||||
## 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.
|
||||
|
||||
Device-only checks (real server E2E, Wi-Fi-only gating, background continuation,
|
||||
HEIC conversion of a real asset) require a device or emulator.
|
||||
Reference in New Issue
Block a user