- роль локального файла определяется меткой провенанса, а не состоянием
индекса: собственное превью выправляет запись (MarkPreview) вместо
повторной загрузки, постер видео связывается с самим видео;
- изменённое содержимое заменяет серверную копию только после сверки
original_checksum (ReplaceUpload): прежняя копия удаляется мягко, а при
расхождении принятая загрузка отбрасывается и элемент не разгружается;
- файл не разгружается, пока по нему есть незавершённая загрузка;
- восстановленный оригинал помечается UPLOADED, чтобы следующий проход не
разгрузил его снова;
- миграция 8→9 добавляет upload_jobs.replacesPhotoId.
Связанная пара «альбом устройства → серверный альбом» становится миррором:
серверные фото материализуются на устройстве (оригинал в режиме копий, превью/
постер в переносе), состав меняется в обе стороны.
- Признак происхождения (OWN/MIRROR) в индексе и метке провенанса; БД v8.
- Материализация серверных фото (MirrorManager) с датой съёмки и меткой.
- Дедуп, материализация ровно в один бакет при n:1, baseline «только новое»/«всё».
- Двустороннее удаление с учётом прав; для зеркал намерением считается только
явное удаление в приложении (надгробие), пропавшая локальная копия
материализуется заново — иначе терялись серверные оригиналы.
- Неудачное чтение серверного списка не считается пустым альбомом.
- Устойчивое выключение синхронизации (syncDisabledAt): восстановление
происхождения не возвращает связь.
- Очередь загрузки — одно дренирующее задание вместо задания на файл
(JobScheduler переставал запускать остаток на больших альбомах).
- Дебаунс наблюдателя MediaStore и кэш sha256 по (size, dateModified).
Покрыто unit-тестами и instrumented-тестами (режимы A/B/C и переключения,
двусторонний миррор, объём 100 фото + 20 видео на тест).
Возврат из переноса в копии скачивает оригиналы с сервера, но ход нигде не
отражался: карточка умела показывать только загрузку, а восстановление шло в
области экрана и обрывалось при уходе с него.
Добавлен RestoreCoordinator: выполняет восстановление в области приложения
(уход с экрана больше не прерывает), ведёт ход «альбом -> восстановлено из
всего» и защищает от одновременного запуска по одному альбому. RestoreManager
сообщает ход по каждому элементу и вынесен за интерфейс AlbumRestorer; SyncCheck
ходит через координатора и, как и раньше, дожидается завершения.
Карточка альбома показывает «Копии — восстановление N%» (пульсирует), экран
альбома устройства — строку «Восстановление оригиналов: X из Y» с полосой.
Изменение OpenSpec: android-album-restore-progress.
Карточка альбома устройства получает третью строку: метку режима («Копии»/
«Перенос») и ход загрузки. Процент считается как (uploaded + running) / total
для обоих синхронизируемых режимов, пульсирует только при активной загрузке,
показывает очередь, ожидание Wi-Fi и ошибку. В режиме переноса карточка
показывает и карту памяти, и облако — режимы различает метка, а не значки.
Разгрузка оригиналов теперь требует подтверждённой серверной копии в текущем
целевом альбоме: устаревший serverPhotoId перепроверяется по альбому, смена
цели переносит уже загруженные фото и отклоняется, если перенести нельзя.
Потеря серверной копии видна на карточке и на экране альбома, автозатирания
превью нет.
Изменения OpenSpec: android-album-card-sync-status, safe-album-offload.
Unit-тесты планировщика, координатора, политики и восстановления, Compose-
тесты маркера и шита, instrumented-проверка удаления превью с устройства и
E2E-сценарии потери/возврата серверной копии. E2E не прогонялись: нужен
эмулятор и сервер на 10.0.2.2:8088.
В режиме переноса, если серверное фото исчезает из целевого альбома,
разгруженное превью помечается состоянием ORPHANED: в сетке показывается
перечёркнутое облако, элемент можно удалить с устройства, а вернувшийся
оригинал заливается заново. Детекция — в reconcile по живым фото альбома,
возврат серверного фото снимает пометку.
Выбор порядка раньше действовал только на серверные альбомы: карточки устройства
были закреплены сверху и не сортировались, а направления не было вовсе.
- порядок применяется ко всему списку: карточки устройства и серверные
упорядочиваются вместе единым компаратором
- альбом устройства получает дату последнего изменения (самый свежий элемент,
как при выборе обложки), поэтому «по времени изменения» осмыслен и для него
- обратный порядок инвертирует выбранный ключ; карточка без даты остаётся
последней в любом направлении
- пара (ключ, направление) хранится в DataStore за узким портом
AlbumSortPreference; меню сортировки получило переключатель «Обратный порядок»
- README: раздел Album sorting обновлён
Тестовая активность Compose (androidx.activity.ComponentActivity) объявляется
зависимостью ui-test-manifest, подключённой как debugImplementation, поэтому в
release-манифесте её нет и createComposeRule падает на testReleaseUnitTest.
Источники тестов вариант-агностичны, поэтому прогон один раз на debug даёт то
же покрытие и не тащит тестовую активность в production-манифест.
LiveAlbumsE2ETest против локального сервера и двух аккаунтов: участник
регистрируется на устройстве, владелец создаёт альбом и приглашает его,
альбом появляется в списке без обновления, переименование меняет открытый
экран, исключение убирает альбом и возвращает экран к списку.
- E2E: хелперы invite/accept/rename/remove и получение id пользователя
Приложение подписывается на персональный поток GET /api/sse/me и применяет
album.created/updated/deleted к кэшу Room, поэтому список и открытый экран
следуют изменениям с другого устройства или из веба без ручного обновления.
- PersonalSseClient и парсер событий жизненного цикла; подписка живёт вместе с
сессией (AlbumsViewModel), закрывается на выходе
- AlbumRepository.apply: upsert альбома или удаление его вместе с фото;
при переподключении список догоняется серверным снимком
- потеря доступа (удаление альбома или исключение участника) убирает альбом из
списка, а открытый экран возвращается к списку
- README: раздел Live updates и ограничение с истечением токена
E2EFlowTest failed because the system media-permission dialog stole focus and
left no compose hierarchy (orchestrator clears package data between tests).
Grant media access in an outer rule before the activity launches, and dismiss the
all-files access prompt so it does not block taps.
Album panel and photo sheet with attributes/actions for every album (server and
device), local album rename/cover/delete, EXIF attribute settings, and a unified
device-gallery sync check:
- add repository "Атрибуты и действия" panel (swipe left) on server and device
albums: cover, info, navigation, rename/delete; album settings screen; photo
sheet (swipe up) with info, tags and actions; EXIF attribute settings backed by
profile preferences
- local albums use local metadata and actions: app-only display name and cover
(new device_album_meta table), delete removes photos from the phone via the
system delete consent
- unified mode x state policy (GallerySyncPolicy/SyncCheck) runs on start and on
album open: upload/offload in move, restore originals in copy/off
- fix move->copy restore, offload of any linked-but-local original, provenance
recovery that no longer overwrites the album mode, serialized reconcile and
duplicate-upload suppression; prompt for "all files" access on start
- album card caption: name (with linked server name when different) and storage
icons (memory card / cloud) with item count
Sync delta specs into android-client and archive the change
add-mobile-album-photo-actions.
Verified: unit/Robolectric suite green, :app:assembleDebug, :app:lint, and the
DeviceSyncE2ETest instrumented suite on the emulator (7/7).
These network, repo, upload and image tests existed only in the main checkout because the old unanchored data/ rule ignored them. With the rule anchored to /data/ they now belong to the tracked data layer.
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.
Show a spinner in the top-right of the photo viewer while the original is
being loaded on zoom, so the wait is visible. Only the original triggers it;
the fitted preview and a re-shown preview never do.
Cover the visibility gate with a unit test.
OpenSpec change: add-mobile-photo-zoom
Add pinch/pan/double-tap zoom to the full-screen photo viewer, mirroring the
web viewer semantics:
- scale is a multiplier of the natural size, capped at 200%; the fitted view
is the lower bound
- double tap toggles fitted view <-> 100% natural anchored at the tap
- the original loads on zoom, with the preview kept as a placeholder so
entering zoom never flashes a black surface
- the pager is disabled while zoomed (drags pan the image) and the zoom resets
when leaving a photo
- video and 360 panoramas are unaffected
Implement PhotoZoom (zoom math) and ZoomableImage (gestures), wire it into
ViewerContent/MediaPage, and cover it with unit, Compose and device tests.
OpenSpec change: add-mobile-photo-zoom
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.